Supabase + Angular 用户管理实战:Magic Link 无密码登录、个人资料与头像存储完整落地
发布时间:2026/9/7 10:17:23 作者:尧图编辑部 阅读量:1,286

Supabase Angular 用户管理实战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/angular-user-management示例工程讲解如何用 Angular supabase/supabase-js从零搭建一套完整的用户管理应用包括 Magic Link邮箱免密登录、profiles资料表设计、Row Level SecurityRLS行级安全策略以及基于 Supabase Storage 的头像上传与展示。读完本文你将掌握一套可直接复制的 认证 资料 存储 全流程实现方案并理解各功能背后的数据库与客户端调用原理。示例工程概览与核心功能该示例演示了如何构建一个 Supabase Angular 用户管理应用其三个核心功能点非常典型几乎覆盖了大多数业务 App 的起步需求Magic Link 无密码登录passwordless用户输入邮箱Supabase Auth 下发一次性登录链接点击即完成登录用户个人资料管理登录后读取/更新profiles表中的用户名、网站与头像地址头像上传Supabase Storage将图片上传到avatars存储桶并在页面中即时回显。整个工程是一个由 Angular CLI 创建的标准应用采用经典的 NgModule 架构把 Supabase 相关能力收敛到一个可注入的SupabaseService中组件只负责 UI 与交互结构清晰、易于移植到真实项目。工程源码位于 examples/user-management/angular-user-management核心依赖见 package.json为angular/core^21.1.1与supabase/supabase-js^2。工程目录结构速览先整体浏览一下源码布局方便后续对照阅读examples/user-management/angular-user-management/ ├── src/ │ ├── app/ │ │ ├── account/ # 个人资料展示与编辑登录后可见 │ │ ├── auth/ # Magic Link 登录表单未登录时可见 │ │ ├── avatar/ # 头像上传与展示子组件 │ │ ├── app.component.* # 根组件根据登录态切换 auth / account │ │ ├── app.module.ts # NgModule 声明 │ │ └── supabase.service.ts # 封装 Supabase 客户端与业务方法 │ ├── environments/ │ │ └── environment.ts # Supabase URL 与 Key 配置 │ ├── index.html │ ├── main.ts # 平台启动入口 │ └── styles.css ├── angular.json └── package.json第一步创建 Supabase 项目并获取凭据首先在 Supabase 控制台创建一个新项目。创建完成后进入项目的Settings → API区域可以找到两样关键凭据Project URL形如https://project-ref.supabase.copublishable keyanon / public key当前示例环境变量命名为supabasePublishableKey即 anon 公开密钥。这两个值需要填入 Angular 端的环境配置文件详见下文第三步建议妥善保管并注意anon key 是公开的真正的权限管控依赖数据库的 RLS 策略与 Storage 存储桶策略而不是靠隐藏这个 key。第二步用 SQL 建立数据库表、RLS 与存储桶在 Supabase 的 SQL Editor 中执行下面整段 SQL。它会一次性完成三件事建profiles表、开启并配置 Row Level Security行级安全、创建avatars存储桶及访问策略。原样 SQL 见示例 README.md。-- Create a table for public profiles create table profiles ( id uuid references auth.users on delete cascade not null primary key, updated_at timestamp with time zone, username text unique, avatar_url text, website text, constraint username_length check (char_length(username) 3) ); -- Set up Row Level Security (RLS) 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); -- Set up Storage insert into storage.buckets (id, name) values (avatars, avatars); -- Set up access controls for storage. Allows downloading object with public key create policy Avatar images are publicly accessible. on storage.objects for select using (bucket_id avatars and storage.allow_any_operation(array[object.get_authenticated_info, object.get_authenticated])); create policy Anyone can upload an avatar. on storage.objects for insert with check (bucket_id avatars);关键点拆解1.profiles表结构设计id直接引用auth.usersSupabase Auth 内置用户表on delete cascade保证用户注销时资料一并删除且该列同时是主键username带unique约束并配合username_lengthcheck 约束要求用户名至少 3 个字符updated_at用于记录资料最后修改时间客户端在更新时会同步写入见下文SupabaseService.updateProfile的源码行为。2. RLS 三策略的含义select策略对所有人开放using (true)典型社交产品公开资料可被任何人浏览的写法insert策略用with check ((select auth.uid()) id)只允许用户插入id等于自己auth.uid()的行——即只能创建自己的资料update策略同理仅允许修改属于自己的行。3. 存储桶与对象策略insert into storage.buckets创建名为avatars的公开桶第一条对象策略让头像图片可被公开读取/下载第二条策略放开avatars桶的插入权限示例为了演示简便允许任何人上传生产环境建议替换为auth.uid()归属校验等更细粒度约束。说明示例 SQL 中调用的storage.allow_any_operation(...)与object.get_authenticated_info等为 Supabase 存储模块提供的辅助函数/上下文键。若你的项目使用的 Supabase 版本策略签名不一致请以对应版本文档中推荐的策略写法为准。第三步配置 Angular 环境变量编辑 src/environments/environment.ts将上一步获取的两项凭据填入export const environment { production: false, supabaseUrl: YOUR_SUPABASE_URL, supabasePublishableKey: YOUR_SUPABASE_PUBLISHABLE_KEY, }该文件是示例应用的开发环境配置沿用 Angular CLI 惯例你可以为生产环境另建environment.prod.ts并设置production: true。supabaseUrl对应 Project URLsupabasePublishableKey对应 anon/publishable key。第四步封装 Supabase 客户端——核心服务 SupabaseService与很多把createClient直接撒在各组件里的写法不同本示例把所有 Supabase 交互收敛到单例服务 src/app/supabase.service.ts 中。它用 Angular 依赖注入的Injectable({ providedIn: root })声明任何组件都能直接注入使用。import { Injectable } from angular/core import { AuthChangeEvent, createClient, Session, SupabaseClient, User } from supabase/supabase-js import { environment } from ../environments/environment export interface Profile { id?: string username: string website: string avatar_url: string } Injectable({ providedIn: root, }) export class SupabaseService { private supabase: SupabaseClient constructor() { this.supabase createClient(environment.supabaseUrl, environment.supabasePublishableKey) } // ... }Profile接口描述了profiles表中当前应用用到的三个业务字段外加可选的id与数据库 schema 一一对应。构造函数中通过createClient(url, key)实例化SupabaseClient后续方法都基于该客户端。各业务方法逐一解读获取当前用户async getUser(): PromiseUser | null { const { data, error } await this.supabase.auth.getUser() if (error) { return null } return data.user }auth.getUser()是当前推荐的方式——它会向 GoTrueSupabase Auth 后端校验本地会话中的 access token 有效性后返回用户出错如未登录、token 过期时返回null。监听认证状态变化authChanges(callback: (event: AuthChangeEvent, session: Session | null) void) { return this.supabase.auth.onAuthStateChange(callback) }onAuthStateChange会在登录、登出、token 刷新等事件触发时回调是本应用实现登录后自动从登录页切到资料页的关键订阅点。读取当前用户资料profile(user: User) { return this.supabase .from(profiles) .select(username, website, avatar_url) .eq(id, user.id) .single() }按id精确查询profiles单条记录只取三个展示字段。注意.single()期望结果集最多一条配合主键查询很合适。发送 Magic LinksignIn(email: string) { return this.supabase.auth.signInWithOtp({ email }) }signInWithOtp即免密登录接口Supabase Auth 会向该邮箱发送一封含一次性登录链接Magic Link的邮件同时若启用也支持把 OTP 验证码回传到前端。更新 / 创建资料upsertupdateProfile(profile: Profile) { const update { ...profile, updated_at: new Date(), } return this.supabase.from(profiles).upsert(update) }这里使用upsert而非update用户在首次登录后若还没有profiles记录需要插入新行已有记录则原地更新一个调用兼容两种场景。updated_at: new Date()与数据库的updated_at列呼应。头像下载与上传downLoadImage(path: string) { return this.supabase.storage.from(avatars).download(path) } uploadAvatar(filePath: string, file: File) { return this.supabase.storage.from(avatars).upload(filePath, file) }两者都指向avatars存储桶。download把对象以 Blob 形式取回以便前端预览upload支持自定义对象路径。第五步根组件按登录态分流页面根组件 src/app/app.component.ts 只做一件事维护当前user状态并在模板里根据状态渲染不同子组件。export class AppComponent implements OnInit { user: User | null null async ngOnInit() { this.user await this.supabase.getUser() this.supabase.authChanges(async () { this.user await this.supabase.getUser() }) } }启动时先尝试恢复会话随后订阅认证状态变化——无论是用户点击登录邮件完成认证还是点击 Sign OutonAuthStateChange都会触发重新拉取user并驱动视图切换。模板 src/app/app.component.html 的分流逻辑非常直白app-account *ngIfuser [user]user/app-account app-auth *ngIf!user/app-authuser非空时显示资料管理页app-account并把用户对象作为Input传入为空则显示登录页app-auth。第六步Magic Link 登录表单AuthComponent登录组件 src/app/auth/auth.component.ts 使用 Angular 响应式表单ReactiveFormsModule收集邮箱并触发免密登录export class AuthComponent { loading false signInForm: FormGroup constructor( private readonly supabase: SupabaseService, private readonly formBuilder: FormBuilder ) { this.signInForm this.formBuilder.group({ email: , }) } async onSubmit(): Promisevoid { try { this.loading true const email this.signInForm.value.email as string const { error } await this.supabase.signIn(email) if (error) throw error alert(Check your email for the login link!) } catch (error) { if (error instanceof Error) { alert(error.message) } } finally { this.signInForm.reset() this.loading false } } }需要注意的实现细节通过this.supabase.signIn(email)走到上文的signInWithOtp成功后弹出提示Check your email for the login link!——因此真正的登录动作发生在用户点击邮件链接后由第五步的onAuthStateChange自动感知并切换页面提交期间loading true置灰按钮finally中无论成败都会重置表单模板 src/app/auth/auth.component.html 通过[formGroup]/formControlNameemail将输入框绑定到表单并声明aria-livepolite提升可访问性。第七步个人资料读取与更新AccountComponent资料页 src/app/account/account.component.ts 通过Input() user接收当前登录用户其职责分为两段。初始化并回填表单async ngOnInit(): Promisevoid { await this.getProfile() const { username, website, avatar_url } this.profile this.updateProfileForm.patchValue({ username, website, avatar_url, }) }组件模板与逻辑分离表单用响应式方式绑定username、website、avatar_url三个控件见 account.component.html。其中 email 输入框只读展示[value]user.email disabled因为邮箱由 Auth 系统管理不属于可编辑资料。拉取资料getProfileasync getProfile() { try { this.loading true const { data: profile, error, status } await this.supabase.profile(this.user) if (error status ! 406) { throw error } if (profile) { this.profile profile } } catch (error) { if (error instanceof Error) { alert(error.message) } } finally { this.loading false } }一个实用细节当用户还没创建过资料时PostgREST 返回status 406Not Acceptable.single()无匹配行的典型响应。代码把 406 视为暂无资料而不是异常从而允许表单从空值开始——随后由updateProfile的upsert完成首次建档。提交更新updateProfileasync updateProfile(): Promisevoid { try { this.loading true const username this.updateProfileForm.value.username as string const website this.updateProfileForm.value.website as string const avatar_url this.updateProfileForm.value.avatar_url as string const { error } await this.supabase.updateProfile({ id: this.user.id, username, website, avatar_url, }) if (error) throw error } catch (error) { if (error instanceof Error) { alert(error.message) } } finally { this.loading false } }这里显式带上id: this.user.id与服务端 RLS 的using ((select auth.uid()) id)校验配合确保只能写自己的行。模板中更新与登出按钮分别绑定updateProfile()与signOut()signOut()内部调用supabase.auth.signOut()登出事件会被根组件订阅捕获页面自动回到登录态。头像联动avatar.component上传成功后通过Output() upload发出新对象路径AccountComponent 的updateAvatar(event)把值patchValue进avatar_url控件并立即调用updateProfile()持久化——用户换头像即拍即存无需额外点击保存。第八步头像组件——Blob 预览 随机路径上传头像子组件 src/app/avatar/avatar.component.ts 是一个典型的上传 回显复合控件设计上有两个值得借鉴的点。预览不走公开 URL而是下载 Blob 并用对象 URL 展示async downloadImage(path: string) { try { const { data } await this.supabase.downLoadImage(path) if (data instanceof Blob) { this._avatarUrl this.dom.bypassSecurityTrustResourceUrl(URL.createObjectURL(data)) } } catch (error) { if (error instanceof Error) { console.error(Error downloading image: , error.message) } } }通过storage.from(avatars).download(path)取得文件Blob用浏览器URL.createObjectURL(data)生成临时对象 URL因为是运行时动态 URL必须经DomSanitizer.bypassSecurityTrustResourceUrl包装成SafeResourceUrl否则 Angular 会因安全策略拦截img [src]的绑定。上传路径用随机文件名避免覆盖冲突async uploadAvatar(event: any) { try { this.uploading true if (!event.target.files || event.target.files.length 0) { throw new Error(You must select an image to upload.) } const file event.target.files[0] const fileExt file.name.split(.).pop() const filePath ${Math.random()}.${fileExt} await this.supabase.uploadAvatar(filePath, file) this.upload.emit(filePath) } catch (error) { if (error instanceof Error) { alert(error.message) } } finally { this.uploading false } }用Math.random()生成随机文件名保留原扩展名避免不同用户上传同名文件相互覆盖上传成功后把filePath通过Output() upload抛给父组件见第七步的联动逻辑。模板 src/app/avatar/avatar.component.html 用隐藏的input typefile acceptimage/*触发文件选择change事件调用上传上传中按钮文案切换为 Uploading ...并通过[disabled]uploading防重复提交。第九步安装依赖并启动开发服务器回到工程根目录依次执行npm install安装完成后启动 Angular 开发服务器npm startnpm start实际执行的是 Angular CLI 的ng serve见 package.json 中的 scripts 定义。然后在浏览器访问http://localhost:4200/Angular CLI 会开启热更新修改任意源码后页面自动重新加载。若在 Supabase 控制台开启了邮箱确认首次登录还需要真实邮箱收取 Magic Link 邮件完成验证。从示例到真实项目可复用的工程化要点结合整个实现可以从源码中提炼出几个适用于真实 Angular 项目的模式把 Supabase 收敛为单一服务supabase.service.ts 用providedIn: root的 DI 服务封装createClient与全部业务方法组件层只依赖业务方法签名后续切换/扩展数据访问策略时改动面最小认证状态由事件驱动统一驱动 UI根组件只订阅一次onAuthStateChange登录/登出/资料页展示完全由状态流驱动避免各组件自行轮询或重复判断upsert 406 容忍的组合让首次建档与日常更新共用同一段代码配合 PostgREST 无匹配行时返回 406 的语义做空态兜底对象存储路径随机化用随机文件名 原扩展名构造对象路径天然规避同桶内的命名冲突与缓存串扰。延伸学习本示例是仓库 examples/user-management 下多框架用户管理示例的 Angular 分支同目录还提供了 nextjs-user-management、svelte-user-management、expo-user-management 等版本可用于对照不同前端框架接入 Supabase 的差异数据库迁移与 RLS 策略的工程化书写可参考仓库根目录 supabase/migrations 与 supabase/seed.sql应用与 Supabase 项目联调、本地启动的通用配置见仓库根目录 supabase/config.toml理解anon角色、bucket 等基础概念后可以更好地为本示例扩展更严格的对象级访问策略。【免费下载链接】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),仅供参考