Back to skills
SKILL.md
Login Auto
ASecurity一套现成方案装好登录注册:密码/谷歌/邮箱验证码+改密
- 5 stars
- 0 votes
- 0 copies
- 1 view
- Added September 19, 2026
Works with
Security analysis
96/100- Installs packages at runtime which could introduce malicious dependencies
Pro scans all 2 files and shows the line behind each finding
npx -y skills add weiyi88/cc-code --skill login_auto --agent claude-codeAre you the author of Login Auto?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/weiyi88-login-auto)---
description: 一套现成方案装好登录注册:密码/谷歌/邮箱验证码+改密
disable-model-invocation: true
---
# Supabase Auth 通用登录系统实现指南
本 skill 是**与项目无关的通用登录逻辑指南**,适用于任意 Next.js + Supabase 项目。不绑定特定 i18n 框架、不绑定特定语言组合、不绑定特定 UI 库。三大场景全覆盖:
- **登录**:邮箱密码 + OAuth (Google/GitHub)
- **注册**:邮箱 + 验证码 + 密码 + 确认密码
- **修改密码**(忘记密码/未登录):邮箱 + 验证码 + 新密码
**邮件通道**:验证码邮件由 Supabase Auth 触发,经 Supabase 自定义 SMTP 接入 **Resend** 投递(不在代码里调 Resend API,不自造验证码存储)。
---
## Skill 启动流程(显式触发时按此走)
```
Step 1 整理项目当前登录逻辑
读项目 auth 相关代码(auth-modal/回调路由/profile/middleware)
→ 画「项目现状登录逻辑」ASCII 图
Step 2 对比 skill 标准逻辑 vs 项目现状
逐链路差异表: ✅一致 / ⚠️偏离 / ❌缺失
(标准逻辑见下方「三大场景」与 Phase 1-3)
Step 3 环境盘点 + 询问 key
检查: 依赖 / Supabase(URL+anon key, service_role可选)
/ Resend(API Key+发件域名) / OAuth(Client ID+Secret)
/ Dashboard(Confirm email关闭? SMTP配Resend? Redirect URLs? Providers?)
缺什么列清单 → 询问主人提供
Step 4 整理改动清单
输出表: 文件 | 现状 | 目标 | 动作
Step 5 调用 /cc-code:cc-code 执行
按角色串行 PM→Architect→Dev→QA 落地
(本 skill 只整理方案, 执行交 cc-code 工作流)
```
> 本 skill 主体是「标准逻辑 + 实现指南」,供 Step 2 对比、Step 4 改动参考。Step 5 不在本 skill 内写代码,交由 /cc-code:cc-code。
---
## 实现指南流程
```
Phase 0 → Phase 1 → Phase 2 → Phase 3 → Phase 4
环境检查 后端实现 前端UI 回调路由 构建验证
```
---
## Phase 0: 环境检查
### 0.1 检查已有 Supabase 配置
```
扫描:
├── package.json → @supabase/supabase-js 存在?
├── package.json → @supabase/ssr 存在?
├── lib/supabase/client.ts 存在?
├── lib/supabase/server.ts 存在?
├── middleware.ts (或 Next.js 16 的 proxy.ts) → 包含 supabase session 刷新?
├── .env.local → NEXT_PUBLIC_SUPABASE_URL + ANON_KEY 存在?
└── app/auth/callback/route.ts 存在?
```
> 若项目使用 i18n,额外检查其 Auth 命名空间是否已含本 skill 所需的错误消息 key(见文末「错误消息键名清单」)。i18n 框架与语言组合由项目自定,本 skill 不强制。
### 0.2 若缺失,先安装依赖
```bash
pnpm add @supabase/supabase-js @supabase/ssr
```
### 0.3 若缺失,先创建基础设施文件
按下面的模板创建缺失文件,已有的跳过。
---
## Phase 1: 后端实现
### 1.1 环境变量
**`.env.example`** 追加:
```
# Supabase Auth
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
```
**`.env.local`** 填入实际值。
### 1.2 Supabase 客户端
**`lib/supabase/client.ts`** (浏览器端):
```typescript
import { createBrowserClient } from '@supabase/ssr'
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
}
```
**`lib/supabase/server.ts`** (服务端):
```typescript
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export async function createClient() {
const cookieStore = await cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll()
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
)
} catch {
// Server Component 中 setAll 可能失败,忽略
}
},
},
}
)
}
```
### 1.3 Middleware (Session 刷新)
**`middleware.ts`** (或 Next.js 16 的 `proxy.ts`) — 在已有 middleware 中追加 Supabase session 刷新逻辑:
```typescript
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
export async function updateSession(request: NextRequest) {
let supabaseResponse = NextResponse.next({ request })
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll()
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))
supabaseResponse = NextResponse.next({ request })
cookiesToSet.forEach(({ name, value, options }) =>
supabaseResponse.cookies.set(name, value, options)
)
},
},
}
)
// 刷新 session (不保护路由,所有页面公开)
await supabase.auth.getUser()
return supabaseResponse
}
```
> **注意**: 如果项目有 next-intl 等其他 middleware,Supabase 刷新应放在最后执行。
### 1.4 Resend SMTP 配置(邮件通道)
验证码邮件由 Supabase Auth 触发(`signInWithOtp`),经 **Supabase 自定义 SMTP** 转发到 **Resend** 投递。不在代码里调 Resend API,不自造验证码存储。
**Supabase Dashboard → Authentication → SMTP Settings → Custom SMTP:**
| 字段 | 值 |
|---|---|
| Host | `smtp.resend.com` |
| Port | `465`(SSL)或 `587`(STARTTLS) |
| Username | `resend` |
| Password | `<Resend API Key>`(`re_` 开头) |
| Sender email | `noreply@<Resend 已验证域名>` |
| Minimum interval | 按需(默认即可) |
**前置准备(Resend 侧):**
1. 注册 Resend → 拿 API Key(`re_...`)
2. 添加并验证发件域名(Resend → Domains → 配置 DNS 记录)
3. Sender 地址必须属于已验证域名
**邮件模板(Supabase Dashboard → Auth → Email Templates → Magic Link):**
仅保留 8 位验证码,删除 magic link 链接(本方案前端直接 `verifyOtp` 输码,不走链接):
```
Your verification code is: {{ .Token }}
```
> **为何用 Resend**: Supabase 免费版内置邮件 3 封/小时 + 易进垃圾箱,生产必须自定义 SMTP。Resend 提供 SMTP 接入,每月 3000 封免费额度,投递率高。
### 1.5 三大场景 — Supabase API 映射
```
┌──────────────────┬──────────────────────────────────────────────┐
│ 功能 │ Supabase Auth API │
├──────────────────┼──────────────────────────────────────────────┤
│ ① 邮箱+密码登录 │ supabase.auth.signInWithPassword({ │
│ │ email, password │
│ │ }) │
├──────────────────┼──────────────────────────────────────────────┤
│ ① OAuth 登录 │ supabase.auth.signInWithOAuth({ │
│ │ provider: 'google' | 'github', │
│ │ options: { redirectTo: origin+/auth/callback }│
│ │ }) │
├──────────────────┼──────────────────────────────────────────────┤
│ ② 注册-发验证码 │ supabase.auth.signInWithOtp({ │
│ │ email, │
│ │ options: { shouldCreateUser: true } │
│ │ }) │
├──────────────────┼──────────────────────────────────────────────┤
│ ② 注册-验证+设密│ 1. supabase.auth.verifyOtp({ │
│ │ email, token, type: 'email' │
│ │ }) → 拿到 session │
│ │ 2. supabase.auth.updateUser({ │
│ │ password, │
│ │ data: { nickname } │
│ │ }) │
├──────────────────┼──────────────────────────────────────────────┤
│ ③ 改密-发验证码 │ supabase.auth.signInWithOtp({ │
│ (忘记密码) │ email, │
│ │ options: { shouldCreateUser: false } │
│ │ }) │
├──────────────────┼──────────────────────────────────────────────┤
│ ③ 改密-验证+设密│ 1. supabase.auth.verifyOtp({ │
│ │ email, token, type: 'email' │
│ │ }) → 拿到 session │
│ │ 2. supabase.auth.updateUser({ │
│ │ password: newPassword │
│ │ }) │
├──────────────────┼──────────────────────────────────────────────┤
│ 退出登录 │ supabase.auth.signOut() │
├──────────────────┼──────────────────────────────────────────────┤
│ 监听状态变化 │ supabase.auth.onAuthStateChange(callback) │
├──────────────────┼──────────────────────────────────────────────┤
│ 获取当前用户 │ supabase.auth.getUser() │
└──────────────────┴──────────────────────────────────────────────┘
```
> **关键**: 注册与改密都走「signInWithOtp → verifyOtp → updateUser」三步组合,**不使用** `signUp` 和 `resetPasswordForEmail`。区别仅在 `shouldCreateUser`:注册=true,改密=false。
### 1.6 错误映射 (Supabase → 用户友好消息)
```
Supabase error.message → 消息 key
─────────────────────────────────────────────────────
"Invalid login credentials" → invalidCredentials
"Email not confirmed" → emailNotConfirmed
"User already registered" → emailAlreadyExists
"Password should be at least 6..." → passwordTooShort
"New password should be different" → passwordSameAsOld
"Token has expired or is invalid" → codeExpired
"already been confirmed" → codeExpired
"Invalid" (OTP) → codeIncorrect
"rate limit" / "too many" / "429" → sendCodeRateLimit
"For security purposes, you can..." → passwordRateLimit
其他 → loginFailed / signupFailed / changePasswordFailed
```
### 1.7 前端校验规范 (统一工具函数)
**`lib/auth-validation.ts`**:
```typescript
export const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
export const PASSWORD_MIN_LENGTH = 8
/** 邮箱格式校验 */
export function validateEmail(email: string): boolean {
return EMAIL_REGEX.test(email.trim())
}
/**
* 密码校验: 长度≥8 + 必含字母与数字
* @returns null=通过, 否则返回错误消息 key
*/
export function validatePassword(password: string): string | null {
if (password.length < PASSWORD_MIN_LENGTH) return 'passwordTooShort'
const hasLetter = /[a-zA-Z]/.test(password)
const hasNumber = /[0-9]/.test(password)
if (!hasLetter || !hasNumber) return 'passwordTooWeak'
return null
}
/** 二次确认密码一致性 */
export function validateConfirmPassword(password: string, confirm: string): boolean {
return password.length > 0 && password === confirm
}
/** 验证码格式: 8 位数字 */
export function validateOtp(otp: string): boolean {
return /^\d{8}$/.test(otp.trim())
}
```
校验规则汇总:
```
┌────────────┬─────────────────────────────────┬──────────────────────┐
│ 校验项 │ 规则 │ 失败消息 key │
├────────────┼─────────────────────────────────┼──────────────────────┤
│ 邮箱格式 │ /^[^\s@]+@[^\s@]+\.[^\s@]+$/ │ invalidEmail │
│ 密码长度 │ ≥ 8 字符 │ passwordTooShort │
│ 密码复杂度 │ 必含字母+数字 (推荐大小写+特殊) │ passwordTooWeak │
│ 二次确认 │ password === confirmPassword │ passwordMismatch │
│ 验证码 │ 8 位数字 /^\d{8}$/ │ enter8Digits │
└────────────┴─────────────────────────────────┴──────────────────────┘
```
---
## Phase 2: 前端 UI
> 本 skill 不绑定 UI 库。下方代码用 React + 原生表单示例,项目可按自身 UI 库(shadcn/ui、MUI、Ant Design 等)适配。
### 2.1 AuthModal 视图状态机
```
type AuthTab = 'login' | 'register'
type AuthView = 'login' | 'register' | 'forgot-password'
┌──────────────┐
│ AuthModal │
└──────┬───────┘
│
┌────────────┴─────────────┐
▼ ▼
authTab='login' authTab='register'
│ │
▼ ▼
┌─────────────┐ ┌──────────────────┐
│ 邮箱+密码 │ │ ① 发验证码 │
│ signInWith │ │ signInWithOtp │
│ Password │ │ (shouldCreate │
│ │ │ User:true) │
│ + OAuth 按钮 │ │ ② verifyOtp │
│ │ │ ③ updateUser │
└──────┬──────┘ │ (password+ │
│ │ nickname) │
│ "忘记密码?" │ 校验:邮箱+密码复杂 │
│ │ +8位码 │
▼ └──────────────────┘
┌──────────────────┐
│ view='forgot- │
│ password' │
│ ① 发验证码 │
│ signInWithOtp │
│ (shouldCreate │
│ User:false) │
│ ② verifyOtp │
│ ③ updateUser │
│ (newPassword) │
│ 校验:邮箱+新密码 │
│ 复杂度+确认+8位码│
└──────────────────┘
```
### 2.2 AuthModal 组件核心结构
```tsx
// components/auth-modal.tsx
interface AuthModalProps {
open: boolean
onOpenChange: (open: boolean) => void
onLoginSuccess: () => void
}
// Props 说明:
// - open/onOpenChange: 由父组件控制显示/隐藏
// - onLoginSuccess: 登录/注册/改密成功回调 (通常只需触发,实际状态由 onAuthStateChange 监听)
```
**核心处理函数:**
```tsx
import { createClient } from '@/lib/supabase/client'
import {
validateEmail,
validatePassword,
validateConfirmPassword,
validateOtp,
} from '@/lib/auth-validation'
// ─── 登录: 邮箱+密码 ───
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault()
if (!validateEmail(email)) { setError(msg('invalidEmail')); return }
const pwdErr = validatePassword(password)
if (pwdErr) { setError(msg(pwdErr)); return }
const supabase = createClient()
const { error } = await supabase.auth.signInWithPassword({
email: email.trim(),
password,
})
if (error) {
// 错误映射 → invalidCredentials / emailNotConfirmed / loginFailed
return
}
onLoginSuccess()
}
// ─── 注册: 发送验证码 ───
const handleRegisterSendOtp = async () => {
if (!validateEmail(email)) { setError(msg('invalidEmail')); return }
if (countdown > 0) return // 冷却中
const supabase = createClient()
const { error } = await supabase.auth.signInWithOtp({
email: email.trim(),
options: { shouldCreateUser: true },
})
if (error) {
// rate limit → sendCodeRateLimit, 其他 → sendCodeFailed
return
}
setOtpSent(true)
setCountdown(50) // 启动 50s 冷却
}
// ─── 注册: 验证码 + 设密码 ───
const handleRegister = async (e: React.FormEvent) => {
e.preventDefault()
if (!validateEmail(email)) { setError(msg('invalidEmail')); return }
if (!validateOtp(otp)) { setError(msg('enter8Digits')); return }
const pwdErr = validatePassword(password)
if (pwdErr) { setError(msg(pwdErr)); return }
const supabase = createClient()
// 1. 验证 OTP
const { error: otpError } = await supabase.auth.verifyOtp({
email: email.trim(),
token: otp.trim(),
type: 'email',
})
if (otpError) {
// codeExpired / codeIncorrect
return
}
// 2. 设密码 + nickname
const nickname = email.trim().split('@')[0]
const { error: updateError } = await supabase.auth.updateUser({
password,
data: { nickname },
})
if (updateError) { setError(msg('signupFailed')); return }
onLoginSuccess()
}
// ─── 忘记密码: 发送验证码 ───
const handleForgotSendOtp = async () => {
if (!validateEmail(email)) { setError(msg('invalidEmail')); return }
if (countdown > 0) return
const supabase = createClient()
const { error } = await supabase.auth.signInWithOtp({
email: email.trim(),
options: { shouldCreateUser: false }, // ← 关键: 已存在用户,不创建新用户
})
if (error) {
// rate limit → sendCodeRateLimit, 其他 → sendCodeFailed
return
}
setOtpSent(true)
setCountdown(50)
}
// ─── 忘记密码: 验证码 + 设新密码 ───
const handleResetPassword = async (e: React.FormEvent) => {
e.preventDefault()
if (!validateEmail(email)) { setError(msg('invalidEmail')); return }
if (!validateOtp(otp)) { setError(msg('enter8Digits')); return }
const pwdErr = validatePassword(newPassword)
if (pwdErr) { setError(msg(pwdErr)); return }
if (!validateConfirmPassword(newPassword, confirmNewPassword)) {
setError(msg('passwordMismatch')); return
}
const supabase = createClient()
// 1. 验证 OTP
const { error: otpError } = await supabase.auth.verifyOtp({
email: email.trim(),
token: otp.trim(),
type: 'email',
})
if (otpError) {
// codeExpired / codeIncorrect
return
}
// 2. 设新密码
const { error: updateError } = await supabase.auth.updateUser({
password: newPassword,
})
if (updateError) { setError(msg('changePasswordFailed')); return }
onLoginSuccess()
}
```
> `msg(key)` 是项目自身的消息取值函数(i18n 或硬编码文案),本 skill 不规定其实现。
### 2.3 UI 布局示意
**登录视图:**
```
┌─────────────────────────────────────┐
│ 📧 欢迎登录 │
├─────────────────────────────────────┤
│ [🟡 Sign in with Google] │
│ [⚫ Sign in with GitHub] │
│ │
│ ─────── or use email ──────── │
│ │
│ 📧 [Email_______________] │
│ 🔒 [Password__________👁] │
│ │
│ [ Log in ] │
│ │
│ Forgot password? │
│ Don't have an account? Sign up │
└─────────────────────────────────────┘
```
**注册视图 (含验证码):**
```
┌─────────────────────────────────────┐
│ ✨ 创建账号 │
│ 验证码已发送至 xxx@xx.com │
├─────────────────────────────────────┤
│ 📧 [Email_______________] [Send] │ ← 发送验证码(50s冷却)
│ │
│ 🔢 [______] 8位验证码 │
│ 🔒 [Password__________👁] │ ← ≥8位+字母+数字
│ │
│ [ Sign up ] │
│ │
│ Already have an account? Log in │
└─────────────────────────────────────┘
```
**忘记密码视图 (验证码改密+确认密码):**
```
┌─────────────────────────────────────┐
│ 🔑 重置密码 │
├─────────────────────────────────────┤
│ 📧 [Email_______________] [Send] │ ← 发验证码(shouldCreateUser:false)
│ │
│ 🔢 [______] 8位验证码 │
│ 🔒 [New Password_____👁] │ ← ≥8位+字母+数字
│ 🔒 [Confirm__________👁] │ ← 二次确认
│ │
│ [ Reset password ] │
│ │
│ Back to login │
└─────────────────────────────────────┘
```
### 2.4 OAuth Provider 配置
Supabase Dashboard 配置 (非代码):
```
Supabase Dashboard → Authentication → Providers
需要启用的 Provider:
├── Google → 需要 Google Cloud Console 的 Client ID + Secret
└── GitHub → 需要 GitHub Settings → Developer 的 Client ID + Secret
回调 URL 统一填: https://你的域名/auth/callback
```
代码中无需配置 Client ID/Secret,只需调用:
```typescript
supabase.auth.signInWithOAuth({
provider: 'google', // 或 'github'
options: { redirectTo: `${window.location.origin}/auth/callback` }
})
```
### 2.5 重发冷却倒计时 (50s)
```tsx
const [countdown, setCountdown] = useState(0)
const countdownRef = useRef<ReturnType<typeof setInterval> | null>(null)
useEffect(() => {
if (countdown > 0) {
countdownRef.current = setInterval(() => {
setCountdown((prev) => {
if (prev <= 1) {
if (countdownRef.current) clearInterval(countdownRef.current)
return 0
}
return prev - 1
})
}, 1000)
}
return () => {
if (countdownRef.current) clearInterval(countdownRef.current)
}
}, [countdown > 0]) // eslint-disable-line react-hooks/exhaustive-deps
// 发送验证码成功后: setCountdown(50)
// 按钮禁用条件: countdown > 0
// 按钮文案: countdown > 0 ? msg('resendInXs', { sec: countdown }) : msg('resend')
```
---
## Phase 3: 回调路由
> 回调路由放在 `app/auth/` 下,**不在** i18n 的 `[locale]` 路由段内(因为 Supabase 回调不带 locale 前缀)。若项目无 i18n,放 `app/auth/` 即可。
### 3.1 OAuth 回调
**`app/auth/callback/route.ts`**:
```typescript
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
/** 仅允许相对路径,防止开放重定向 */
function sanitizeNext(next: string | null): string {
if (!next) return '/'
return next.startsWith('/') && !next.startsWith('//') ? next : '/'
}
export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url)
const code = searchParams.get('code')
const next = sanitizeNext(searchParams.get('next'))
if (code) {
const supabase = await createClient()
const { error } = await supabase.auth.exchangeCodeForSession(code)
if (!error) {
return NextResponse.redirect(`${origin}${next}`)
}
}
return NextResponse.redirect(`${origin}/?auth=error`)
}
```
### 3.2 邮箱确认兜底路由(含安全加固)
**`app/auth/confirm/route.ts`**:
```typescript
import { createClient } from '@/lib/supabase/server'
import { NextRequest, NextResponse } from 'next/server'
const VALID_TYPES = ['email', 'signup', 'recovery', 'magiclink', 'invite'] as const
type AuthType = (typeof VALID_TYPES)[number]
/** 仅允许相对路径,防止开放重定向 */
function sanitizeNext(next: string | null): string {
if (!next) return '/'
return next.startsWith('/') && !next.startsWith('//') ? next : '/'
}
/** type 参数白名单校验 */
function validateType(type: string | null): AuthType | null {
if (!type) return null
return VALID_TYPES.includes(type as AuthType) ? (type as AuthType) : null
}
export async function GET(request: NextRequest) {
const { searchParams, origin } = new URL(request.url)
const tokenHash = searchParams.get('token_hash')
const type = validateType(searchParams.get('type'))
const next = sanitizeNext(searchParams.get('next'))
if (tokenHash && type) {
const supabase = await createClient()
const { error } = await supabase.auth.verifyOtp({ token_hash: tokenHash, type })
if (!error) {
return NextResponse.redirect(`${origin}${next}`)
}
}
return NextResponse.redirect(`${origin}/?auth=error`)
}
```
> **注意**: 本方案注册/改密均在前端用 `verifyOtp({ email, token, type:'email' })` 直接验证,不依赖邮件链接。`confirm` 路由仅作 magic link 兜底,已加 `sanitizeNext` 防开放重定向 + `validateType` 白名单。
---
## Phase 4: 构建验证
```bash
pnpm build
```
验证清单:
- [ ] 构建通过,无 TypeScript 错误
- [ ] 登录: 邮箱+密码 可登录
- [ ] 登录: Google / GitHub OAuth 可跳转
- [ ] 注册: 邮箱 → 收验证码 → 输入验证码+密码 → 注册成功
- [ ] 注册校验: 邮箱格式/密码≥8+复杂度/8位验证码 均拦截
- [ ] 忘记密码: 邮箱 → 收验证码 → 输入验证码+新密码+确认密码 → 改密成功
- [ ] 改密校验: 邮箱格式/新密码≥8+复杂度/两次一致/8位验证码 均拦截
- [ ] 重发冷却: 50s 内按钮禁用
- [ ] 回调路由: /auth/callback 和 /auth/confirm 存在且安全加固生效
---
## 错误消息键名清单
错误映射需要以下消息 key(若项目使用 i18n)。键名通用,具体文案由项目按自身语言方案提供:
```
invalidEmail / invalidCredentials / emailNotConfirmed / unsupportedProvider
loginFailed / signupFailed / changePasswordFailed / emailAlreadyExists
passwordTooShort / passwordTooWeak / passwordMismatch / passwordSameAsOld
passwordRateLimit
sendCodeFailed / sendCodeRateLimit / codeExpired / codeIncorrect / enter8Digits
codeSentTo / resend / resendInXs / passwordResetSuccess
```
> **注**: 本 skill 不绑定特定 i18n 框架。项目用 next-intl / react-i18next / 原生方案均可,自行落地这些 key 的多语言文案。
---
## 关键文件清单
| 文件 | 职责 | Phase |
|------|------|-------|
| `lib/supabase/client.ts` | 浏览器端 Supabase SDK | 1 |
| `lib/supabase/server.ts` | 服务端 Supabase SDK | 1 |
| `lib/auth-validation.ts` | 邮箱/密码/验证码统一校验 | 1 |
| `middleware.ts` (或 `proxy.ts`) | Session cookie 刷新 | 1 |
| `app/auth/callback/route.ts` | OAuth 回调(含安全加固) | 3 |
| `app/auth/confirm/route.ts` | 邮箱确认兜底(含安全加固) | 3 |
| `components/auth-modal.tsx` | 登录/注册/忘记密码 UI | 2 |
| `.env.example` | Supabase 环境变量模板 | 1 |
---
## 实现注意事项
1. **统一 OTP 方案**: 注册与改密均走「signInWithOtp → verifyOtp → updateUser」三步,不使用 `signUp` 和 `resetPasswordForEmail`。区别仅在 `shouldCreateUser`(注册=true / 改密=false)。
2. **前端校验先行**: 邮箱格式、密码长度(≥8)、密码复杂度(字母+数字)、验证码8位,全部前端拦截,减少无效请求。**二次确认归属**: 注册**不设**二次确认(仅密码+验证码),忘记密码**必设**二次确认(新密码+确认密码)。
3. **Supabase Dashboard 配置**: OAuth Provider、邮件模板、确认策略都在 Dashboard 配置,不在代码中。
4. **邮箱确认策略**: Dashboard → Authentication → Email → "Confirm email" 开关。本方案前端直接 verifyOtp,建议关闭"发送确认链接"或保留作兜底。
5. **回调路由在 i18n 路由段外**: `app/auth/callback` 和 `app/auth/confirm` 不在 `[locale]/` 下。
6. **安全加固**: confirm/callback 路由用 `sanitizeNext` 防开放重定向,`validateType` 白名单限制 type。
7. **重发冷却**: 50 秒倒计时,防止滥用 Supabase 邮件发送(有频率限制)。
8. **零新依赖**: Supabase Auth 全部功能已包含在 `@supabase/supabase-js` + `@supabase/ssr` 中。
9. **已有文件不覆盖**: Phase 0 检测到已有文件时跳过,只创建缺失的。
10. **与项目解耦**: 本 skill 不绑定 i18n 框架、语言组合、UI 库。`msg(key)` 取文案的方式由项目自定。
---
## 附录: service_role 可选安全增强
**默认方案(KISS,本 skill 主体)**: 注册走 `signInWithOtp(shouldCreateUser:true) → verifyOtp → updateUser(password)`,全程前端 anon key,**无需 service_role**。
**理论漏洞**: 有人绕过前端,直接拿 anon key 调 Supabase Auth 接口。Supabase Auth 自身有频率限制与 RLS 兜底,风险可控。
**若要后端强约束(可选增强)**:
1. 新增服务端 endpoint `/api/auth/register`,仅用 `SUPABASE_SERVICE_ROLE_KEY`(服务端环境变量,**绝不暴露前端**)
2. 前端 OTP 验证邮箱后,调该 endpoint
3. 服务端用 admin API 建已确认用户:
```typescript
supabase.auth.admin.createUser({
email, password, email_confirm: true,
})
```
4. 强制必须经 OTP 验证才能建号,后端也把关
> **取舍**: 加 service_role = 多一个服务端 endpoint + 管理一把高权 key。除非有强合规需求,默认 KISS 方案够用。
Files in this skill
- SKILL.md
- agents/openai.yaml
Attribution
Comments
Loading comments…