diff --git a/docs/superpowers/specs/2026-04-12-auth-user-design.md b/docs/superpowers/specs/2026-04-12-auth-user-design.md new file mode 100644 index 0000000..4d44f8e --- /dev/null +++ b/docs/superpowers/specs/2026-04-12-auth-user-design.md @@ -0,0 +1,258 @@ +# 登录、注册与用户信息 — 设计规格 + +**状态**:已定稿(2026-04-12) +**范围**:第一版邮箱密码 + Redis Session;未验证邮箱分级门禁;忘记密码闭环(无生产邮件);最小 Nuxt 页面;为 OAuth 预留数据与流程边界。 + +--- + +## 1. 目标与非目标 + +### 1.1 目标(第一版交付) + +- **注册**:邮箱 + 密码;持久化密码哈希;可创建邮箱验证类 challenge(与是否发信解耦)。 +- **登录 / 登出**:校验密码后创建 **Redis Session**,通过 **HttpOnly Cookie** 下发 opaque session id;登出删除会话并清 Cookie;登录成功 **轮换 session id**。 +- **当前用户**:从 Cookie → Redis → `user_id` 解析会话上下文。 +- **用户信息**:`GET` / `PATCH` 个人资料;**第一版不允许修改 `email`**;可更新字段如 `name`、`age`(与现有 `users_table` 对齐)。 +- **未验证账号**:不阻止登录;对指定操作通过 **集中配置** 要求 `email_verified_at`;新增能力时优先改配置而非复制校验逻辑。 +- **忘记密码**:`forgot-password` 与 `reset-password` **API 闭环**;无生产邮件时 token 通过开发日志或运维/DB 获取;对外响应防枚举。 +- **OAuth**:不在第一版实现;数据与流程边界见第 8 节。 + +### 1.2 非目标(第一版不做) + +- 第三方 OAuth 回调与 UI。 +- 2FA、设备管理、复杂风控。 +- 生产级「发验证邮件 / 发重置邮件」投递(通过 `Mailer` 抽象预留,第一版 `NoopMailer`)。 +- 完整运营后台;管理员手工重置仅作为运维流程文档化。 + +--- + +## 2. 数据模型与迁移 + +### 2.1 `users_table` 扩展 + +在现有表上 **增量迁移**(保持表名 `users_table` 与 Drizzle 映射,减少改名风险): + +| 列 | 说明 | +|----|------| +| `password_hash` | 非空(已有行需迁移策略:开发环境可清空后重建;生产需单独评估) | +| `email_verified_at` | `timestamptz`,可空 | +| `created_at` / `updated_at` | 建议新增,便于审计 | + +保留:`id`、`name`、`age`、`email`(唯一)。 + +**邮箱变更**:第一版 **禁止** PATCH 修改 `email`。 + +### 2.2 `auth_challenges` + +单表承载多种「挑战」: + +| 列 | 说明 | +|----|------| +| `id` | 主键 | +| `user_id` | 外键 → `users_table` | +| `type` | 枚举:`email_verify` \| `password_reset` | +| `token_hash` | 仅存储哈希,不存明文 token | +| `expires_at` | 过期时间 | +| `consumed_at` | 可空;消费后置位 | +| 可选 | `created_at`、`created_ip` | + +索引:按 `token_hash` 查询(实现时注意与过期清理策略配合)。 + +### 2.3 Session + +- **仅存 Redis**,不建 Postgres session 表。 +- Key 建议:`sess:{sessionId}`;Value JSON 至少含 `userId`,可选 `createdAt` 等。 +- TTL 与 Cookie `Max-Age` **一致**;续期策略在实现计划中写死默认(如固定过期或滑动窗口)。 + +### 2.4 `linked_accounts`(OAuth 预留) + +第二版可落库,第一版可在迁移中 **创建空表** 或 **仅文档约定**(二选一在实现计划中固定): + +- `user_id`、`provider`(如 `github`)、`provider_user_id` +- 唯一约束:`(provider, provider_user_id)` + +OAuth 绑定不写入 `users` 宽表。 + +### 2.5 迁移注意 + +- 使用 Drizzle 生成并版本化迁移。 +- 示例接口 `server/api/hello` 若返回用户列表,须在实现阶段改为 **鉴权后可用** 或 **删除**,禁止公开泄露。 + +--- + +## 3. Cookie、Redis 与安全 + +### 3.1 Cookie + +- **HttpOnly**:必选。 +- **Secure**:`NODE_ENV=production` 且 HTTPS 时必选;本地 HTTP 可关闭。 +- **SameSite**:默认 **`Lax`**。 +- **Path**:`/`;`Domain` 由部署环境可选配置。 +- Cookie 名:实现中用常量(如 `SESSION_COOKIE_NAME`),全仓统一。 + +### 3.2 Redis + +- 环境变量 **`REDIS_URL`**。 +- Nitro 进程内 **单例连接**(插件或惰性初始化),禁止每请求新建连接。 + +### 3.3 密码与 token + +- 密码哈希:**argon2id 或 bcrypt 二选一**,全仓单一封装;实现计划选定并锁定依赖。 +- 挑战 token:生成随机明文 → 仅存 **`token_hash`**;校验时使用恒定时间比较。 + +### 3.4 滥用防护 + +- 对登录、注册等接口做 **限流**(建议 Redis 计数器 + 时间窗口)。 +- `forgot-password`:**始终 HTTP 200** + 统一文案,不泄露邮箱是否注册。 + +### 3.5 CSRF + +- 第一版假设 **同站** Nuxt 调用 API,`SameSite=Lax`。 +- 若未来开放跨站写操作,须引入 **CSRF token** 或等价机制(单独规格)。 + +--- + +## 4. API 约定 + +### 4.1 路由一览 + +| 方法与路径 | 行为 | +|------------|------| +| `POST /api/auth/register` | 注册;**建议成功后直接建立会话**(与登录一致) | +| `POST /api/auth/login` | 登录;轮换 session;写 Cookie | +| `POST /api/auth/logout` | 登出;幂等 | +| `POST /api/auth/forgot-password` | body:`{ email }`;统一 200 响应 | +| `POST /api/auth/reset-password` | body:`{ token, new_password }`;成功后消费 challenge、更新 `password_hash`、**吊销该用户所有 Redis session** | +| `POST /api/auth/verify-email` | body:`{ token }`;成功则写 `email_verified_at`、消费 challenge | +| `GET /api/me` | 当前用户摘要;未登录 **401** | +| `PATCH /api/me` | 更新资料;未登录 **401**;若配置要求已验证而未满足 **403** | + +全仓路径命名保持一致;若调整仅允许整仓重命名并更新本文档。 + +### 4.2 错误响应 + +统一 JSON,例如: + +```json +{ + "error": { + "code": "EMAIL_IN_USE", + "message": "该邮箱已注册" + } +} +``` + +建议状态码: + +- **400**:校验失败 +- **401**:未登录或 session 无效 +- **403**:已登录但不满足策略(如未验证邮箱) +- **409**:资源冲突(邮箱已存在) +- **429**:限流 +- **500**:不暴露内部细节;服务端结构化日志记录 + +响应体 **不得** 包含 `password_hash` 或明文 session id。 + +--- + +## 5. 「未验证」可配置门禁 + +### 5.1 原则 + +- **`requireUser`**:解析会话,无则 401。 +- **`requireVerifiedFor(handlerId)`** 或等价:根据 **集中配置** 判断是否要求 `email_verified_at` 非空。 +- **默认策略**:未在配置中声明的操作 **仅需登录**(避免默认过严导致全站不可用);敏感写操作 **显式** 声明需验证。 + +### 5.2 配置形态 + +- 使用「路由/能力 id → 是否需要已验证」的映射(对象、模块导出或 JSON)。 +- 第一版至少示例:**`PATCH /api/me` 需已验证**(可配置关闭以联调);**`GET /api/me` 仅需登录**。 + +### 5.3 前端 + +- **401**:跳转 `/login`,可带 `redirect`。 +- **403**(未验证):在 `/me` 使用提示条或轻量引导;不强制整页拦截只读,除非产品后续变更。 + +--- + +## 6. 邮件与占位行为 + +### 6.1 `Mailer` 抽象 + +- `sendVerificationEmail`、`sendPasswordResetEmail`(签名随实现细化)。 +- 第一版:**`NoopMailer`**,不发起网络投递。 + +### 6.2 开发调试 + +- 环境变量 **`AUTH_DEBUG_LOG_TOKENS=true`** 时,将验证/重置用 **明文 token 写入结构化日志一次**;**生产默认关闭**。 + +### 6.3 无邮件生产 + +- `reset-password` / `verify-email` 仍可用;token 获取走 **运维/DB 应急流程**(文档化,非产品功能)。 + +--- + +## 7. 最小 Nuxt 前端 + +- 页面:`/login`、`/register`、`/me`(资料 + 登出);登出可在 `/me` 上以按钮完成。 +- 路由中间件:`auth`(保护 `/me`);可选 `guest`(已登录访问登录/注册页时重定向 `/me`)。 +- 数据请求:浏览器与 SSR 须保证 **携带 Cookie**(`credentials: 'include'` 或同源默认行为以实现时验证为准)。 +- 客户端可做基础格式校验;**服务端校验为权威**。 + +--- + +## 8. OAuth 第二阶段(边界) + +- 登录成功仍使用 **同一套 Redis Session + Cookie**。 +- 用户主体在 **`users_table`**;外部身份在 **`linked_accounts`**。 +- **禁止** OAuth 邮箱与已有账号 **静默合并**;须显式「连接账号」或拒绝策略(实现计划细化)。 +- 回调路由占位:`/api/auth/oauth/{provider}/start`、`/api/auth/oauth/{provider}/callback`(路径以实现计划为准)。 +- `email_verified_at` 是否因 OAuth 声明而自动写入:第二阶段按 provider 策略在实现计划中规定。 + +--- + +## 9. 测试、环境变量与上线检查 + +### 9.1 测试 + +- **单元**:哈希、token 校验、门禁配置解析。 +- **集成**:Postgres + Redis;覆盖注册→登录→me、未验证 403、验证后通过、登出 401、重置密码吊销会话等(具体用例实现计划列出)。 +- 可复用/扩展 `scripts/migrate-test.sh` 等现有脚本启动测试库。 + +### 9.2 环境变量(最小集) + +| 变量 | 说明 | +|------|------| +| `DATABASE_URL` | 已有 | +| `REDIS_URL` | 新增 | +| `NODE_ENV` | `production` 行为见上文 | +| `AUTH_DEBUG_LOG_TOKENS` | 可选;开发用 | +| `SESSION_TTL_SECONDS` | 可选;缺省用代码默认 | + +部署可选:`COOKIE_DOMAIN`、限流相关前缀变量(实现计划定义)。 + +### 9.3 上线检查单 + +- 迁移已执行;Redis 可用;HTTPS + `Secure` Cookie。 +- 关闭 `AUTH_DEBUG_LOG_TOKENS`;限流与公开 API 审查完成。 +- 日志不含密码与生产 session 明文。 + +### 9.4 日志 + +- 复用项目 logger;认证失败、限流、Redis 错误结构化记录。 + +--- + +## 10. 与现有代码库的关系 + +- 栈:**Nuxt 4、Nitro、Bun、Drizzle、Postgres**;Session **Redis**。 +- 现有 `users_table` 与 `drizzle-pkg` 迁移流程延续;新表纳入同一包或约定目录。 +- 本文档为实现的 **唯一需求来源**之一;冲突以本文档与后续已批准的变更记录为准。 + +--- + +## 11. 修订记录 + +| 日期 | 说明 | +|------|------| +| 2026-04-12 | 初版定稿(brainstorming 各节确认合并) |