Supabase Ionic Vue 用户管理实战:魔法链接登录、个人资料与头像上传全流程 Supabase Ionic Vue 用户管理实战魔法链接登录、个人资料与头像上传全流程【免费下载链接】supabaseThe Postgres development platform. Supabase gives you a dedicated Postgres database to build your web, mobile, and AI applications.项目地址: https://gitcode.com/GitHub_Trending/supa/supabase本文基于仓库示例 ionic-vue-user-management/README.md 编写并结合 示例源码 逐步拆解。目标读者是希望在 Ionic Vue Capacitor 移动/跨端应用中接入 Supabase Auth邮箱魔法链接登录、Storage头像图片与 Postgres Row Level Security行级安全策略的开发者。读完本文你将能独立复刻一个邮箱一键登录 → 完善个人资料 → 上传头像 → RLS 保护数据的完整用户系统并理解每一步背后的源码与数据库策略。该示例解决的是一个非常常见的业务场景用户通过邮箱魔法链接完成注册登录随后维护自己的个人资料用户名、网站、头像。它不像传统登录那样需要用户先设置密码而是用 Auth 的一次性密码链接OTP / magic link完成鉴权再配合 Storage 实现头像上传、配合 Postgres 行级安全策略保证每个用户只能读写自己的资料。示例要演示的核心能力根据 README.md本示例完整演示了以下三块能力及其协作方式用户注册登录基于 Supabase [Auth] 的邮箱魔法链接登录用户头像存储基于 Supabase [Storage] 的对象存储上传/下载图片前端交互基于 VueComposition API script setup的响应式页面。后端全部依赖 Supabase 托管的 Postgres 数据库与自动生成的 RESTful API无需自建服务器。技术栈一览前端Ionic —— 移动风格 UI 组件ion-page、ion-input、ion-button等与生命周期路由Capacitor —— 把 Web 应用包装成原生 App 的运行时桥接层Vue 3 —— 视图层与状态响应Supabase.jssupabase/supabase-jsv2—— 用户管理与数据同步的客户端 SDK。后端Supabase 托管的 Postgres 数据库为 Supabase.js 提供 RESTful API、实时Realtime订阅与行级安全策略支持。从 package.json 可以看到实际依赖版本ionic/vue ^8.8.1、supabase/supabase-js ^2、vue ^3.5.30、capacitor/camera ^8.0.2等项目基于 Vue CLI 构建提供serve/build/test:unit/test:e2e/lint脚本。仓库目录结构与职责速览在动手前先看清示例源码的布局相对仓库根目录文件职责src/main.ts应用入口装配 Ionic 与路由src/App.vue根组件监听 Auth 状态变化并做页面重定向src/supabase.ts读取环境变量并创建唯一的 Supabase 客户端src/router/index.ts路由表 登录态路由守卫src/views/Login.vue邮箱输入与魔法链接发送页src/views/Account.vue个人资料展示与编辑、退出登录src/views/HomePage.vueIonic 模板自带的空白占位页src/components/Avatar.vue摄像头拍照、上传到 Storage、回显头像capacitor.config.tsCapacitor 原生打包配置appId、webDir: dist等值得注意的是 App.vue 只渲染了一个ion-router-outlet /所有页面均为 Ionic 页面组件整个 App 就是路由 鉴权状态的薄壳。从零搭建5 步跑通完整示例1. 创建 Supabase 项目先在 Supabase 云平台注册账号并创建新项目等待数据库完成初始化。这一步会为你分配一个独立的托管 Postgres 实例。2. 执行 User Management Quickstart SQL数据库启动后进入项目控制台的SQL Editor标签页向下滚动找到名为User Management Starter建立一张可通过 API 访问的公开profiles表的预设脚本并运行。该脚本即为下文数据库侧设计一节展示的完整 SQL它会创建profiles表、开启行级安全RLS并写入三条策略、把profiles加入实时发布同时创建avatars存储桶与访问策略。3. 获取 API URL 与 anon Key进入 Project Settings齿轮图标→ API 标签页找到API URL—— 后续客户端请求的基地址anonkey—— 客户端专用公钥。anonkey 是客户端 API 密钥它允许匿名访问数据库直到用户登录用户登录后请求会自动切换到用户自己的登录令牌。这正是实现行级安全Row Level Security的机制前提细节见下文对应章节。重要提醒来自原 READMEsecretkey服务端密钥拥有绕过一切安全策略的完全数据访问权限。它必须严格保密只允许在服务端环境中使用绝不能出现在客户端或浏览器代码里。4. 配置环境变量原 README 给出的占位配置文件形如environment.tsexport const environment { // ... supabaseUrl: YOUR_SUPABASE_URL, supabaseKey: YOUR_SUPABASE_KEY };而当前仓库源码已经演进为构建期注入环境变量的方式。查看 src/supabase.ts 的完整实现import { createClient } from supabase/supabase-js const supabaseUrl process.env.VUE_APP_SUPABASE_URL const supabaseKey process.env.VUE_APP_SUPABASE_KEY if (!supabaseUrl) { throw new Error( Environment variable VUE_APP_SUPABASE_URL is not set. Please define it before starting the application. ) } if (!supabaseKey) { throw new Error( Environment variable VUE_APP_SUPABASE_KEY is not set. Please define it before starting the application. ) } export const supabase createClient(supabaseUrl, supabaseKey)可以看到它基于 Vue CLI 的VUE_APP_前缀约定从process.env中读取VUE_APP_SUPABASE_URL与VUE_APP_SUPABASE_KEY。如果缺少任一变量会直接抛错防止带着空配置悄悄启动。因此实际操作时应在示例根目录创建.env文件Vue CLI 约定会自动加载根目录.env内容形如VUE_APP_SUPABASE_URLYOUR_SUPABASE_URL VUE_APP_SUPABASE_KEYYOUR_SUPABASE_ANON_KEY之后createClient(url, key)产出的单例客户端会在整个 App 中被所有页面复用。5. 运行应用在示例目录执行ionic serve浏览器会自动打开https://localhost:8100/此时已经可以体验输入邮箱 → 查收魔法链接 → 点击链接登录 → 编辑资料/上传头像的完整闭环。一键部署选项原 README 还提供了 Vercel 的 Instant deploy 部署入口点击 Vercel 部署按钮后部署向导会引导你创建 Supabase 账号与项目安装 Supabase 集成后部署平台会自动为你写入全部相关环境变量VUE_APP_SUPABASE_URL、VUE_APP_SUPABASE_KEY部署完成即可直接使用无需手动配置密钥。客户端核心流程源码级解读下面把登录 → 会话恢复 → 资料读写 → 头像上传的关键链路逐一对照源码拆开讲。登录页发送魔法链接Login.vue 的登录逻辑非常精简核心只有一行const { error } await supabase.auth.signInWithOtp({ email: email.value });const handleLogin async () { const loader await loadingController.create({}); const toast await toastController.create({ duration: 5000 }); try { await loader.present(); const { error } await supabase.auth.signInWithOtp({ email: email.value }); if (error) throw error; toast.message Check your email for the login link!; await toast.present(); } catch (error: any) { toast.message error.error_description || error.message; await toast.present(); } finally { await loader.dismiss(); } };模板部分是一个 Ionic 表单form submit.preventhandleLogin包裹ion-input typeemail并用v-modelemail绑定输入值。signInWithOtp会向该邮箱发送一封包含登录链接的邮件——用户点击链接即完成鉴权全程无需设置/输入密码。示例还演示了用 Ionic 的loadingController/toastController做加载态与结果反馈错误信息优先取error_description再回退到error.message。路由守卫未登录不能进 /accountrouter/index.ts 定义了仅有的两条路由/登录与/account资料页随后用全局前置守卫拦截跳转router.beforeEach(async (to, _from, next) { const { data } await supabase.auth.getClaims() const claims data?.claims if (to.path /account !claims) { next(/) return } if (to.path / claims) { next(/account) return } next() })守卫通过supabase.auth.getClaims()读取当前会话的 JWT 声明claims。逻辑清晰对称没有登录态时访问/account一律踢回登录页已登录时停留在登录页也会被引导进资料页。根组件登录状态变化自动同步路由App.vue 是鉴权闭环的大脑它订阅了onAuthStateChangeasync function syncAuthRedirect() { const { data } await supabase.auth.getClaims(); const claims data?.claims; const path router.currentRoute.value.path; if (claims path /) { router.replace(/account); } else if (!claims path /account) { router.replace(/); } } const { data: { subscription }, } supabase.auth.onAuthStateChange(() { syncAuthRedirect(); }); onUnmounted(() { subscription.unsubscribe(); });当用户点击邮件里的魔法链接完成登录、或点击退出登录时Auth 状态变化会立刻触发重新评估路由位置例如从登录页跳到资料页并且组件卸载时会subscription.unsubscribe()释放订阅避免内存泄漏。这与路由守卫互为补充守卫负责进入前拦截状态订阅负责登录/登出后实时纠正。资料页读取与更新个人资料Account.vue 完整演示了读与写两个动作。读取资料getProfileconst { data: { claims } } await supabase.auth.getClaims(); if (!claims) throw new Error(No user logged in); email.value (claims.email as string) ?? ; const { data, error, status } await supabase .from(profiles) .select(username, website, avatar_url) .eq(id, claims.sub) .single(); if (error status ! 406) throw error; if (data) { profile.value { username: data.username, website: data.website, avatar_url: data.avatar_url, }; }要点用户的邮箱与sub用户 UUID都直接来自 JWT 声明查询按id claims.sub过滤.single()期望恰好一行status ! 406才抛错意味着记录尚不存在Not Acceptable时静默放行让空资料的新用户也能正常进入页面。页面在onMounted时调用getProfile()。更新资料updateProfileconst updates { id: claims.sub, ...profile.value, updated_at: new Date(), }; const { error } await supabase.from(profiles).upsert(updates); if (error) throw error;这里用了upsert存在则更新、不存在则插入因为插入策略约束auth.uid() id用户只能写入自己那行主键id由 JWT 的sub提供配合updated_at: new Date()实现资料变更时间戳的自动维护。退出登录signOut则调用supabase.auth.signOut()成功后通过 Ionic 路由router.push(/, forward, replace)回到登录页。头像组件拍照 → 上传 Storage → 下载回显Avatar.vue 是本示例中 Storage 能力的最佳范例。它通过v-model:path与父组件双向绑定头像文件路径upload通知父组件刷新资料。上传链路摄像头拍照到存储const photo await Camera.getPhoto({ resultType: CameraResultType.DataUrl, }); if (photo.dataUrl) { const file await fetch(photo.dataUrl) .then((res) res.blob()) .then( (blob) new File([blob], my-file, { type: image/${photo.format} }) ); const fileName ${Math.random()}-${new Date().getTime()}.${ photo.format }; const { error: uploadError } await supabase.storage .from(avatars) .upload(fileName, file); if (uploadError) { throw uploadError; } emit(update:path, fileName); emit(upload); }细节拆解使用 Capacitor 的Camera.getPhoto({ resultType: CameraResultType.DataUrl })唤起系统相机/相册拿到 DataUrl →fetch(dataUrl).then(r r.blob())转成 Blob → 再包装成带 MIME 类型的File→ 文件名用Math.random() 时间戳 扩展名保证唯一性 →storage.from(avatars).upload(fileName, file)上传到avatars桶 → 通过emit(update:path, fileName)把路径回传给父组件。下载回显存储到imgconst downloadImage async () { try { const { data, error } await supabase.storage .from(avatars) .download(path.value!); if (error) throw error; avatarUrl.value URL.createObjectURL(data!); } catch (error: any) { console.error(Error downloading image: , error.message); } };storage.download拿到二进制后用浏览器原生URL.createObjectURL生成本地预览 URL 赋给avatarUrl模板中img :srcavatarUrl完成回显同时用watch(path, ...)监听路径变化自动重新下载实现上传完成即刷新头像。数据库侧设计Postgres Row Level Security 与 Storage 策略RLS 是什么、为什么够安全本项目使用 Postgres 的Row Level Security行级安全README 中拼写为 Role Level Security做上层授权控制。当你在 Supabase 创建数据库时平台会自动填充authschema 与一批辅助函数。用户登录后会被签发一个携带authenticated角色与用户 UUID的 JWT。之后客户端所有带登录态的请求都会携带该令牌数据库据此对谁能读/写哪一行做细粒度控制——即使前端代码被篡改也无法越权操作别人的数据行。完整的 Quickstart 建表 SQL原 README 全量以下是原文档完整给出的精简 schema 与全部策略实际部署时直接粘贴到 SQL Editor 运行-- Create a table for Public Profiles create table profiles ( id uuid references auth.users not null, updated_at timestamp with time zone, username text unique, avatar_url text, website text, primary key (id), unique(username), constraint username_length check (char_length(username) 3) ); alter table profiles enable row level security; create policy Public profiles are viewable by everyone. on profiles for select using ( true ); create policy Users can insert their own profile. on profiles for insert with check ( auth.uid() id ); create policy Users can update own profile. on profiles for update using ( auth.uid() id ); -- Set up Realtime! begin; drop publication if exists supabase_realtime; create publication supabase_realtime; commit; alter publication supabase_realtime add table profiles; -- Set up Storage! insert into storage.buckets (id, name) values (avatars, avatars); create policy Avatar images are publicly accessible. on storage.objects for select using ( bucket_id avatars ); create policy Anyone can upload an avatar. on storage.objects for insert with check ( bucket_id avatars );逐段语义解读profiles 表主键id同时是外键引用auth.users并not null保证一条资料必然对应一个真实用户username全局唯一且通过check (char_length(username) 3)约束用户名至少 3 个字符这条约束与前端表单、upsert写入直接相关——不满足会返回数据库错误示例前端通过 toast 原样呈现给用户。启用 RLSalter table profiles enable row level security;是这一切的前提。开启后没有命中任何策略的读写都会被拒绝。三条策略与前端动作一一对应Public profiles are viewable by everyone.——for selectusing (true)所有profiles行可被任何人读取包含未登录的anon角色。这正是公开资料的语义Users can insert their own profile.——for insertwith check (auth.uid() id)只有目标行的 id 等于当前登录用户 UUID时才允许插入防止用户伪造他人 id 写入数据Users can update own profile.——for updateusing (auth.uid() id)只允许更新属于自己的行。配合客户端的upsert逻辑这三条策略构成了完整的任何人都能看、但只有本人能建/改的资料模型。Realtime 发布通过重建supabase_realtimepublication 并把profiles加入其中使该表支持实时变更推送——这是后续做资料实时同步README 技术栈一节提到的 realtime data syncing的数据库端开关。Storage 桶与策略先insert into storage.buckets创建名为avatars的公开桶然后Avatar images are publicly accessible.——for selectusing (bucket_id avatars)avatars桶内的对象允许公开读取任何人可看他人头像Anyone can upload an avatar.——for insertwith check (bucket_id avatars)任何登录用户都可以向该桶上传配合前面的公开读策略实现任何人可传、任何人可看的头像场景。这与前文 Avatar.vue 中storage.from(avatars)的调用一一对应客户端无需额外鉴权配置即可上传/下载权限完全收敛在数据库策略层。关键安全边界anon key 与 secret keyanonkey 是客户端公钥允许对数据库做匿名访问仅在用户登录后切换为本人令牌从而触发 RLS 生效secretkey服务端密钥拥有绕过全部安全策略的完全数据访问权限必须保密只用于服务端环境绝不能打包进客户端/浏览器代码。本示例能放心把钥匙放进前端正是因为有 RLS 兜底——密钥泄露风险之外的每一次数据访问都被数据库策略精确约束在公开读 本人写的范围内。小结这个示例的价值在于它用最小的代码量展示了 Supabase 三条核心产品线的协同AuthsignInWithOtp魔法链接登录 onAuthStateChange/getClaims驱动的会话与路由管理Login.vue、App.vue、router/index.tsPostgres RLSprofiles表、三条 select/insert/update 策略与auth.uid()的权限模型配合客户端upsert完成资料读写Account.vueStorageavatars公开桶 上传/下载策略配合 Capacitor Camera 实现拍照上传与URL.createObjectURL回显Avatar.vue。如果你需要把该示例集成进自己的 Ionic Vue 项目只需复用 src/supabase.ts 的客户端初始化、三个页面与路由守卫模式再在 SQL Editor 中运行上文完整建表脚本即可。仓库中同目录还包含 Jest 单元测试骨架tests/unit/example.spec.ts与 Cypress e2e 配置可通过npm run test:unit、npm run test:e2e验证工程质量。基于 Capacitorcapacitor.config.ts 中webDir: dist的集成配置也意味着同一套代码在ionic build后即可进一步桥接为 iOS / Android 原生应用。【免费下载链接】supabaseThe Postgres development platform. Supabase gives you a dedicated Postgres database to build your web, mobile, and AI applications.项目地址: https://gitcode.com/GitHub_Trending/supa/supabase创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考