# 认证模块审计报告

> 审计日期：2026-06-25
> 审计范围：`src/auth.ts`、`src/proxy.ts`、`src/next-auth.d.ts`、`src/modules/auth/**`、`src/modules/onboarding/**`、`src/app/(auth)/**`、`src/app/(onboarding)/**`、`src/app/api/auth/**`、`src/app/api/onboarding/**`、`src/shared/components/{auth-session-provider,onboarding-gate}.tsx`、`src/shared/lib/{auth-guard,session,permissions,role-utils,bcrypt-utils,http-utils,password-policy,password-security-service,rate-limit,login-logger,audit-logger,change-logger}.ts`、`src/shared/hooks/use-permission.ts`、`src/shared/types/permissions.ts`
> 审计依据：`e:\Desktop\CICD\.trae\rules\project_rules.md`、`docs/architecture/004_architecture_impact_map.md` §2.24/§3/§4/§5、`docs/architecture/005_architecture_data.json` `auth`/`usersAndAuth`
> 前序报告：`docs/architecture/audit/permissions-audit-report.md`、`docs/architecture/audit/00_summary.md`

---

## 一、现有实现概要

### 1.1 文件分布

认证模块共 **42** 个文件，按职责分布如下：

| 层级 | 文件数 | 主要文件 |
|------|--------|----------|
| 根目录（应用层） | 3 | `auth.ts`(234)、`proxy.ts`(158)、`next-auth.d.ts`(24) |
| 认证 UI 模块 `modules/auth` | 3 | `auth-layout.tsx`(32)、`login-form.tsx`(196)、`register-form.tsx`(259) |
| Onboarding 模块 `modules/onboarding` | 5 | `actions.ts`(294)、`data-access.ts`(146)、`schema.ts`(84)、`types.ts`(57)、`onboarding-stepper.tsx`(451) |
| (auth) 路由组 | 7 | `layout.tsx`(5)、`login/page.tsx`(16)、`register/page.tsx`(167)、`privacy/page.tsx`(128)、`terms/page.tsx`(130+)、`error.tsx`(23)、`not-found.tsx`(22) |
| (onboarding) 路由组 | 1 | `onboarding/page.tsx`(49) |
| API 路由 | 3 | `api/auth/[...nextauth]/route.ts`(2)、`api/onboarding/status/route.ts`(19)、`api/onboarding/complete/route.ts`(20) |
| shared/lib（认证辅助） | 12 | `auth-guard.ts`(78)、`session.ts`(35)、`permissions.ts`(304)、`role-utils.ts`(50)、`bcrypt-utils.ts`(19)、`http-utils.ts`(44)、`password-policy.ts`(119)、`password-security-service.ts`(94)、`rate-limit.ts`(119)、`login-logger.ts`(42)、`audit-logger.ts`(46)、`change-logger.ts`(43) |
| shared/components | 2 | `auth-session-provider.tsx`(12)、`onboarding-gate.tsx`(313，**已废弃未删除**) |
| shared/hooks | 1 | `use-permission.ts`(53) |
| shared/types | 1 | `permissions.ts`(236) |
| i18n | 2 | `auth.json`(31)、`onboarding.json`(78) — **存在但完全未被 login-form / register-form 使用** |
| e2e 测试 | 1 | `tests/e2e/smoke-auth.spec.ts`(20) — **与实际 UI 不一致** |

### 1.2 数据流

```
[匿名访问]
  proxy.ts → 检查 token → 未登录 → /login
                          ↓
              LoginForm → signIn("credentials")
                          ↓
              auth.ts authorize → rate-limit → user 查询 → 锁定校验 → bcrypt 比对 → 2FA 校验 → 角色/权限加载
                          ↓
              jwt callback → 写入 token.{id,roles,permissions,onboarded}
                          ↓
              proxy.ts → onboarding 检查 → 未完成 → /onboarding
                                              ↓
                                  OnboardingStepper → completeOnboardingAction → data-access + classes data-access
                                              ↓
                                  users.onboardedAt = now → jwt refresh → 默认路径

[已认证访问]
  proxy.ts → 检查 ROUTE_PERMISSIONS → 通过 → 页面 requirePermission → getAuthContext → resolveDataScope（rbac resolver）→ data-access
```

### 1.3 架构图完整性

`docs/architecture/004_architecture_impact_map.md` §2.24（auth 认证 UI 模块）与 §3（根目录 auth.ts/proxy.ts）记录的导出函数、依赖关系与实际代码**大体一致**，但存在以下遗漏：

1. **未记录 `modules/onboarding`**：架构图 §2.24 仅描述 `modules/auth`，但 onboarding 与 auth 强耦合（首次登录引导），未在 auth 章节中关联说明，仅在 §5 `usersAndAuth` 域中提及。
2. **未记录 `auth.ts` 对 `modules/settings` 的反向依赖**：`auth.ts#L106` `await import("@/modules/settings/actions-security")` 调用 `verifyTwoFactorForLogin`，但架构图 §3 描述 auth.ts 仅依赖 `shared/lib/*`，遗漏了跨层依赖。
3. **未记录 `proxy.ts` 的 `resolveDefaultPath` 函数**：导出函数仅记录 `proxy` 主入口，但 `resolveDefaultPath` 被 `onboarding/data-access.ts` 重复实现（`resolveDefaultPathByRoles`），属于逻辑重复未在依赖矩阵中标记。
4. **未记录 `onboarding-gate.tsx`**：架构图 `shared/components` 清单中未列出此组件，但文件仍存在。
5. **未记录 `use-permission.ts` 与 `auth-session-provider.tsx` 在 auth 模块中的归属**：被随意划入 shared，但语义上属于 auth 模块对外暴露的客户端 API。

---

## 二、现存问题与原因分析

### 2.1 三层架构违规（P0）

#### 问题 1：注册页面 Server Action 内联直接访问数据库

- **位置**：[register/page.tsx](file:///e:/Desktop/CICD/src/app/(auth)/register/page.tsx#L36-L164)
- **问题**：`registerAction` 直接内联在页面文件中，通过 `await import("@/shared/db")` 与 `await import("@/shared/db/schema")` 直接 `INSERT users / usersToRoles / roles` 三张表，未通过任何 `modules/*/data-access`。同时文件内重复实现了 `calcAge`、`normalizeBcryptHash` 两个纯函数（前者与 `register-form.tsx#L24-35` 重复，后者与 `shared/lib/bcrypt-utils.ts#L15-19` 重复）。
- **违反规则**：
  - 项目规则"`app/` 只能调用 `modules/` 的 Server Actions 和 data-access，不直接访问数据库"
  - 项目规则"模块标准结构要求 `data-access.ts`、`actions.ts` 分层"
  - 项目规则"Reusable UI components must be extracted to avoid JSX duplication"（此处是逻辑重复）
- **后果**：注册逻辑无法被单测、无法被 mock；DB schema 变更需改页面文件；后续添加邮箱验证、审批流、邀请码等扩展需重写；与 `modules/users/data-access.ts` 的 `getUserProfile`/`updateUserProfileById` 等同样操作 `users` 表的函数形成两套并行的写入口，职责割裂。

#### 问题 2：`src/auth.ts` 跨越 modules 边界依赖 settings 模块

- **位置**：[auth.ts](file:///e:/Desktop/CICD/src/auth.ts#L106) `const { verifyTwoFactorForLogin } = await import("@/modules/settings/actions-security")`
- **问题**：根目录 `auth.ts`（属于应用层）通过 dynamic import 直接调用 `modules/settings/actions-security.ts` 中的 `verifyTwoFactorForLogin`，且 `actions-security.ts` 是 `"use server"` 文件，内部函数默认被认为是 Server Action，被当作普通函数调用语义混乱。
- **违反规则**：
  - 项目规则"严格三层架构，依赖方向单向：`app → modules → shared`"——auth.ts 通过 import modules 是合法方向，但 2FA 校验逻辑被嵌入 settings 模块而非 auth 自身或 shared，造成"settings 反向服务于 auth"的语义错乱
  - 项目规则"模块间只能通过对方 data-access 通信"——此处直接调用 `actions-security.ts`，未通过 settings data-access
- **后果**：settings 模块重构（如拆分 actions-security）会破坏 auth；`verifyTwoFactorForLogin` 被 `auth.ts` 调用导致其内部依赖的 `getTwoFactorEnabled/getTotpSecret/getBackupCodesHashed` 等函数在登录时被加载，bundle 体积无谓增加。

#### 问题 3：onboarding 页面绕过统一认证入口

- **位置**：[onboarding/page.tsx](file:///e:/Desktop/CICD/src/app/(onboarding)/onboarding/page.tsx#L6-L19) `import { auth } from "@/auth"`、`const session = await auth()`
- **问题**：项目硬约束明确要求"Authentication must use `getAuthContext()` and `getCurrentStudentUser()`; avoid mixing with other auth methods"，但 onboarding 页面绕过 `shared/lib/auth-guard.getAuthContext()`，直接调用 `auth()` 获取 session。同时仅靠 `if (!userId) redirect("/login")` 手动处理未登录态，不享受 `PermissionDeniedError` 统一异常与 DataScope 解析。
- **违反规则**：
  - 项目硬约束"Authentication must use `getAuthContext()` and `getCurrentStudentUser()`"
  - 项目 memory"Authentication must use `getAuthContext()` and `getCurrentStudentUser()`; avoid mixing with other auth methods"
- **后果**：未登录异常未走 `route-error.tsx` 统一处理；JWT session 与 DB 中 `users.onboardedAt` 不一致时只能靠手动 `redirect`；与同项目中所有其他 dashboard 页面使用 `requireAuth()` 的模式不一致，认知成本增加。

#### 问题 4：废弃的 `OnboardingGate` 仍残留在代码库

- **位置**：[onboarding-gate.tsx](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx)（313 行）
- **问题**：架构图明确记录"P2-1 已修复：用 middleware 强制重定向替代客户端 Dialog"，但 `OnboardingGate` 文件仍存在且未被任何代码 import（`grep OnboardingGate` 仅返回定义行）。文件内含 `isAdmin`/`isTeacher`/`isStudent`/`isParent` 用权限点反推角色的硬编码逻辑：
  ```ts
  const isAdmin = permissions.includes(Permissions.SETTINGS_ADMIN)
  const isTeacher = permissions.includes(Permissions.EXAM_CREATE)
  const isStudent = permissions.includes(Permissions.HOMEWORK_SUBMIT) && !permissions.includes(Permissions.EXAM_CREATE)
  const isParent = !permissions.includes(Permissions.EXAM_CREATE) && !permissions.includes(Permissions.HOMEWORK_SUBMIT) && permissions.includes(Permissions.EXAM_READ)
  ```
- **违反规则**：
  - 项目规则"前端权限判断统一使用 `usePermission().hasPermission()`，严禁 `role === 'xxx'` 硬编码"
  - 项目规则"避免 backwards-compatibility 残留"
- **后果**：误用风险（新人可能 import 它）；阅读代码困惑；增加了未使用的 313 行代码体积；权限点反推角色的逻辑本身存在漏洞（如同时拥有 EXAM_CREATE 和 HOMEWORK_SUBMIT 会被判定为 teacher）。

### 2.2 文件大小与组织（P1）

#### 问题 5：`onboarding-stepper.tsx` 451 行已接近 500 行上限

- **位置**：[onboarding-stepper.tsx](file:///e:/Desktop/CICD/src/modules/onboarding/components/onboarding-stepper.tsx)
- **问题**：单组件混合 4 步骤 UI 渲染、状态管理、子女行动态增删、URL 参数同步、表单提交、跳过逻辑、权限校验。文件中 `step === 0/1/2/3` 分支硬编码，新增"邮箱验证"、"隐私同意"等步骤需改大组件。
- **违反规则**：项目规则"React 组件 ≤ 500 行（复杂表单可放宽至 800 行）"虽未超出，但已接近上限，且组件未拆分。
- **后果**：单测困难；步骤间复用无法实现；新增步骤需大改。

#### 问题 6：`auth.ts` authorize 回调仍混合 7 类职责

- **位置**：[auth.ts#L34-L141](file:///e:/Desktop/CICD/src/auth.ts#L34-L141)
- **问题**：authorize 回调内联：① 速率限制 ② 用户查询 ③ 账户锁定校验 ④ bcrypt 密码比对 ⑤ 失败计数 ⑥ 2FA 调用（跨模块 import settings） ⑦ 角色加载与 `resolvePrimaryRole`。架构图 §3.2.4 标注"后续可选优化：authorize 回调可进一步拆分为 checkRateLimit / checkAccountLockout / verifyPassword / loadUserRoles，使 auth.ts 降至 ≤150 行"，但实际未拆分。
- **违反规则**：项目规则"Server Actions / Data Access 模块 ≤ 800 行"虽未超，但 authorize 单函数 107 行违反单一职责。
- **后果**：难以单测；任何修改都可能破坏登录链路；rate-limit 改为 Redis 时需改 auth.ts。

### 2.3 权限缺失与硬编码（P0/P1）

#### 问题 7：注册 Server Action 完全无安全防护

- **位置**：[register/page.tsx#registerAction](file:///e:/Desktop/CICD/src/app/(auth)/register/page.tsx#L36-L164)
- **问题**：
  1. **无速率限制**：auth.ts authorize 有 `rateLimit(RATE_LIMIT_RULES.LOGIN)`，但 registerAction 完全无限制
  2. **无 Zod 校验**：仅 `if (!email)` / `if (password.length < 6)` 字符串判断，未使用项目标准的 `OnboardingSchema` 模式
  3. **无邮箱验证流程**：注册即激活，无验证邮件、无管理员审批
  4. **无 CAPTCHA**：机器人可批量注册
  5. **无审计日志**：注册成功未调用 `logLoginEvent({ action: "signup", status: "success" })`（schema 中 `loginLogActionEnum` 已包含 "signup"，但实际未使用）
  6. **角色查找静默失败并自动创建**：第 92-94 行 `if (!roleRow) { await db.insert(roles).values({ name: "student" }) }`——任何访问 /register 的匿名用户都可触发 `roles` 表插入，且无 `isSystem: true`、无 `isEnabled: true`，污染 RBAC 配置
  7. **密码哈希 cost=10**：低于推荐 cost=12；未做 breached password 检测
  8. **错误信息泄露数据库细节**：第 158-162 行直接返回 `创建账户失败：${msg}`（含 sqlMessage）
- **违反规则**：
  - 项目规则"Server Action 必须使用 `requirePermission()` 进行权限校验"（注册虽无登录态，但应有等效防护）
  - 项目规则"输入使用 Zod 验证，验证失败返回结构化错误"
  - 项目安全规范"JWT/session ID 存储在 httpOnly + Secure + SameSite=Strict 的 Cookie 中"
  - 项目规则"禁止 `dangerouslySetInnerHTML`"——此处虽未用，但错误消息直接暴露 SQL 错误违反同源安全原则
- **后果**：批量注册攻击、roles 表污染、SQL 错误信息泄露、注册行为无法审计与监控。

#### 问题 8：`proxy.ts` 的 `resolveDefaultPath` 硬编码角色名

- **位置**：[proxy.ts#L45-L52](file:///e:/Desktop/CICD/src/proxy.ts#L45-L52)
  ```ts
  function resolveDefaultPath(roles: string[]): string {
    if (roles.includes("admin")) return "/admin/dashboard"
    if (roles.includes("grade_head") || roles.includes("teaching_head")) return "/teacher/dashboard"
    if (roles.includes("teacher")) return "/teacher/dashboard"
    if (roles.includes("student")) return "/student/dashboard"
    if (roles.includes("parent")) return "/parent/dashboard"
    return "/dashboard"
  }
  ```
- **问题**：硬编码 6 个角色名。同时 `onboarding/data-access.ts#resolveDefaultPathByRoles` 重复实现了相同逻辑，两份代码存在同步风险。
- **违反规则**：项目规则"严禁 `role === 'xxx'` 硬编码"（虽然 proxy 是服务端，但应使用权限点或角色配置驱动）。
- **后果**：新增自定义角色（如"教研员"、"访客"）时不会自动路由；与 RBAC 模块动态角色支持不一致；两份实现可能漂移。

#### 问题 9：onboarding 完成时角色信息从 JWT 读取但未与 DB 二次校验

- **位置**：[onboarding/actions.ts#L100-L107](file:///e:/Desktop/CICD/src/modules/onboarding/actions.ts#L100-L107) `const roleNames = ctx.roles`
- **问题**：`ctx.roles` 来自 JWT，但 JWT 在用户被撤销角色后到下次 refresh 之间存在窗口期。onboarding 仍按旧角色执行 `enrollStudentByInvitationCode` / `enrollTeacherByInvitationCode` / `bindParentToChild`，可能为已被撤销 student 角色的用户绑定班级。
- **违反规则**：项目规则"Server Action 必须调用 `requirePermission()` 进行权限校验"——onboarding Action 仅 `requireAuth()`，未对"执行 student/teacher/parent 角色特定操作"做权限校验。
- **后果**：被撤销角色的用户仍能完成 onboarding 并绑定班级/子女，造成数据污染。

### 2.4 国际化遗漏（P1）

#### 问题 10：`login-form.tsx` 完全未使用 i18n，硬编码英文

- **位置**：[login-form.tsx](file:///e:/Desktop/CICD/src/modules/auth/components/login-form.tsx)
- **问题**：硬编码 "Welcome back"、"Enter your email to sign in to your account"、"Email"、"Password"、"Forgot password?"、"Invalid email or password"、"Invalid 2FA code"、"Sign In with Email"、"Verify & Sign In"、"Or continue with"、"GitHub"、"Don't have an account?"、"Sign up"。但 `shared/i18n/messages/{en,zh-CN}/auth.json` 已存在完整翻译键，**完全未被使用**。
- **违反规则**：项目规则"所有用户可见文本必须适配 i18n（使用 next-intl），提取翻译键"。
- **后果**：中英文混杂（login-form 全英文，register-form 全中文）；多语言切换无效；与项目其他模块均使用 `useTranslations` 的模式不一致。

#### 问题 11：`register-form.tsx` 硬编码中文，未使用 i18n

- **位置**：[register-form.tsx](file:///e:/Desktop/CICD/src/modules/auth/components/register-form.tsx)
- **问题**：硬编码 "创建账户"、"填写以下信息以创建您的账户"、"姓名"、"邮箱"、"密码"、"出生日期"、"当前年龄：{age} 岁"、"检测到您是未成年人，请填写监护人信息"、"监护人姓名"、"监护人电话"、"监护人与您的关系"、"父亲"、"母亲"、"其他法定监护人"、"我已阅读并同意"、"《隐私政策》"、"《用户协议》"、"我确认已获得监护人同意使用本服务"、"创建账户"、"已有账户？"、"立即登录"。
- **违反规则**：同上。
- **后果**：英文用户无法理解；翻译键已存在但未使用；toast 错误消息也硬编码。

#### 问题 12：onboarding `actions.ts` 与 `data-access.ts` 错误消息硬编码中文

- **位置**：
  - [onboarding/actions.ts#L94,130,147,159,225](file:///e:/Desktop/CICD/src/modules/onboarding/actions.ts#L94)
  - [onboarding/data-access.ts#L90-103](file:///e:/Desktop/CICD/src/modules/onboarding/data-access.ts#L90-L103)
- **问题**：硬编码 "输入校验失败"、"班级码绑定失败"、"Onboarding 提交失败"、"未找到该邮箱对应的学生"、"学生未设置出生日期，请联系管理员"、"子女生日不匹配"、"子女手机号后 4 位不匹配"。`onboarding.json#toast` 已有 `submitFailed`、`inputInvalid` 等键，但 Server Action 未通过 `getTranslations` 读取。
- **违反规则**：项目规则"所有用户可见文本必须适配 i18n"。
- **后果**：英文 locale 下 toast 显示中文；与项目其他模块 Server Action 已迁移至 i18n 的模式不一致。

#### 问题 13：`(auth)` 路由组的 `error.tsx` 与 `not-found.tsx` 静态英文

- **位置**：
  - [error.tsx](file:///e:/Desktop/CICD/src/app/(auth)/error.tsx)
  - [not-found.tsx](file:///e:/Desktop/CICD/src/app/(auth)/not-found.tsx)
- **问题**：硬编码 "Authentication Error"、"There was a problem signing you in. Please try again."、"Try again"、"Page Not Found"、"The authentication page you are looking for does not exist."、"Return to Login"。
- **违反规则**：项目规则"所有用户可见文本必须适配 i18n"。
- **后果**：与 (auth) 路由组下的 login/register（混合中英文）语言不一致。

#### 问题 14：`privacy/page.tsx` 与 `terms/page.tsx` 静态中文

- **位置**：
  - [privacy/page.tsx](file:///e:/Desktop/CICD/src/app/(auth)/privacy/page.tsx)
  - [terms/page.tsx](file:///e:/Desktop/CICD/src/app/(auth)/terms/page.tsx)
- **问题**：整页隐私政策与用户协议硬编码中文，未提取 i18n 键。
- **违反规则**：项目规则"所有用户可见文本必须适配 i18n"。
- **后果**：英文 locale 下展示中文；维护政策文本需改代码而非改 JSON。

#### 问题 15：`AuthLayout` 左侧品牌 testimonial 硬编码英文且与 K12 场景不符

- **位置**：[auth-layout.tsx#L19-L22](file:///e:/Desktop/CICD/src/modules/auth/components/auth-layout.tsx#L19-L22)
- **问题**：
  ```tsx
  <blockquote className="space-y-2">
    <p className="text-lg">
      &ldquo;This platform has completely transformed how we deliver education to our students. The attention to detail and performance is unmatched.&rdquo;
    </p>
    <footer className="text-sm">Sofia Davis</footer>
  </blockquote>
  ```
  硬编码英文 testimonial 与虚构人物 "Sofia Davis"，与 K12 教育系统定位不符。
- **违反规则**：项目规则"所有用户可见文本必须适配 i18n"。
- **后果**：i18n 失效；学校白标部署无法配置 testimonial；与产品定位不符。

### 2.5 错误处理与边界（P1）

#### 问题 16：`LoginForm` 未区分账户锁定、速率限制、2FA 缺失等错误

- **位置**：[login-form.tsx#L62-L69](file:///e:/Desktop/CICD/src/modules/auth/components/login-form.tsx#L62-L69)
- **问题**：所有失败统一显示 "Invalid email or password" 或 "Invalid 2FA code"。`signIn("credentials", { redirect: false })` 返回的 `result.error` 是字符串（如 "CredentialsSignin"），代码仅判断 `!result?.error`，未针对不同 error code 给出差异化提示。
- **违反规则**：项目规则"明确处理空数据、无权限、网络异常等边界状态"。
- **后果**：合法用户被锁定后不知如何解锁；攻击者无法感知是否成功；2FA 缺失与密码错误提示混淆。

#### 问题 17：`LoginForm` 的 `preflightTwoFactorAction` 失败时静默降级

- **位置**：[login-form.tsx#L44-L46](file:///e:/Desktop/CICD/src/modules/auth/components/login-form.tsx#L44-L46)
  ```ts
  try {
    const preflight = await preflightTwoFactorAction(email)
    ...
  } catch {
    // 预检失败时静默降级为普通登录
  }
  ```
- **问题**：2FA 预检失败时降级为普通登录，但用户可能已启用 2FA，会被 authorize 拒绝，用户需重复输入密码。
- **违反规则**：项目规则"明确处理空数据、无权限、网络异常等边界状态"。
- **后果**：用户体验差；暴露 2FA 状态（攻击者可通过预检失败推断用户启用 2FA）。

#### 问题 18：`(auth)` 路由组无 `loading.tsx`

- **位置**：`src/app/(auth)/`（缺文件）
- **问题**：缺少 `loading.tsx`，违反项目硬约束"所有 student 路由必须包含 loading.tsx 和 error.tsx"（虽约束明确为 student 路由，但 (auth) 应同样对待）。
- **违反规则**：项目硬约束。
- **后果**：路由切换时无骨架屏；与 (dashboard) 路由组模式不一致。

#### 问题 19：`LoginForm` 与 `RegisterForm` 无 React Error Boundary 包裹

- **位置**：[login/page.tsx](file:///e:/Desktop/CICD/src/app/(auth)/login/page.tsx)、[register/page.tsx](file:///e:/Desktop/CICD/src/app/(auth)/register/page.tsx)
- **问题**：仅 `<Suspense fallback={null}>` 包裹 LoginForm，无 Error Boundary；如果组件抛错会冒泡到 `error.tsx` 全页错误，用户丢失已输入数据。
- **违反规则**：项目规则"每个独立的数据区块必须用 React Error Boundary 包裹"。
- **后果**：单个组件错误导致整页不可用。

#### 问题 20：`proxy.ts` 重定向时未携带原始路径到 onboarding 完成后回跳

- **位置**：[proxy.ts#L84-L90](file:///e:/Desktop/CICD/src/proxy.ts#L84-L90)
- **问题**：未登录用户访问 `/teacher/classes` 时被重定向到 `/login?callbackUrl=...`，但登录成功后若 onboarding 未完成又被重定向到 `/onboarding`，onboarding 完成后又跳转到默认路径 `/teacher/dashboard`，丢失了用户原始意图 `/teacher/classes`。
- **违反规则**：项目规则"URL parameters (e.g., `classId`) must be explicitly read and handled in target pages to prevent broken navigation"。
- **后果**：用户体验割裂，需重新导航到目标页面。

### 2.6 类型不安全（P0/P1）

#### 问题 21：`auth.ts` 多处 `as` 断言

- **位置**：
  - [auth.ts#L146](file:///e:/Desktop/CICD/src/auth.ts#L146) `const u = user as { id: string; role?: string; roles?: string[]; name?: string }`
  - [auth.ts#L196](file:///e:/Desktop/CICD/src/auth.ts#L196) `session.user.permissions = (token.permissions ?? []) as typeof token.permissions`
  - [auth.ts#L217-L223](file:///e:/Desktop/CICD/src/auth.ts#L217-L223) `(message as { userId?: string })?.userId ?? (message as { token?: { id?: string } })?.token?.id ?? ""`
- **问题**：使用 `as` 断言而非类型守卫。`next-auth.d.ts` 已声明 Session/JWT 类型，但 jwt callback 中 `user` 参数是 `any`（NextAuth 类型缺陷），需用类型守卫。
- **违反规则**：项目规则"禁止 `as` 断言（除类型收窄外）"。
- **后果**：类型与运行时不一致；重构易引入 bug；jwt callback 中 token.permissions 类型与 Permission[] 不兼容。

#### 问题 22：`proxy.ts` 中 `(token.roles as string[])` 断言

- **位置**：[proxy.ts#L93,99](file:///e:/Desktop/CICD/src/proxy.ts#L93-L99)
- **问题**：直接 `as string[]`，未做类型守卫；旧 token 数据格式异常时（如 token 为 v1 格式无 roles 字段）会 runtime error。
- **违反规则**：项目规则"禁止 `as` 断言"。
- **后果**：旧 token 用户访问时崩溃。

#### 问题 23：`register-form.tsx` `ActionState` 未指定泛型

- **位置**：[register-form.tsx#L20,38](file:///e:/Desktop/CICD/src/modules/auth/components/register-form.tsx#L20-L38)
- **问题**：`import type { ActionState } from "@/shared/types/action-state"` 与 `registerAction: (formData: FormData) => Promise<ActionState>` 未指定泛型参数 `T`，`data` 字段类型为 `unknown`。
- **违反规则**：项目规则"函数返回值必须显式标注"。
- **后果**：success 分支访问 `res.data` 时无类型提示。

### 2.7 单测不可行（P0）

#### 问题 24：auth 模块几乎无单元测试，e2e 测试与 UI 不一致

- **位置**：[tests/e2e/smoke-auth.spec.ts](file:///e:/Desktop/CICD/tests/e2e/smoke-auth.spec.ts)
- **问题**：
  1. `rate-limit.ts`、`password-policy.ts`、`role-utils.ts`、`bcrypt-utils.ts`、`http-utils.ts`、`password-security-service.ts`、`session.ts`、`auth-guard.ts`、`permissions.ts`（`resolvePermissions`）均无对应 `.test.ts`
  2. `auth.ts` 的 authorize/jwt/session 回调无测试
  3. `proxy.ts` 的路由权限判断无测试
  4. e2e 测试期望 `getByLabel("Full Name")` 与 `button { name: "Create Account" }`（英文），但 `register-form.tsx` 实际是中文"姓名"和"创建账户"——**e2e 必然失败**
- **违反规则**：项目规则"数据获取、计算、格式化等纯逻辑全部放入纯函数或 hooks，与 UI 分离；导出清晰的接口类型以便 mock"。
- **后果**：重构无回归保障；rate-limit 改为 Redis 时无对照基线；e2e 在 CI 失败或被 skip。

### 2.8 组件不可复用（P1）

#### 问题 25：`LoginForm` / `RegisterForm` 无法被复用与测试

- **位置**：[login-form.tsx](file:///e:/Desktop/CICD/src/modules/auth/components/login-form.tsx)、[register-form.tsx](file:///e:/Desktop/CICD/src/modules/auth/components/register-form.tsx)
- **问题**：
  - LoginForm 硬编码 `signIn("credentials")`，未抽象 `AuthService` 接口，与 `next-auth/react` 紧耦合
  - RegisterForm 接受 `registerAction` props 但未定义数据契约接口（`Promise<ActionState>` 无泛型）
  - 两个组件均直接 import `@/modules/settings/actions-security`（login-form）和通过 props 注入（register-form），方式不统一
  - GitHub 登录按钮是死链无 onClick
- **违反规则**：项目规则"完全解耦：通过定义 TypeScript 接口抽象数据依赖，使用 React Context 注入数据服务"。
- **后果**：换 SSO 提供商需重写组件；无法在测试中 mock；多角色场景下无法复用。

#### 问题 26：`AuthLayout` 静态硬编码品牌信息

- **位置**：[auth-layout.tsx#L12-L22](file:///e:/Desktop/CICD/src/modules/auth/components/auth-layout.tsx#L12-L22)
- **问题**：`GraduationCap` 图标、"Next_Edu" 品牌名、"Sofia Davis" testimonial 全部硬编码，无 props 注入。
- **违反规则**：项目规则"最大化复用：识别四个角色共用的 UI 块和业务逻辑块，抽象为泛型组件和 hooks"。
- **后果**：学校白标部署无法配置 logo/标语；无法在 admin 设置页预览登录页样式。

### 2.9 安全性（P0/P1）

#### 问题 27：`rate-limit.ts` 是单实例内存实现

- **位置**：[rate-limit.ts#L1-L9](file:///e:/Desktop/CICD/src/shared/lib/rate-limit.ts#L1-L9)
  ```ts
  /**
   * In-memory rate limiter (single-instance only).
   * For multi-instance deployments, replace with @upstash/ratelimit + @upstash/redis.
   */
  const rateLimitMap = new Map<string, RateLimitEntry>()
  ```
- **问题**：注释明确说"For multi-instance deployments, replace with @upstash/ratelimit + @upstash/redis"，但当前是 `Map` 单实例。
- **违反规则**：项目规则"安全性：所有敏感数据查询必须在 data-access 层结合当前用户权限过滤"。
- **后果**：多实例部署时速率限制失效，攻击者可分散请求绕过；K8s 水平扩容后暴力破解防护失效。

#### 问题 28：JWT 中存储 permissions 数组导致 token 体积过大

- **位置**：[auth.ts#L153,184](file:///e:/Desktop/CICD/src/auth.ts#L153) `token.permissions = await resolvePermissions(allRoles)`
- **问题**：67 个权限点字符串数组写入 JWT（admin 角色约 67 个 × 平均 20 字符 = ~1.3KB），JWT cookie 体积可能超过 4KB 限制；权限变更需用户重新登录或 JWT refresh 才能生效。
- **违反规则**：项目规则"JWT/session ID 存储在 httpOnly + Secure + SameSite=Strict 的 Cookie 中"——cookie 体积过大可能被浏览器截断。
- **后果**：cookie 体积超限导致 session 丢失；权限撤销有延迟；admin 角色拥有最多权限，最易触发。

#### 问题 29：`onboarding/data-access.ts` `bindParentToChild` 未做独立速率限制

- **位置**：[onboarding/data-access.ts#bindParentToChild](file:///e:/Desktop/CICD/src/modules/onboarding/data-access.ts#L80-L127)
- **问题**：家长绑定子女的三因子验证（生日 365 × 手机后4 10000 = 3.65M）虽然组合空间大，但单次 onboarding Action 内可循环调用 10 次（`children.length ≤ 10`），无独立速率限制；被撤销子女关系的家长可反复尝试枚举绑定。
- **违反规则**：项目安全规范。
- **后果**：枚举攻击窗口；audit_logs 仅记录成功，未记录失败次数。

#### 问题 30：`sessions` 表存在但 JWT 策略下未使用，远程登出功能形同虚设

- **位置**：
  - [schema.ts#L84](file:///e:/Desktop/CICD/src/shared/db/schema.ts#L84) `sessions` 表定义
  - [auth.ts#L25](file:///e:/Desktop/CICD/src/auth.ts#L25) `session: { strategy: "jwt" }`
  - [actions-security.ts#revokeAllOtherSessionsAction#L362-L403](file:///e:/Desktop/CICD/src/modules/settings/actions-security.ts#L362-L403)
- **问题**：NextAuth 配置 JWT 策略，但 schema 中 `sessions` 表仍存在（无数据）；`revokeAllOtherSessionsAction` 删 sessions 表但实际无活跃记录，注释承认 "JWT-based, no active DB sessions"。
- **违反规则**：项目规则"避免 backwards-compatibility 残留"。
- **后果**：用户误以为可远程登出其他设备；sessions 表 schema 与运行时不一致，增加困惑。

#### 问题 31：`proxy.ts` 未对 `/admin/*` 子路径做细粒度权限校验

- **位置**：[proxy.ts#L13-L19](file:///e:/Desktop/CICD/src/proxy.ts#L13-L19)
- **问题**：`/admin` 前缀匹配 `school:manage` 即放行所有 admin 子路径。`/admin/ai-settings`、`/admin/roles`、`/admin/permissions` 已用 `SPECIFIC_ROUTE_PERMISSIONS` 例外处理，但 `/admin/announcements`、`/admin/audit-logs`、`/admin/elective`、`/admin/questions`、`/admin/school/*`、`/admin/users` 等仍依赖页面内 `requirePermission` 兜底。
- **违反规则**：项目规则"安全性：所有敏感数据查询必须在 data-access 层结合当前用户权限过滤，Server Action 二次校验"——proxy 应作为第一道防线。
- **后果**：页面若忘记调用 `requirePermission` 即造成越权；与"`/teacher` 和 `/parent` 已使用角色独有权限点"的设计不一致。

### 2.10 a11y 与性能（P1）

#### 问题 32：`LoginForm` 无 ARIA 错误关联、无"显示密码"按钮、无 Caps Lock 检测

- **位置**：[login-form.tsx#L89,111-117,156-158](file:///e:/Desktop/CICD/src/modules/auth/components/login-form.tsx)
- **问题**：
  1. 错误 `<p className="text-sm text-red-600">{error}</p>` 未使用 `role="alert"`、未 `aria-describedby` 关联到输入框
  2. 密码输入框无显示/隐藏切换
  3. 无 Caps Lock 检测提示
  4. GitHub 登录按钮无 onClick 是死链
- **违反规则**：项目规则"可访问性（a11y）：语义化标签、ARIA 属性、键盘导航"。
- **后果**：屏幕阅读器用户无法感知错误；违反 WCAG 2.2 Target Size 与 Focus Not Obscured。

#### 问题 33：`(auth)` 路由组无 loading.tsx、无骨架屏

- **位置**：`src/app/(auth)/`（缺文件）
- **问题**：路由切换无骨架屏；login-form 内部用 `isLoading` state 控制按钮禁用，但无整体加载态。
- **违反规则**：项目规则"异步数据使用 React Suspense + 骨架屏"。
- **后果**：网络慢时用户体验差。

### 2.11 监控埋点（P1）

#### 问题 34：auth 模块未定义任何 `trackEvent` 事件

- **位置**：[track-event.ts#EventName](file:///e:/Desktop/CICD/src/shared/lib/track-event.ts)
- **问题**：`EventName` 类型枚举中无 `auth.signin`、`auth.signout`、`auth.signup`、`auth.2fa_enabled`、`auth.password_reset`、`auth.account_locked`、`auth.rate_limited` 等。
- **违反规则**：项目规则"监控：方案中预留关键操作埋点接口"。
- **后果**：登录成功率、2FA 启用率、账户锁定触发率等核心指标无法统计；无法接 Sentry/PostHog。

#### 问题 35：`auth.ts` `events.signIn/signOut` 仅写 login_logs，未触发业务埋点

- **位置**：[auth.ts#L205-L232](file:///e:/Desktop/CICD/src/auth.ts#L205-L232)
- **问题**：登录成功仅 `logLoginEvent`，未 `void trackEvent({ name: "auth.signin", ... })`。
- **违反规则**：项目规则"监控：方案中预留关键操作埋点接口"。
- **后果**：监控仪表盘缺失；无法告警异常登录地理/设备。

### 2.12 可扩展性（P1）

#### 问题 36：`proxy.ts` 路由权限配置硬编码，无配置驱动

- **位置**：[proxy.ts#L13-L43](file:///e:/Desktop/CICD/src/proxy.ts#L13-L43)
- **问题**：`ROUTE_PERMISSIONS`、`DASHBOARD_ROUTE_PERMISSIONS`、`SPECIFIC_ROUTE_PERMISSIONS`、`API_PERMISSIONS` 4 个常量写死在文件中，新增角色或权限点需改代码。
- **违反规则**：项目规则"可扩展性：采用配置驱动设计"。
- **后果**：与 RBAC 模块的 `DATA_SCOPE_RULES` 配置驱动不一致；新增路由时易遗漏权限配置。

#### 问题 37：onboarding 流程不支持配置驱动

- **位置**：[onboarding-stepper.tsx](file:///e:/Desktop/CICD/src/modules/onboarding/components/onboarding-stepper.tsx)
- **问题**：步骤硬编码 4 步（roleConfirm/basicInfo/roleInfo/complete），新增"邮箱验证"、"隐私同意"、"家长扫码绑定子女"等步骤需改组件代码。
- **违反规则**：项目规则"可扩展性：采用配置驱动设计，例如通过角色配置决定该模块渲染哪些 Widget/子模块"。
- **后果**：扩展性差；与项目其他模块（如 dashboard widget-configs.ts）的配置驱动模式不一致。

### 2.13 数据库与 Schema 一致性（P2）

#### 问题 38：`users` 表混合了认证、用户资料、未成年人保护等多类字段

- **位置**：[schema.ts#L27-L57](file:///e:/Desktop/CICD/src/shared/db/schema.ts#L27-L57)
- **问题**：`users` 表 31 个字段混合：认证（password, emailVerified, image）、用户资料（name, phone, address, gender, age）、未成年人保护（birthDate, guardianName, guardianPhone, guardianRelation, consentAcceptedAt）、组织（gradeId, departmentId）、onboarding（onboardedAt）。
- **违反规则**：项目规则"模块标准结构要求 schema 按职责拆分"。
- **后果**：schema 文件 1111 行（架构图 P0-1 已记录）；认证模块修改 password 字段时影响用户资料模块；难以按表分库分表。

---

## 三、行业差距对比

### 3.1 K12 认证主流模式对标

| 维度 | 行业最佳实践 | 本项目现状 | 差距 |
|------|------------|----------|------|
| 账号创建 | **管理员预分配 + 开放注册关闭**（PowerSchool、Veracross、Clever） | 开放注册，任何人可注册 student 账号 | **重大差距**：未实现邀请码/批量导入预分配；任何人可污染 student 角色池 |
| SSO 单点登录 | **Clever SSO（2750 万师生）/ SAML/OIDC / Clever Badges（低年级二维码徽章）** | 仅 Credentials Provider，无 SSO/SAML/OIDC | **重大差距**：未对接 Google Workspace / Microsoft Entra ID / ClassLink；无法满足大型学区统一身份需求 |
| 家长-学生绑定 | **PowerSchool Access ID + Access Password 双因子凭据**（校方发放） | 邮箱 + 生日 + 手机后4 三因子（学生已有信息） | **中等差距**：组合空间 3.65M 已接近，但因子均来自学生已有数据，校方未发放独立绑定码；缺少学校发放的 Access Code 表 |
| 邮箱验证 | 注册后强制邮箱验证 + 接受条款 | 注册即激活，无邮箱验证 | **重大差距**：违反 PowerSchool/Veracross 标准 |
| Breached Password 检测 | Auth0 Breached Passwords 比对已知泄露库 | 仅 bcrypt cost=10 | **重大差距**：未接入 HaveIBeenPwned API 或 k-anonymity 范围查询 |
| 2FA 覆盖 | TOTP + 备用码 + 推送 + 短信 + WebAuthn 多因子 | TOTP + 备用码 | **良好**：TOTP+备用码已实现，符合主流；可扩展 WebAuthn |
| 未成年人保护 | COPPA/FERPA 声明 + 监护人同意书 | 隐私政策/用户协议页 + 注册时勾选 | **良好**：已实现未成年人监护人字段；但缺少监护人电子签名/同意书记录 |

### 3.2 UI/UX 差距

| 维度 | 行业最佳实践 | 本项目现状 | 差距 |
|------|------------|----------|------|
| 品牌展示 | PowerSchool District Branding / Auth0 Universal Login 自定义域+模板 | AuthLayout 硬编码 Next_Edu + Sofia Davis testimonial | **差距**：无学校 logo/配色/标语自定义；无 admin 端品牌配置入口 |
| 多角色切换 UI | Auth0 Actions 在 Token 中下发多角色，前端 role-switcher 下拉 | 已实现（layout/app-sidebar.tsx 多角色切换） | **达标** |
| Onboarding 步骤 | 3-5 步可跳过式 + 进度持久化到 localStorage/Cookie | 4 步可跳过 + URL query 持久化 | **达标**：URL query 比 localStorage 更优 |
| 错误反馈 | Auth0 区分"用户不存在/密码错/账户锁定/速率限制/2FA 缺失" | 统一 "Invalid email or password" | **差距**：违反 NIST SP 800-63-4 "明确错误反馈"建议；用户体验差 |
| 显示密码按钮 | WCAG 2.2 Target Size ≥24×24 + 可见焦点环 | 无 | **差距**：违反 WCAG 2.2 |
| Caps Lock 检测 | 主流浏览器原生支持 + UI 提示 | 无 | **差距** |

### 3.3 安全合规差距

| 维度 | 行业最佳实践 | 本项目现状 | 差距 |
|------|------------|----------|------|
| 账户锁定 | NIST SP 800-63-4 risk-based throttling，5-10 次失败后临时锁定 15-30 分钟 | 5 次失败后锁定 30 分钟（PASSWORD_RULES） | **达标** |
| 速率限制 | 按 IP + 账号双维度，登录端点 ≥10 req/min + 指数退避 | 5 attempts / 15 min（IP+email），单实例内存 | **差距**：未实现 Redis 分布式；阈值偏严（5 次/15min 可能误锁合法用户） |
| 密码策略 | NIST 800-63-4 取消强制复杂度与定期轮换，最低 8 位，强制 breached password 检查 | 8 位 + 大小写 + 数字（无特殊字符要求） | **部分达标**：长度符合；未做 breached password 检查；强制复杂度与 NIST 最新建议有偏差 |
| 审计日志保留 | CoSN K-12 CVAT + CIS 2025 K-12 报告建议 ≥1 年；FERPA 要求可审计 | login_logs 表存在但无保留期配置 | **差距**：无保留期清理任务（audit 模块已有 retention.ts，但 login_logs 未接入） |
| 跨角色越权 | 服务端强制 RBAC/ABAC，禁止仅前端隐藏 | proxy.ts + requirePermission 双重校验 | **达标** |
| Sessions 远程登出 | JWT 黑名单或短期 token + refresh token | sessions 表存在但 JWT 策略下未使用 | **差距**：远程登出形同虚设 |

### 3.4 a11y 与 i18n 差距

| 维度 | 行业最佳实践 | 本项目现状 | 差距 |
|------|------------|----------|------|
| WCAG 2.2 | Focus Not Obscured (AA)、Target Size ≥24×24、Consistent Help (A)、Dragging Movements (AA) | LoginForm 无 ARIA 错误关联、无显示密码按钮、无 Caps Lock 检测 | **差距** |
| ARIA | `aria-label`、`aria-invalid`、`aria-describedby`、`role="alert"` | LoginForm 错误 p 标签无 role="alert" | **差距** |
| 键盘导航 | 完整 Tab 序、可见焦点环（对比度 ≥3:1） | 基本支持，但未显式验证 | **部分达标** |
| i18n | PowerSchool 支持 EN/ES/ZH，URL 持久化 | auth.json/onboarding.json 存在但 LoginForm/RegisterForm 未使用 | **重大差距**：i18n 翻译键已准备但完全未接入 |

### 3.5 可观测性差距

| 维度 | 行业最佳实践 | 本项目现状 | 差距 |
|------|------------|----------|------|
| 核心指标 | 登录成功率、首次登录失败原因分布、2FA 启用率、密码重置率、锁定触发率、SSO 同步失败率 | login_logs 表记录原始事件，但无聚合仪表盘 | **差距**：无仪表盘；无埋点 |
| 异常告警 | 异常登录地理/设备、暴力破解阈值触发、breached password 命中激增；接入 SIEM | 无 | **重大差距** |
| 审计日志切片 | 按学区/学校/角色维度切片 | 仅按 userId/userEmail | **差距** |

---

## 四、改进优先级建议

### P0（紧急，影响安全/架构合规）

| # | 问题 | 改进方向 |
|---|------|---------|
| P0-1 | 注册 Server Action 直接访问 DB（问题 1） | 新建 `modules/auth/data-access.ts` 与 `modules/auth/actions.ts`，将 `registerAction` 迁移至 actions.ts 并通过 data-access 访问 users 表；删除页面内联 `registerAction` 与重复的 `calcAge`/`normalizeBcryptHash` |
| P0-2 | `auth.ts` 跨模块依赖 settings（问题 2） | 将 2FA 校验逻辑从 `modules/settings/actions-security.ts` 抽取到 `modules/auth/services/two-factor-service.ts`（纯函数 + 通过 data-access 访问 settings 表），auth.ts 改为 `await import("@/modules/auth/services/two-factor-service")` |
| P0-3 | onboarding 页面绕过统一认证入口（问题 3） | 改 `onboarding/page.tsx` 使用 `getAuthContext()` 替代直接 `auth()`，未登录抛 `PermissionDeniedError` 由 `route-error.tsx` 处理 |
| P0-4 | 废弃 OnboardingGate 残留（问题 4） | 删除 `src/shared/components/onboarding-gate.tsx` |
| P0-5 | 注册接口无安全防护（问题 7） | 在 `modules/auth/actions.ts` 中：① 添加 `registerRateLimit`（独立于 LOGIN）；② 用 `RegisterSchema` Zod 校验；③ 注册成功调用 `logLoginEvent({ action: "signup" })`；④ 删除"自动创建 student 角色"逻辑，改为查找失败时返回错误；⑤ 错误消息不暴露 SQL 细节；⑥ 接入 CAPTCHA（hCaptcha 或 Cloudflare Turnstile） |
| P0-6 | `auth.ts` 多处 `as` 断言（问题 21） | 用类型守卫替代：`function isAuthUser(u: unknown): u is { id: string; role?: string; ... }`；jwt callback 中 token 类型已声明，无需断言 |
| P0-7 | e2e 测试与 UI 不一致（问题 24） | 重写 `tests/e2e/smoke-auth.spec.ts`，使用 i18n 后的翻译键或 `data-testid` 定位；同时为 `rate-limit.ts`/`password-policy.ts`/`role-utils.ts`/`bcrypt-utils.ts` 添加 `.test.ts` 单元测试 |
| P0-8 | `proxy.ts` resolveDefaultPath 硬编码角色（问题 8） | 抽取 `shared/lib/route-resolver.ts`，按权限点优先 + 角色兜底解析默认路径；`proxy.ts` 与 `onboarding/data-access.ts` 共用一份实现 |

### P1（重要，影响体验/可维护性）

| # | 问题 | 改进方向 |
|---|------|---------|
| P1-1 | LoginForm/RegisterForm 未使用 i18n（问题 10/11/12/13/14/15） | 接入 `useTranslations("auth")` / `useTranslations("onboarding")`；Server Action 通过 `getTranslations` 读取错误消息；扩展 `auth.json` 增加 `errors.accountLocked`/`errors.rateLimited`/`errors.twoFactorRequired` 等键 |
| P1-2 | onboarding-stepper 拆分（问题 5） | 拆为 `step-role-confirm.tsx`/`step-basic-info.tsx`/`step-role-info.tsx`/`step-complete.tsx` 4 个子组件 + `useOnboardingForm` hook 管理状态；主组件仅做步骤编排 |
| P1-3 | auth.ts authorize 回调拆分（问题 6） | 拆为 `checkRateLimit`/`checkAccountLockout`/`verifyPassword`/`verifyTwoFactor`/`loadUserRoles` 5 个纯函数 + service，authorize 仅编排 |
| P1-4 | LoginForm 错误反馈细化（问题 16/17/32） | 区分 error code（`CredentialsSignin`/`CredentialsSignout`/`OAuthCallbackError`）；添加 ARIA `role="alert"` 与 `aria-describedby`；添加显示密码按钮、Caps Lock 检测 |
| P1-5 | (auth) 路由组添加 loading.tsx + Error Boundary（问题 18/19/33） | 新建 `app/(auth)/loading.tsx` 骨架屏；用 `shared/components/widget-boundary.tsx` 包裹 LoginForm/RegisterForm |
| P1-6 | rate-limit 改为分布式（问题 27） | 抽象 `RateLimiter` 接口，默认内存实现，可注入 Redis 实现；环境变量 `RATE_LIMIT_DRIVER=memory\|redis` 切换 |
| P1-7 | JWT 体积优化（问题 28） | 方案 A：JWT 仅存 `userId` + `roles`，permissions 每次请求通过 `getAuthContext()` 从 DB/缓存查询（牺牲少量性能换 token 体积）；方案 B：JWT 存 permissions 哈希，DB 中按哈希缓存权限集 |
| P1-8 | bindParentToChild 速率限制（问题 29） | 在 `modules/onboarding/actions.ts` 中针对 `children.length` 添加独立速率限制 key `onboarding:bind:{parentId}`，限制每小时 5 次 |
| P1-9 | sessions 表清理（问题 30） | 删除 schema.ts 中 `sessions` 表定义 + `revokeAllOtherSessionsAction` 改为基于 JWT 短期 token + refresh token 黑名单；或在 `auth.ts` 中切换为 `strategy: "database"`（不推荐，性能差） |
| P1-10 | proxy.ts 细粒度权限（问题 31） | 扩展 `SPECIFIC_ROUTE_PERMISSIONS` 覆盖 `/admin/announcements`/`/admin/audit-logs`/`/admin/users` 等子路径；或改为基于 RBAC 模块的 `ROUTE_PERMISSION_CONFIG` 配置驱动 |
| P1-11 | trackEvent 接入 auth 事件（问题 34/35） | 在 `EventName` 中新增 `auth.signin_success`/`auth.signin_failure`/`auth.signout`/`auth.signup`/`auth.2fa_enabled`/`auth.account_locked`/`auth.rate_limited`；`auth.ts` events.signIn/signOut 与 authorize 失败分支调用 `void trackEvent(...)` |
| P1-12 | proxy.ts 配置驱动（问题 36） | 抽取 `shared/lib/route-permissions.ts`，导出 `ROUTE_PERMISSIONS` 等常量；proxy.ts 从此文件 import |
| P1-13 | onboarding 配置驱动（问题 37） | 定义 `OnboardingStepConfig` 类型 + `ONBOARDING_STEPS` 数组，根据角色过滤步骤；onboarding-stepper 仅渲染配置 |

### P2（中长期，影响可扩展性/合规）

| # | 问题 | 改进方向 |
|---|------|---------|
| P2-1 | users 表字段混合（问题 38） | 长期拆分：`users`（认证核心：id/email/password/emailVerified）+ `user_profiles`（资料：name/phone/address/gender/age）+ `user_minor_protection`（监护人信息）+ `user_onboarding`（onboardedAt）；通过 view 或 data-access 层聚合 |
| P2-2 | 引入 SSO/SAML/OIDC | 接入 NextAuth 的 `GoogleProvider`/`MicrosoftEntraIDProvider`/`SAMLProvider`；admin 端配置 IdP 元数据 |
| P2-3 | 引入邀请码预分配机制 | 新增 `invitation_codes` 表（code/email/role/classId/createdAt/expiresAt/usedBy）；admin 批量生成邀请码替代开放注册 |
| P2-4 | Breached Password 检测 | 接入 `haveibeenpwned` API（k-anonymity 范围查询）；注册/改密时校验 |
| P2-5 | 登录审计日志保留期 | 复用 audit 模块的 `retention.ts`，为 `login_logs` 配置 1 年保留期；添加定时任务清理 |
| P2-6 | AuthLayout 品牌配置 | admin 端新增"学校品牌"配置（logo/配色/标语/testimonial）；AuthLayout 通过 props 注入 |
| P2-7 | 监控仪表盘 | 新增 admin/monitoring 页面展示登录成功率/2FA 启用率/锁定触发率；接入 Sentry/PostHog |
| P2-8 | WebAuthn 支持 | 接入 `@simplewebauthn/server`；2FA 设置页新增 WebAuthn 选项 |

---

## 五、架构图同步说明

本次审计发现架构图（`004_architecture_impact_map.md` 与 `005_architecture_data.json`）需补充/修改以下节点：

### 5.1 需新增节点

1. **`modules/onboarding`** 作为独立模块章节（§2.X），导出 `getOnboardingStatusAction`/`completeOnboardingAction`/`OnboardingSchema`，依赖 `shared/*` + `modules/classes/data-access` + `modules/rbac/lib/data-scope-resolver`（间接）。
2. **`shared/components/onboarding-gate.tsx`** 加入 shared/components 清单，标记为"已废弃，待删除"。
3. **`shared/lib/route-resolver.ts`**（重构后新增）作为 proxy.ts 与 onboarding/data-access.ts 共用的纯函数。

### 5.2 需修改节点

1. **§3 根目录 auth.ts**：
   - 依赖列表新增 `modules/settings/actions-security`（当前遗漏）
   - 标注"重构后将改为依赖 `modules/auth/services/two-factor-service`"
2. **§2.24 modules/auth**：
   - 文件清单新增 `data-access.ts`、`actions.ts`、`schema.ts`、`types.ts`、`services/two-factor-service.ts`（重构后）
   - 删除"纯 UI 模块，无 data-access/actions/types"描述
3. **§3 proxy.ts**：
   - 导出函数列表新增 `resolveDefaultPath`（当前遗漏）
   - 依赖列表新增 `shared/lib/route-resolver`（重构后）
4. **`shared/lib` 清单**：
   - 新增 `route-permissions.ts`、`route-resolver.ts`（重构后）

### 5.3 需删除节点

1. **`shared/components/onboarding-gate.tsx`** 标记为"待删除（P0-4）"，重构后从清单移除。
2. **`app/api/onboarding/status/route.ts` 与 `app/api/onboarding/complete/route.ts`** 标记为"已废弃（410 Gone）"，重构后可删除。

---

## 六、重构方案设计

### 6.1 完全解耦：AuthService 接口抽象与依赖注入

#### 6.1.1 模块边界重新划分

```
src/modules/auth/
├─ actions.ts                    # Server Actions（registerAction / loginRateLimitedAction）
├─ data-access.ts                # data-access（createUser / getUserByEmail / 等）
├─ schema.ts                     # Zod（RegisterSchema / LoginSchema）
├─ types.ts                      # 类型（AuthService interface / RegisterInput / 等）
├─ services/
│  ├─ two-factor-service.ts     # 2FA 校验纯函数（供 auth.ts 调用）
│  ├─ password-service.ts        # 密码哈希纯函数 + breached password 检测
│  └─ rate-limit-service.ts      # 速率限制抽象（memory / redis 双实现）
├─ context/
│  └─ auth-service-provider.tsx  # 客户端 Context 注入 AuthService
├─ hooks/
│  ├─ use-login-form.ts          # 登录表单状态机
│  └─ use-register-form.ts       # 注册表单状态机
├─ components/
│  ├─ auth-layout.tsx            # 布局（接 brand props）
│  ├─ login-form.tsx             # 登录表单（接 AuthService 注入）
│  ├─ register-form.tsx          # 注册表单（接 RegisterService 注入）
│  ├─ two-factor-input.tsx       # 2FA 验证码输入组件（可复用）
│  ├─ password-input.tsx         # 密码输入 + 显示切换 + Caps Lock 检测（可复用）
│  ├─ oauth-buttons.tsx           # OAuth 按钮组（GitHub/Google/Microsoft）
│  ├─ brand-panel.tsx            # 品牌左侧面板（接 brand 配置）
│  └─ auth-error-boundary.tsx    # 认证错误边界
└─ config/
   └─ brand.ts                   # 默认品牌配置（可被 admin 覆盖）
```

#### 6.1.2 AuthService 接口定义

```ts
// src/modules/auth/types.ts
export interface AuthService {
  /** 登录预检：是否需要 2FA */
  preflightTwoFactor(email: string): Promise<{ required: boolean }>
  /** 执行登录 */
  signIn(input: LoginInput): Promise<SignInResult>
  /** 执行登出 */
  signOut(): Promise<void>
  /** 获取当前 session */
  getSession(): Promise<AppSession | null>
}

export interface RegisterService {
  /** 注册新用户 */
  registerUser(input: RegisterInput): Promise<ActionState<RegisterResult>>
  /** 检查邮箱是否已注册 */
  checkEmailAvailable(email: string): Promise<boolean>
}

// 默认实现：NextAuth adapter
export const createNextAuthService = (): AuthService => ({ ... })
```

#### 6.1.3 客户端 Context 注入

```tsx
// src/modules/auth/context/auth-service-provider.tsx
"use client"
const AuthServiceContext = createContext<AuthService | null>(null)

export function AuthServiceProvider({ service, children }: {
  service: AuthService
  children: React.ReactNode
}) {
  return <AuthServiceContext.Provider value={service}>{children}</AuthServiceContext.Provider>
}

export function useAuthService(): AuthService {
  const svc = useContext(AuthServiceContext)
  if (!svc) throw new Error("useAuthService must be used within AuthServiceProvider")
  return svc
}
```

页面使用：

```tsx
// app/(auth)/login/page.tsx
import { createNextAuthService } from "@/modules/auth/services/next-auth-service"
import { AuthServiceProvider } from "@/modules/auth/context/auth-service-provider"
import { LoginForm } from "@/modules/auth/components/login-form"

const authService = createNextAuthService()

export default function LoginPage() {
  return (
    <AuthServiceProvider service={authService}>
      <ErrorBoundary>
        <LoginForm />
      </ErrorBoundary>
    </AuthServiceProvider>
  )
}
```

**收益**：LoginForm 不再直接 import `next-auth/react`，可被测试 mock；未来切换 SSO 提供商只需替换 service 实现；不同学校可注入不同 brand 配置。

### 6.2 组合优先：步骤组件拆分与 hooks 复用

#### 6.2.1 onboarding-stepper 拆分

```tsx
// src/modules/onboarding/components/onboarding-stepper.tsx（重构后）
export function OnboardingStepper({ initialStatus }: OnboardingStepperProps) {
  const { step, goToStep } = useOnboardingStep()
  const form = useOnboardingForm(initialStatus)

  return (
    <OnboardingStepContainer step={step} total={form.stepKeys.length}>
      <OnboardingStepRoleConfirm v-if={step === 0} role={form.primaryRole} />
      <OnboardingStepBasicInfo v-if={step === 1} form={form} />
      <OnboardingStepRoleInfo v-if={step === 2} form={form} />
      <OnboardingStepComplete v-if={step === form.maxStep} />
      <OnboardingStepFooter
        step={step}
        maxStep={form.maxStep}
        canNext={form.canNext}
        canSkip={form.canSkip}
        onBack={() => goToStep(step - 1)}
        onNext={() => goToStep(step + 1)}
        onFinish={form.submit}
      />
    </OnboardingStepContainer>
  )
}
```

#### 6.2.2 共享 hooks

```ts
// src/modules/auth/hooks/use-login-form.ts
export function useLoginForm() {
  const [isLoading, setIsLoading] = useState(false)
  const [requiresTwoFactor, setRequiresTwoFactor] = useState(false)
  const [error, setError] = useState<string | null>(null)
  const authService = useAuthService()
  // ... 状态机逻辑
  return { isLoading, requiresTwoFactor, error, submit }
}
```

### 6.3 国际化就绪：翻译文件结构示例

#### 6.3.1 `auth.json` 扩展

```json
{
  "login": {
    "title": "登录",
    "subtitle": "使用您的账号登录 Next_Edu",
    "email": "邮箱",
    "password": "密码",
    "rememberMe": "记住我",
    "signIn": "登录",
    "signingIn": "登录中...",
    "forgotPassword": "忘记密码？",
    "noAccount": "还没有账号？",
    "register": "注册",
    "twoFactor": {
      "title": "请输入二次验证码",
      "subtitle": "输入认证器应用中的 6 位数字",
      "placeholder": "123456",
      "hint": "请输入 6 位认证器代码或 8 位备份码",
      "back": "返回登录",
      "verify": "验证并登录"
    }
  },
  "register": {
    "title": "创建账户",
    "subtitle": "填写以下信息以创建您的账户",
    "name": "姓名",
    "email": "邮箱",
    "password": "密码",
    "birthDate": "出生日期",
    "currentAge": "当前年龄：{age} 岁",
    "minorProtection": {
      "title": "检测到您是未成年人，请填写监护人信息",
      "guardianName": "监护人姓名",
      "guardianPhone": "监护人电话",
      "guardianRelation": "监护人与您的关系",
      "relations": {
        "father": "父亲",
        "mother": "母亲",
        "other": "其他法定监护人"
      },
      "consent": "我确认已获得监护人同意使用本服务"
    },
    "consent": {
      "agree": "我已阅读并同意",
      "privacy": "《隐私政策》",
      "terms": "《用户协议》"
    },
    "signUp": "创建账户",
    "hasAccount": "已有账号？",
    "signIn": "登录"
  },
  "errors": {
    "invalidCredentials": "邮箱或密码错误",
    "emailExists": "该邮箱已被注册",
    "passwordTooWeak": "密码强度不足",
    "accountLocked": "账户已锁定，请 {minutes} 分钟后重试",
    "rateLimited": "登录尝试过于频繁，请稍后再试",
    "twoFactorRequired": "需要二次验证",
    "twoFactorInvalid": "二次验证码错误",
    "twoFactorBackupConsumed": "已使用备份码，剩余 {remaining} 个",
    "network": "网络异常，请稍后重试",
    "unknown": "未知错误，请稍后重试"
  },
  "layout": {
    "brandName": "Next_Edu",
    "testimonial": {
      "quote": "这款平台彻底改变了我们为学生提供教育的方式。对细节的关注和性能无与伦比。",
      "author": "Sofia Davis"
    },
    "orContinueWith": "或使用以下方式登录"
  },
  "error": {
    "title": "认证错误",
    "description": "登录时出现问题，请重试",
    "retry": "重试"
  },
  "notFound": {
    "title": "页面未找到",
    "description": "您查找的认证页面不存在",
    "backToLogin": "返回登录"
  }
}
```

#### 6.3.2 Server Action i18n

```ts
// src/modules/auth/actions.ts
"use server"
import { getTranslations } from "next-intl/server"

export async function registerAction(
  prevState: ActionState<RegisterResult> | null,
  formData: FormData
): Promise<ActionState<RegisterResult>> {
  const t = await getTranslations("auth")
  // ... 使用 t("errors.emailExists") 等
}
```

### 6.4 最大化复用：四个角色共用 UI 块

| 复用单元 | 复用范围 | 实现方式 |
|---------|---------|---------|
| `PasswordInput` | LoginForm / RegisterForm / Settings 修改密码 | 独立组件 + `usePasswordVisibility` hook |
| `TwoFactorInput` | LoginForm / Settings 2FA 设置 / Settings 关闭 2FA | 独立组件 + `useTotpInput` hook |
| `EmailInput` | LoginForm / RegisterForm / 邀请成员 | 独立组件 |
| `OAuthButtons` | LoginForm / RegisterForm | 独立组件 + provider 配置数组 |
| `BrandPanel` | LoginForm / RegisterForm / 自定义 SSO 页 | 独立组件 + brand props |
| `AuthErrorBoundary` | 所有认证组件 | 复用 `shared/components/widget-boundary.tsx` |
| `useFormState` | 所有表单 | 复用 `shared/hooks/use-action-mutation.ts` |

### 6.5 错误与边界处理

#### 6.5.1 Error Boundary 包裹

```tsx
<AuthErrorBoundary fallback={<AuthErrorFallback />}>
  <LoginForm />
</AuthErrorBoundary>
```

#### 6.5.2 边界状态处理

- **空数据**：LoginForm 无 callbackUrl 时默认 `/dashboard`
- **无权限**：proxy.ts 重定向时 URL 携带 `?reason=forbidden`，目标页通过 `useSearchParams` 读取并 toast 提示
- **网络异常**：AuthService 抛 `NetworkError`，LoginForm 区分显示
- **账户锁定**：返回 `AccountLockedError` + `lockedUntil`，LoginForm 显示倒计时
- **速率限制**：返回 `RateLimitedError` + `retryAfterMs`，LoginForm 显示剩余时间

### 6.6 可测试性

#### 6.6.1 纯函数导出

```ts
// src/modules/auth/services/password-service.ts
export function validatePasswordStrength(password: string): PasswordStrengthResult { ... }
export async function hashPassword(password: string): Promise<string> { ... }
export async function verifyPassword(password: string, hash: string): Promise<boolean> { ... }
export async function checkBreachedPassword(password: string): Promise<boolean> { ... }
```

#### 6.6.2 单元测试

```
src/modules/auth/
├─ services/
│  ├─ password-service.test.ts
│  ├─ rate-limit-service.test.ts
│  └─ two-factor-service.test.ts
├─ data-access.test.ts
├─ schema.test.ts
└─ components/
   ├─ login-form.test.tsx
   └─ register-form.test.tsx
```

#### 6.6.3 e2e 测试修正

```ts
// tests/e2e/smoke-auth.spec.ts（重构后）
test("login page renders required controls", async ({ page }) => {
  await page.goto("/login")
  await expect(page.getByTestId("login-form")).toBeVisible()
  await expect(page.getByLabel(/auth.login.email/i)).toBeVisible()
  await expect(page.getByLabel(/auth.login.password/i)).toBeVisible()
  await expect(page.getByRole("button", { name: /auth.login.signIn/i })).toBeVisible()
})
```

### 6.7 可扩展性：配置驱动

#### 6.7.1 路由权限配置

```ts
// src/shared/lib/route-permissions.ts
export const ROUTE_PERMISSION_CONFIG = {
  routePrefix: {
    "/admin": Permissions.SCHOOL_MANAGE,
    "/teacher": Permissions.EXAM_CREATE,
    "/student": Permissions.HOMEWORK_SUBMIT,
    "/parent": Permissions.DASHBOARD_PARENT_READ,
    "/management": Permissions.GRADE_MANAGE,
  },
  dashboard: {
    "/admin/dashboard": Permissions.DASHBOARD_ADMIN_READ,
    "/teacher/dashboard": Permissions.DASHBOARD_TEACHER_READ,
    "/student/dashboard": Permissions.DASHBOARD_STUDENT_READ,
    "/parent/dashboard": Permissions.DASHBOARD_PARENT_READ,
  },
  specific: {
    "/admin/ai-settings": Permissions.AI_CHAT,
    "/admin/roles": Permissions.ROLE_READ,
    "/admin/permissions": Permissions.PERMISSION_READ,
    "/admin/announcements": Permissions.ANNOUNCEMENT_MANAGE,
    "/admin/audit-logs": Permissions.AUDIT_LOG_READ,
    // ... 可由 admin 端配置编辑
  },
  api: {
    "/api/ai/chat": Permissions.AI_CHAT,
  },
} as const
```

#### 6.7.2 onboarding 步骤配置

```ts
// src/modules/onboarding/config/onboarding-steps.ts
export const ONBOARDING_STEPS: OnboardingStepConfig[] = [
  { key: "roleConfirm", component: "StepRoleConfirm", required: true },
  { key: "basicInfo", component: "StepBasicInfo", required: true },
  { key: "roleInfo", component: "StepRoleInfo", required: false, skip: { roles: ["admin"] } },
  { key: "emailVerification", component: "StepEmailVerification", required: false, enabled: false }, // 未来扩展
  { key: "complete", component: "StepComplete", required: true },
]
```

### 6.8 企业级补充

#### 6.8.1 a11y

- 所有输入框 `aria-label` / `aria-describedby` 关联错误
- 错误 `<p role="alert">` 动态提示
- 键盘导航：Tab 序、可见焦点环、Enter 提交
- 显示密码按钮 Target Size ≥24×24
- Caps Lock 检测 + `aria-live="polite"` 提示
- 登录按钮加载态 `aria-busy="true"`

#### 6.8.2 性能

- LoginForm/RegisterForm 使用 `"use client"`，页面其余部分 Server Component
- AuthLayout 品牌 logo 使用 `next/image` 优先加载
- onboarding-stepper 拆分后按步骤 lazy load
- proxy.ts 路由权限判断保持在 middleware 层（不进入应用代码）

#### 6.8.3 安全性

- 注册接口：速率限制 + Zod + CAPTCHA + 邮箱验证 + 审计日志
- 登录接口：rate-limit + 账户锁定 + breached password 检测
- 2FA：TOTP + 备用码 + WebAuthn（P2）
- JWT 体积优化（P1-7）
- sessions 表清理或切换策略（P1-9）

#### 6.8.4 监控

- 接入 `trackEvent` 埋点（P1-11）
- admin/monitoring 页面展示核心指标（P2-7）
- Sentry 接入异常告警（P2-7）

---

## 七、实施计划

### 7.1 第一阶段（P0，立即实施）

1. **新建 `modules/auth/data-access.ts` 与 `actions.ts`**
   - 迁移 `register/page.tsx` 内联 `registerAction` 到 `modules/auth/actions.ts`
   - 新增 `createUser`、`getUserByEmail`、`assignRoleToUser` 到 `data-access.ts`
   - 删除页面内重复的 `calcAge` / `normalizeBcryptHash`，复用 `shared/lib`
   - 添加 `RegisterSchema` Zod 校验
   - 添加注册速率限制（独立 key `register:{ip}`）
   - 添加 `logLoginEvent({ action: "signup" })` 审计日志
   - 删除"自动创建 student 角色"逻辑
   - 错误消息不暴露 SQL 细节

2. **抽取 `modules/auth/services/two-factor-service.ts`**
   - 从 `modules/settings/actions-security.ts` 提取 `verifyTwoFactorForLogin` / `preflightTwoFactorAction`
   - 通过 `modules/settings/data-access-two-factor.ts` 访问 DB
   - `auth.ts` 改为 `await import("@/modules/auth/services/two-factor-service")`

3. **修复 onboarding 页面认证入口**
   - `app/(onboarding)/onboarding/page.tsx` 改用 `getAuthContext()` 替代 `auth()`

4. **删除废弃 OnboardingGate**
   - 删除 `src/shared/components/onboarding-gate.tsx`

5. **修复 `auth.ts` 中的 `as` 断言**
   - 添加 `isAuthUser` 类型守卫
   - 删除 `as typeof token.permissions` 等断言

6. **抽取 `shared/lib/route-resolver.ts`**
   - 合并 `proxy.ts#resolveDefaultPath` 与 `onboarding/data-access.ts#resolveDefaultPathByRoles`
   - 改用权限点优先 + 角色兜底

7. **修复 e2e 测试**
   - 重写 `tests/e2e/smoke-auth.spec.ts` 使用 i18n key 定位

### 7.2 第二阶段（P1，本周内）

1. **接入 i18n**
   - LoginForm / RegisterForm / AuthLayout / error.tsx / not-found.tsx 接入 `useTranslations`
   - onboarding actions.ts / data-access.ts 接入 `getTranslations`
   - 扩展 `auth.json` 翻译键

2. **拆分 onboarding-stepper**
   - 拆为 4 个步骤组件 + `useOnboardingForm` hook

3. **拆分 auth.ts authorize**
   - 拆为 5 个纯函数 + service

4. **添加 LoginForm 错误反馈细化**
   - 区分 error code
   - ARIA 关联
   - 显示密码按钮
   - Caps Lock 检测

5. **添加 (auth) 路由组 loading.tsx + Error Boundary**

6. **rate-limit 抽象接口**
   - 支持 memory / redis 双实现

7. **bindParentToChild 速率限制**

8. **接入 trackEvent**
   - 新增 auth 事件类型
   - auth.ts events 调用 trackEvent

### 7.3 第三阶段（P2，中长期）

1. users 表拆分
2. SSO/SAML 接入
3. 邀请码预分配机制
4. Breached Password 检测
5. 登录审计日志保留期清理任务
6. AuthLayout 品牌配置
7. 监控仪表盘
8. WebAuthn 支持

### 7.4 架构图同步

每完成一个 P0/P1 项，需同步更新 `docs/architecture/004_architecture_impact_map.md` 与 `005_architecture_data.json`：
- 新增/删除的导出函数
- 新增/删除的文件
- 修改的依赖关系
- 修改的权限点

---

## 八、验证清单

完成所有 P0/P1 项后，需通过以下验证：

- [ ] `npm run lint` 零错误
- [ ] `npx tsc --noEmit` 零错误
- [ ] `npm run test`（新增的单元测试）全部通过
- [ ] `npm run e2e`（修正后的 smoke-auth）通过
- [ ] 手动验证：注册 → 登录 → onboarding → 角色切换 → 登出 全流程
- [ ] 手动验证：账户锁定、速率限制、2FA 启用/关闭
- [ ] 手动验证：中英文 locale 切换
- [ ] 手动验证：a11y 键盘导航、屏幕阅读器
- [ ] 架构图 004/005 与代码一致

---

## 九、风险与回滚

| 风险 | 影响 | 缓解措施 |
|------|------|---------|
| registerAction 迁移可能改变错误消息格式 | 前端 toast 显示异常 | 保持 `ActionState` 结构不变；e2e 覆盖 |
| auth.ts 2FA 抽取可能破坏登录链路 | 用户无法登录 | 保留旧 import 路径作为 fallback；分阶段灰度 |
| onboarding 改用 getAuthContext 可能改变未登录行为 | 未登录用户访问 /onboarding 行为变化 | 添加 e2e 覆盖未登录场景 |
| rate-limit 改为 Redis 可能引入网络延迟 | 登录响应变慢 | 内存实现作为默认；Redis 为可选 |
| JWT 体积优化可能改变权限刷新时机 | 权限撤销延迟变化 | 评估方案 A vs B 后再实施 |
