1095 lines
66 KiB
Markdown
1095 lines
66 KiB
Markdown
# 认证模块审计报告
|
||
|
||
> 审计日期: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">
|
||
“This platform has completely transformed how we deliver education to our students. The attention to detail and performance is unmatched.”
|
||
</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 后再实施 |
|