认证
由功能开关控制的邮箱认证,配合安全的桌面会话存储。
认证通过 VITE_AUTH_ENABLED Flag-gated,默认值是 true。模板实现了
Supabase 邮箱密码登录、带六位验证码的注册、带 recovery code 的忘记/重置密码、
已登录 Account 页面、profile 编辑和 avatar 上传。
当前 renderer 即使在 VITE_AUTH_ENABLED=false 时,也会在挂载前校验
VITE_SUPABASE_URL 和 VITE_SUPABASE_PUBLISHABLE_KEY。本地开发桌面外壳时
可以使用真实值或占位值;真实 auth/data 流程需要 Supabase 项目。
运行时开关
| Gate | 默认值 | 效果 |
|---|---|---|
VITE_AUTH_ENABLED | true | 显示 auth 与 account surfaces。 |
| Supabase env validation | 当前必需 | URL 缺失、URL 无效或 publishable key 缺失时,app/renderer.tsx 渲染 SupabaseConfigErrorScreen。 |
| Supabase schema | 手动配置 | Account 与示例数据界面需要 profiles、todos 和 avatars。 |
关闭 auth surfaces 后,应用是一个没有登录 UI 的本地桌面外壳。计费仍由
VITE_BILLING_ENABLED 单独控制。
Auth flows
Auth routes 位于 app/routes/auth/。AuthCard 提供居中外壳,表单使用
react-hook-form 和 app/lib/auth/auth-schemas.ts 中的 Zod schemas。
| 流程 | Route | Supabase 调用 |
|---|---|---|
| 登录 | /login | supabase.auth.signInWithPassword |
| 注册 | /register | supabase.auth.signUp,带 full_name metadata |
| 验证注册 | /register verify step | supabase.auth.verifyOtp({ type: 'signup' }) |
| 重新发送注册验证码 | /register verify step | supabase.auth.resend({ type: 'signup' }) |
| 忘记密码 | /forgot-password | supabase.auth.resetPasswordForEmail |
| 重置密码 | /reset-password | verifyOtp({ type: 'recovery' })、updateUser({ password }),然后 signOut() |
| 退出登录 | Sidebar / Account | 通过 auth store 调用 supabase.auth.signOut() |
模板当前没有接入 OAuth provider。添加 Google、GitHub 或 Apple 时,需要配置 Supabase provider 并补充桌面 deep-link routing;如果新增 callback route,要把它 放进类型化 app-intent allow-list。
Session state
app/stores/auth-store.ts 是 renderer 中的 auth source of truth:
status:unknown、authenticated或unauthenticated- Supabase Auth 的
user和session - 来自
public.profiles的profile - profile refresh 与 sign-out actions
启动时它调用 supabase.auth.getSession(),然后订阅 onAuthStateChange。
SIGNED_IN、INITIAL_SESSION、TOKEN_REFRESHED 和 USER_UPDATED 会触发
profile refresh。SIGNED_OUT 会清空 user、session 和 profile。
安全桌面存储
app/lib/supabase/client.ts 中的 Supabase client 使用
secure-session-storage.ts 提供的自定义 storage adapter:
Supabase auth storage key
-> app/lib/supabase/secure-session-storage.ts
-> window.conveyor.secure
-> lib/main/secure-storage.ts
-> Electron safeStorage encrypted file under userData/secrets.bin所有 Supabase sessions 都存入固定 secure slot supabase-session。启动时会移除旧
localStorage session keys,避免 token 留在浏览器存储里。
当 secure storage 无法解密或读取值时,adapter 返回 null,Supabase 会把应用视为
已退出。写入时如果系统不可用加密,main process 返回 ENCRYPTION_UNAVAILABLE,
auth write 会失败,而不是静默把 token 明文保存。
Account 与 avatar
Account 页面是 app/routes/AccountPage.tsx,背后复用
app/routes/settings/sections/AccountSection.tsx。
| 界面 | Source | 后端要求 |
|---|---|---|
| Profile load | app/stores/auth-store.ts、app/services/profile.ts | 存在 profiles.id = auth.uid() 行。 |
| Profile update | updateProfile(userId, patch) | RLS 只允许用户更新自己的行。 |
| Avatar upload | app/services/avatar.ts | 私有 avatars bucket,5 MB 限制,PNG/JPEG/WebP,路径为 <user-id>/avatar.<ext>。 |
| Avatar display | useAvatarUrl 与 createSignedUrl | Signed URL TTL 是 7 天,并使用 profile.updated_at 做 cache key。 |
Avatar 和 profile 是 Example surfaces。它们展示了桌面文件选择、Storage 上传、signed URL 读取和 profile RLS 的模式,但发布前应按你的产品字段改造。
Redirects 与 deep links
当前邮箱流程基于 OTP,而不是 link callback。Supabase 发送验证码,用户在桌面应用中
输入。client 也设置了 detectSessionInUrl: false。
如果之后切换到链接式邮件模板或 OAuth,应在 Supabase Auth 允许
soar-electron://,但不要假设今天 /login 或 /reset-password deep links 已经
可用。当前 app-intent allow-list 接受 /settings、/account、/billing 等
shell routes。
验证认证
- 删除或破坏
VITE_SUPABASE_URL,确认应用挂载前出现SupabaseConfigErrorScreen。 - 用邮箱密码注册,输入六位验证码,确认应用回到 Home。
- 确认创建了用户 id 对应的
profiles行。 - 退出、重新登录、重启应用,并确认 session 通过 secure storage 持久化。
- 跑忘记/重置密码流程,确认更新密码后会退出登录。
- 在 Account 上传并移除 avatar。
