基于 Supabase 与 Vue 3 构建用户管理应用:Magic Link 登录、数据库与头像存储全流程实战

基于 Supabase 与 Vue 3 构建用户管理应用:Magic Link 登录、数据库与头像存储全流程实战
基于 Supabase 与 Vue 3 构建用户管理应用Magic Link 登录、数据库与头像存储全流程实战【免费下载链接】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本文以开源仓库 examples/user-management/vue3-user-management 为例系统讲解如何用 Vue 3Composition API script setup结合 Supabase 从零搭建一个带邮箱 Magic Link 免密登录、用户资料读写、头像上传展示的完整用户管理应用。读完本文你将掌握 Supabase Auth / Database / Storage 三件套与 Vue 3 的整合套路并理解每一段 SQL 与每一段前端代码背后的设计意图可直接照搬到自己的 Vite Vue 3 项目中。示例项目概览与技术栈这是一个小而完整的 Vue 3 单页应用示例围绕用户中心这一常见业务场景串联起 Supabase 的三项核心能力Supabase Auth用邮箱 一次性密码Magic Link完成免密登录Supabase Database存储与检索用户profiles资料用户名、个人网站、头像地址Supabase Storage把头像图片上传到avatarsbucket并可公开下载展示。项目采用的依赖与脚本可直接从 package.json 看到{ name: vue3-user-management, type: module, scripts: { dev: vite, build: vite build, preview: vite preview }, dependencies: { supabase/supabase-js: ^2, vue: ^3.5.30 }, devDependencies: { vitejs/plugin-vue: ^6.0.4, vite: ^7.3.1 } }从依赖版本可以看到这是 Vite 7 Vue 3.5 Supabase JS v2 的现代组合构建工具配置非常精简vite.config.js 仅注册了vitejs/plugin-vue一个插件入口 HTML 通过script typemodule src/src/main.js挂载应用。目录结构划分清晰各文件职责单一examples/user-management/vue3-user-management/ ├── src/ │ ├── main.js # 应用入口mount 到 #app │ ├── App.vue # 顶层路由分流已登录显示 Account未登录显示 Auth │ ├── supabase.js # 创建并导出 supabase 客户端单例 │ ├── store.js # 全局响应式 store存放 user │ ├── style.css # 深色主题与栅格/表单样式 │ └── components/ │ ├── Auth.vue # 邮箱输入 发送 Magic Link │ ├── Account.vue # 资料表单的读取/更新与退出登录 │ └── Avatar.vue # 头像上传与下载展示支持 v-model:path ├── index.html ├── vite.config.js └── package.json一、前置准备创建 Supabase 项目并注入环境变量在运行应用之前需要先创建一个 Supabase 项目拿到 URL 和密钥后写入.env文件。示例约定使用 Vite 的import.meta.env机制读取两个环境变量见 src/supabase.jsimport { createClient } from supabase/supabase-js const supabaseUrl import.meta.env.VITE_SUPABASE_URL const supabasePublishableKey import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY export const supabase createClient(supabaseUrl, supabasePublishableKey)因此需要在项目根目录创建.env文件该文件通常应被加入.gitignore避免密钥泄露VITE_SUPABASE_URL你的项目地址形如 https://xxxx.supabase.co VITE_SUPABASE_PUBLISHABLE_KEY你的 Publishable Key两点需要注意只有以VITE_前缀开头的变量才会被 Vite 暴露给客户端代码这里使用的是Publishable Key发布用公钥而非 Service Role Key——Service Role Key 可以绕过行级安全策略RLS绝不能出现在浏览器端。示例代码全程使用可安全暴露于前端的密钥数据库层面的访问控制完全交给 RLS 策略来完成这正是后续 SQL 中建表即开 RLS的原因。环境变量在客户端为何可用从 Vite 的机制看import.meta.env.VITE_*是在构建期被静态替换为字面量的因此VITE_SUPABASE_URL、VITE_SUPABASE_PUBLISHABLE_KEY写错或缺失时构建不会直接报错但运行时createClient会收到undefined导致所有请求失败。实际运行时如果点击登录无反应或请求报错第一步应先检查.env中两个变量是否与项目仪表盘的设置页一致。二、数据库 Schemaprofiles 表 RLS 策略 Realtime Storage 全量建表 SQLREADME 给出了可直接在 Supabase SQL Editor 中执行的完整初始化脚本。它分为四块我们逐一拆解。1. 创建 profiles 用户资料表-- 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) );设计要点id uuid references auth.users not null主键直接引用 Supabase Auth 内置的auth.users表保证一个登录用户至多一条资料记录并天然建立外键关联。由于外键存在若用户被删除其资料也应被清理生产环境可进一步添加on delete cascade。username text unique与表级unique(username)是同一约束的两种写法作用是确保用户名全局唯一。check (char_length(username) 3)数据库层兜底校验用户名至少 3 个字符即便前端不做校验也无法写入非法值——这与 Account.vue 中前端无用户名长度校验形成呼应安全边界永远在服务端/数据库。2. 开启行级安全并定义三条访问策略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 ( (select auth.uid()) id ); create policy Users can update own profile. on profiles for update using ( (select auth.uid()) id );这是整个示例安全模型的灵魂需要逐条理解enable row level security开启 RLS 后若没有匹配的策略普通客户端对表的任何操作都会被拒绝即使持有合法的公钥select全公开using (true)表示任何已登录或匿名请求都可以读取所有人的资料这是个人主页公开可见的产品需求insert只能写自己with check ((select auth.uid()) id)校验写入行的id必须等于当前登录用户的auth.uid()防止越权创建他人资料update只能改自己using ((select auth.uid()) id)限定只能更新id等于当前用户的行。这里没有单独定义delete策略意味着普通用户无法删除任何资料行默认拒绝符合用户不可注销资料的最小权限原则。auth.uid()是 Supabase 在数据库层提供的内置函数返回当前 JWT 中的用户 ID前端无法伪造因此这套策略是可信的安全边界。3. 启用 Realtime 发布-- Set up Realtime! begin; drop publication if exists supabase_realtime; create publication supabase_realtime; commit; alter publication supabase_realtime add table profiles;Supabase Realtime 基于 PostgreSQL 的逻辑复制publication/subscription实现。这段脚本先重建supabase_realtime发布再把profiles表加入发布列表使该表的变更能够实时推送给订阅的客户端。在本示例中它更多是为实时能力预留后续若想让多端在线同步资料更新无需改表结构前端直接对profiles建订阅即可生效。4. 创建 avatars 存储桶与访问策略-- 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 );先在storage.buckets元数据表中插入名为avatars的存储桶存储对象storage.objects同样受 RLS 管辖因此必须显式授权允许所有人读取avatars桶中的对象头像需要公开展示允许任何人向该桶上传文件。需要强调的是当前存储桶策略并未限制上传者身份与文件大小/类型with check (bucket_id avatars)只约束目标桶。在面向真实生产环境时通常还需要叠加仅登录用户可上传校验auth.uid()、限制对象路径、配合服务端或客户端校验图片类型与体积等更细粒度的策略。原 README 的这段脚本是演示级的最小可用方案读者应结合自身安全要求扩展。完整 SQL 原样保存在 examples/user-management/vue3-user-management/README.md可直接在 Supabase 控制台 SQL Editor 中整段执行。三、初始化 Supabase 客户端与全局响应式状态客户端单例src/supabase.js前面已经看过supabase.js的实现用createClient创建唯一客户端实例并导出。整个应用Auth 登录、Account 读写、Avatar 上传都复用这一个实例这是supabase/supabase-jsv2 的标准用法。全局状态src/store.jsimport { reactive } from vue export const store reactive({ user: {}, })示例用 Vue 3 自带的reactive维护一个极简全局 store 占位用户状态没有引入 Pinia 等额外状态库。需要说明的是本示例实际上并没有深度使用store.user——真正的当前用户是谁判断是通过 App.vue 中读取 JWT claims 完成的见下节登录态由 Supabase Auth 自身维护。store的存在更多是演示如何预留一个全局可共享的响应式状态容器。四、登录分流与登录态判断JWT Claims 驱动视图切换应用入口 src/main.js 只有四行引入样式、引入根组件、createApp(App).mount(#app)。视图层的核心分流逻辑在 src/App.vuescript setup import { onMounted, ref } from vue import Account from ./components/Account.vue import Auth from ./components/Auth.vue import { supabase } from ./supabase const claims ref() onMounted(() { supabase.auth.getClaims().then(({ data }) { claims.value data.claims }) supabase.auth.onAuthStateChange(async () { const { data } await supabase.auth.getClaims() claims.value data.claims }) }) /script template div classcontainer stylepadding: 50px 0 100px 0 Account v-ifclaims :claimsclaims / Auth v-else / /div /template这是本示例最有借鉴价值的模式之一值得深挖三点1.claims作为登录态判据。组件挂载时调用supabase.auth.getClaims()读取当前会话 JWT 的 claims如果claims存在有值则渲染Account否则渲染Auth。注意这里并未使用supabase.auth.getSession()的user对象而是读取 claims——因为后续查询profiles表需要用到 JWT 中的sub用户 ID作为等值条件claims 一次读取、两处使用。2. 订阅认证状态变化。onAuthStateChange是 Supabase JS 的全局会话监听器当用户完成 Magic Link 登录、退出登录、令牌刷新token refresh时都会触发。示例在每次状态变化后重新拉取 claims从而让视图在已登录/未登录之间自动切换不需要手动刷新页面——这正是登录后自动从登录页跳到资料页的实现基础。3. 为什么不直接渲染资料组件。将登录表单与资料面板拆成两个独立组件Auth/Account由父组件App依据认证状态做条件渲染结构清晰、关注点分离认证状态管理在顶层UI 细节收敛在叶子组件。一个隐蔽但重要的细节示例在Account.vue中通过claims.value.sub而非auth.user().id来定位资料行。JWT 的subclaim 与auth.users.id一致因此let { data, error, status } await supabase .from(profiles) .select(username, website, avatar_url) .eq(id, claims.value.sub) .single()查询用.eq(id, claims.value.sub)精确匹配当前用户再配合.single()断言结果唯一。代码对status ! 406PGRST116表示未找到匹配行做了放行处理——首次登录时 profiles 表还没有该用户的行此时不应报错而是静默返回等待用户填写资料后 upsert 插入。这正是先登录、后建档流程的关键容错。五、Magic Link 免密登录实现Auth.vue登录表单组件 src/components/Auth.vue 完整代码如下script setup import { ref } from vue import { supabase } from ../supabase const loading ref(false) const email ref() const handleLogin async () { try { loading.value true const { error } await supabase.auth.signInWithOtp({ email: email.value }) if (error) throw error alert(Check your email for the login link!) } catch (error) { if (error instanceof Error) { alert(error.message) } } finally { loading.value false } } /script template form classrow flex-center flex submit.preventhandleLogin div classcol-6 form-widget h1 classheaderSupabase Vue 3/h1 p classdescriptionSign in via magic link with your email below/p div input classinputField typeemail placeholderYour email v-modelemail / /div div input typesubmit classbutton block :valueloading ? Loading : Send magic link :disabledloading / /div /div /form /template实现要点表单通过submit.preventhandleLogin拦截默认提交行为避免页面刷新SPA 的标配做法核心只有一行 API 调用supabase.auth.signInWithOtp({ email })。该方法会向指定邮箱发送一封含一次性登录链接的邮件用户点击链接即完成登录全程无需设置与记忆密码发送成功后用alert提示用户查收邮件。由于点击邮件中的链接会回到应用并完成会话建立App.vue中监听的onAuthStateChange随即触发claims更新后自动切换到Account组件loading状态在请求期间禁用提交按钮并将按钮文案切换为 Loading防止重复发送若未启用邮件模板自定义Supabase 默认会把邮件里的跳转地址指向项目的 Site URL本地联调时需要把 Auth 设置中的 Site URL 配为http://localhost:5173否则点击邮件链接无法正确回到本地应用。signInWithOtp属于 Supabase Auth 的 GoTrue 客户端实现。值得一提的是除了邮箱 OTP该系列 API 还提供signInWithPassword、signInWithOAuth第三方社交登录等本示例刻意只选用 Magic Link以展示零密码、低摩擦的登录体验。六、资料读取、更新与退出Account.vuesrc/components/Account.vue 承担三件事读取资料、保存upsert资料、退出登录。读取资料async function getProfile() { try { loading.value true let { data, error, status } await supabase .from(profiles) .select(username, website, avatar_url) .eq(id, claims.value.sub) .single() if (error status ! 406) throw error if (data) { username.value data.username website.value data.website avatar_url.value data.avatar_url } } catch (error) { alert(error.message) } finally { loading.value false } }组件挂载后立即执行getProfile()。查询列被精确限定为username, website, avatar_url避免拉取大字段行级过滤交给 RLS 的公开读策略与.eq(id, ...)双重保证。对 406 状态码的容错是首次登录尚无记录场景的关键——没有这行判断新用户进入页面就会被alert报错打断。保存资料upsert 语义async function updateProfile() { try { loading.value true const updates { id: claims.value.sub, username: username.value, website: website.value, avatar_url: avatar_url.value, updated_at: new Date(), } let { error } await supabase.from(profiles).upsert(updates) if (error) throw error } catch (error) { alert(error.message) } finally { loading.value false } }保存时使用.upsert()而非.insert()或.update()这是本示例的巧妙之处若该用户尚无资料行首次提交表单upsert按主键id执行插入若已有资料行则按主键冲突执行更新每次保存都会刷新updated_at与数据表中updated_at timestamp with time zone字段呼应方便后续追踪修改时间。提交对象始终携带id: claims.value.sub这是为了让行级写策略with check ((select auth.uid()) id)能够校验通过。表单通过submit.preventupdateProfile绑定提交按钮根据loading显示 Update 或 Loading ...Email 输入框只读展示邮箱不可由用户资料接口修改。退出登录async function signOut() { try { loading.value true let { error } await supabase.auth.signOut() if (error) throw error } catch (error) { alert(error.message) } finally { loading.value false } }supabase.auth.signOut()会清除本地会话并通知onAuthStateChange随后App.vue中claims被置空视图自动切回Auth登录页形成完整的认证闭环。模板结构模板把Avatar子组件与资料表单放在同一个form中头像位于顶部下方依次是只读 Email、用户名、个人网站与提交按钮最底部是独立的 Sign Out 按钮。文件上传成功后通过uploadupdateProfile触发一次资料保存保证头像路径及时落库详见下一节。七、头像上传与展示Avatar.vueStorage 全流程 v-model 双向绑定src/components/Avatar.vue 是 Storage 能力的集中体现也是组件间通信的示范。它接收path当前头像在桶内的对象路径与size展示尺寸以 em 为单位两个 prop并对外抛出upload与update:path事件。script setup import { ref, toRefs, watch } from vue import { supabase } from ../supabase const prop defineProps([path, size]) const { path, size } toRefs(prop) const emit defineEmits([upload, update:path]) const uploading ref(false) const src ref() const files ref() const downloadImage async () { try { const { data, error } await supabase.storage .from(avatars) .download(path.value) if (error) throw error src.value URL.createObjectURL(data) } catch (error) { console.error(Error downloading image: , error.message) } } const uploadAvatar async (evt) { files.value evt.target.files try { uploading.value true if (!files.value || files.value.length 0) { throw new Error(You must select an image to upload.) } const file files.value[0] const fileExt file.name.split(.).pop() const filePath ${Math.random()}.${fileExt} let { error: uploadError } await supabase.storage .from(avatars) .upload(filePath, file) if (uploadError) throw uploadError emit(update:path, filePath) emit(upload) } catch (error) { alert(error.message) } finally { uploading.value false } } watch(path, () { if (path.value) downloadImage() }) /script上传流程通过隐藏的input typefile acceptimage/*选择图片触发uploadAvatar校验确实选择了文件取原文件扩展名用Math.random()生成随机文件名拼出filePath如0.7391...png。随机命名可有效避免同名文件互相覆盖这也是将随机值作为存储对象路径的常见做法——生产环境可进一步考虑用 UUID 或用户 ID 目录 时间戳的规范便于审计与清理调用supabase.storage.from(avatars).upload(filePath, file)将文件放入avatars桶。注意此时尚未落库数据库里并没有该头像的引用通过emit(update:path, filePath)把新路径写回父组件绑定的avatar_url同时emit(upload)通知父组件。父组件Account.vue中这样绑定Avatar v-model:pathavatar_url uploadupdateProfile size10 /v-model:path是 Vue 3.4 推荐的defineModel之外的自定义 v-model 用法子组件update:path事件会更新父组件的avatar_url随后uploadupdateProfile触发资料 upsert把新路径持久化到profiles.avatar_url。上传与入库因此被拆成两个解耦的步骤中间由事件串起。下载展示流程组件用watch(path, ...)监听头像路径变化一旦path有值就调用downloadImagedownloadImage调用storage.from(avatars).download(path)拉取文件二进制再用URL.createObjectURL(data)生成可被img引用的临时对象 URL 存入src。这样即便桶的策略不允许公开 URL 直链也能通过客户端 SDK 展示内容模板中src有值时渲染img否则渲染一个占位空头像.avatar.no-image尺寸由sizeprop 以 em 控制。组件初次挂载时父组件会先getProfile()把已保存的avatar_url传入watch随即触发下载上传新头像后路径变更再次触发实现了上传后立刻看到新头像的即时反馈。八、运行、构建与整体流程串联启动命令在克隆本仓库并进入示例目录、完成.env配置与 SQL 初始化后npm run dev应用默认运行在http://localhost:5173Vite 默认端口此时浏览器里应出现登录表单输入邮箱点击 Send magic link前往邮箱点击 Supabase 发送的登录链接页面自动跳转到资料面板onAuthStateChange生效此时可上传头像、填写用户名与个人网站并保存点击 Sign Out 回到登录页完成一次完整的登录→建档→登出闭环。生产构建与本地预览分别使用npm run build与npm run preview见 package.json。一次完整保存请求的调用链回溯结合前述源码把用户点击 Update 保存资料在整条链路上的数据流串起来Account.vue 的updateProfile()组装updates含id claims.subsupabase.from(profiles).upsert(updates)发送请求PostgREST 依据 URL 上的Authorization: Bearer JWT执行 SQL数据库侧RLS 策略with check ((select auth.uid()) id)校验 JWT 对应用户与写入行id一致通过后写入或更新profiles若此前刚上传过头像则 Avatar.vue 已先把文件写入avatars桶存储 RLSbucket_id avatars校验avatar_url只是数据库中的一个文本路径整个写操作没有越过 RLS 的 Service Role 参与浏览器端持有的始终是 Publishable Key。这条链路清晰展示了 SupabaseAuth 出身份JWT sub、Postgres RLS 定权限、Storage 管文件、前端 SDK 做编排的安全架构范式也正是本示例最值得复用到真实业务中的部分。九、扩展建议与生产化注意事项示例定位是quick sample快速起步模板聚焦演示而非完整产品投入到生产前通常还需要补齐以下能力头像上传的健壮性对文件类型与大小做前端预检并在数据库/存储层叠加更细的约束与清理策略随机文件名的写法可替换为更规范的路径规划用signUrl/公开 URL 或 CDN 缓存替代每次download拉取降低请求开销。表单校验与反馈示例用alert()做错误提示用户名长度依赖数据库check约束兜底生产可引入表单校验库并在 UI 内联展示错误同时处理username唯一冲突可捕获唯一约束错误提示用户换名。登录能力增强可补充signInWithOAuthGitHub/Google 等、密码重置流程与邮箱确认页定制。Realtime 的应用建表脚本已把profiles加入supabase_realtime发布前端可在此基础上订阅资料变更实现多标签页/多端即时同步。状态管理升级示例用极简reactivestore 演示页面复杂后可平滑迁移到 Piniasupabase.js客户端单例模式无需改动。补充 delete 策略当前 schema 未给profiles定义删除策略若产品需要删除账户能力需显式添加对应 RLS 策略。十、参考文件索引示例说明与完整建表 SQLexamples/user-management/vue3-user-management/README.md依赖与脚本定义package.jsonVite 构建配置vite.config.jsSupabase 客户端创建与环境变量读取src/supabase.js应用入口与登录态分流src/main.js、src/App.vue登录 / 资料 / 头像三组件Auth.vue、Account.vue、Avatar.vue全局响应式状态示例src/store.js【免费下载链接】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),仅供参考

最新新闻

日新闻

周新闻

月新闻