Browse Source

docs: add auth and user profile design spec (2026-04-12)

Made-with: Cursor
feat/auth-user
npmrun 6 days ago
parent
commit
f33b4c9043
  1. 258
      docs/superpowers/specs/2026-04-12-auth-user-design.md

258
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 各节确认合并) |
Loading…
Cancel
Save