refactor: fix all P0/P1/P2 bugs and architecture issues
Bug fixes (from bugs/ directory): - Fix cross-module DB queries in 9 modules (homework, grades, parent, diagnostic, elective, proctoring, notifications, scheduling, classes) by routing through data-access functions - Fix shared/lib <-> auth circular dependency via new session.ts module - Fix divide-by-zero guard in grades data-access - Fix audit export data truncation (paginated fetch for full datasets) - Fix missing transactions in homework grading and elective lottery - Fix missing revalidatePath in course-plans actions - Fix frontend permission checks using requirePermission instead of requireAuth - Fix dashboard role routing using session.user.roles - Fix student auth pattern (migrate getDemoStudentUser to users module) - Fix ActionState return type handling in components Code quality fixes: - Remove 60+ as type assertions (replace with type guards) - Remove non-null assertions (use optional chaining or explicit checks) - Convert dynamic imports to static imports (grades, diagnostic) - Add React.cache() wrapping for read functions - Parallelize independent queries with Promise.all - Add explicit return types to 30+ arrow functions - Replace any with unknown + type guards - Fix import type for type-only imports - Add Zod validation schemas for classes and diagnostic modules - Extract duplicate code (normalizeRoleName, normalizeBcryptHash, logger IP extraction) - Add console.error to silent catch blocks - Fix permission naming consistency (exam:proctor_read -> exam:proctor:read) Architecture doc sync: - Update 004_architecture_impact_map.md and 005_architecture_data.json - Update management-modules-audit.md for P0-7 cross-module fix Moved deleted proctoring event route to deletes/ folder.
This commit is contained in:
258
bugs/001_first_login_onboarding.md
Normal file
258
bugs/001_first_login_onboarding.md
Normal file
@@ -0,0 +1,258 @@
|
||||
# 首次登录引导(Onboarding)重大问题讨论
|
||||
|
||||
> 创建日期:2026-06-18
|
||||
> 状态:**讨论中,待决策**
|
||||
> 关联架构图:`docs/architecture/004_architecture_impact_map.md` §2.1 shared 层 / §3 已知问题 P2-4
|
||||
> 关联代码:
|
||||
> - [src/shared/components/onboarding-gate.tsx](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx)(312 行)
|
||||
> - [src/app/api/onboarding/status/route.ts](file:///e:/Desktop/CICD/src/app/api/onboarding/status/route.ts)
|
||||
> - [src/app/api/onboarding/complete/route.ts](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts)
|
||||
> - [src/app/layout.tsx](file:///e:/Desktop/CICD/src/app/layout.tsx#L41)(全局挂载点)
|
||||
|
||||
---
|
||||
|
||||
## 一、背景与定位
|
||||
|
||||
按项目规则"先图后码",先从架构影响地图定位 Onboarding 相关节点:
|
||||
|
||||
- **shared 层**:`components/onboarding-gate.tsx`(312 行)已被架构图标记为 ⚠️ P2-4「业务逻辑泄漏到 shared」
|
||||
- **app 层**:`/api/onboarding/status`、`/api/onboarding/complete` 两条路由
|
||||
- **数据层**:`users.onboardedAt`([src/shared/db/schema.ts:41](file:///e:/Desktop/CICD/src/shared/db/schema.ts#L41))
|
||||
- **被调用模块**:`modules/classes/data-access.ts` 的 `enrollStudentByInvitationCode`
|
||||
|
||||
当前 Onboarding 是一个**全局 Dialog**:在 `app/layout.tsx` 第 41 行无条件挂载 `<OnboardingGate />`,组件内通过 `useEffect` 拉取 `/api/onboarding/status`,若 `required === true` 则弹出不可关闭的 4 步 Dialog。
|
||||
|
||||
---
|
||||
|
||||
## 二、现状代码盘点
|
||||
|
||||
### 2.1 组件层(onboarding-gate.tsx)
|
||||
|
||||
| 步骤 | 标题 | 采集字段 | 备注 |
|
||||
|------|------|----------|------|
|
||||
| Step 0 | 角色选择 | role(student/teacher/parent) | admin 只读展示;其他角色用户可下拉**自选** |
|
||||
| Step 1 | 通用信息 | name / phone / address | 仅校验非空 |
|
||||
| Step 2 | 角色信息 | classCodes(学生/教师)、teacherSubjects(教师) | 可跳过;家长显示"暂不需要配置" |
|
||||
| Step 3 | 完成 | — | 调 `/api/onboarding/complete` 后跳 `/dashboard` |
|
||||
|
||||
**角色推断逻辑**(第 90-94 行)——用权限点反推角色:
|
||||
|
||||
```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)
|
||||
```
|
||||
|
||||
### 2.2 API 层
|
||||
|
||||
- `GET /api/onboarding/status`:查 `users.onboardedAt` 是否为空 + 查 `usersToRoles` 推断角色
|
||||
- `POST /api/onboarding/complete`:更新 users 表 → 写 usersToRoles → 学生调 `enrollStudentByInvitationCode` → 教师直接 insert `classSubjectTeachers` → 写 `onboardedAt`
|
||||
|
||||
---
|
||||
|
||||
## 三、重大问题清单(按风险分级)
|
||||
|
||||
### 🔴 P0 级:安全/合规/越权
|
||||
|
||||
#### P0-1 用户可自选角色(严重越权)
|
||||
- **位置**:[onboarding-gate.tsx:192-201](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx#L192-L201)
|
||||
- **问题**:Step 0 允许任意登录用户从下拉框选择 `student / teacher / parent` 角色;`complete/route.ts:32-35` 直接信任前端 `body.role` 并写入 `usersToRoles`。
|
||||
- **后果**:任何注册用户可自封为 teacher,从而获得 `exam:create`、`homework:grade` 等权限;可自封为 parent 查看他人成绩。**这是 K12 教务系统的合规红线**。
|
||||
- **违反规则**:项目规则「Server Action 必须使用 `requirePermission()`」、K12 行业铁律「角色由管理员预分配」。
|
||||
|
||||
#### P0-2 教师可绑定任意班级+科目
|
||||
- **位置**:[complete/route.ts:95-130](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts#L95-L130)
|
||||
- **问题**:教师通过 `classCodes`(6 位邀请码)可把自己写入任意班级的 `classSubjectTeachers`,且 `teacherSubjects` 由前端任意提交,服务端仅做"名称存在性"校验,不校验该教师是否被管理员分配到该班。
|
||||
- **后果**:教师可越权查看任意班级学生名单、成绩;可篡改他人班级的任课关系。
|
||||
- **违反规则**:项目规则「modules 之间通过对方 data-access 通信,不直接查询对方 DB 表」——此处 app 层 API 直接 insert `classSubjectTeachers`。
|
||||
|
||||
#### P0-3 无权限校验、无 Zod、无事务
|
||||
- **位置**:[complete/route.ts](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts) 整文件
|
||||
- **问题**:
|
||||
- 仅检查 `auth()` 登录态,**未调用 `requirePermission()`**
|
||||
- 用 `String(body.role ?? "")` 手动解析,**无 Zod**(架构图 005 声称"validation: Zod schema"与实际不符)
|
||||
- 5 次独立 DB 写入(update users / insert usersToRoles / enrollStudent / insert classSubjectTeachers / update onboardedAt)**无 `db.transaction()`**
|
||||
- 运行时 `db.insert(roles).values({ name: role })` 创建角色记录(第 66-68 行)——角色应在 seed 时创建,运行时创建属异常路径
|
||||
- **后果**:中途失败导致数据不一致(如已绑定角色但 `onboardedAt` 仍为 null,用户被反复弹窗);越权写入。
|
||||
|
||||
### 🟠 P1 级:架构违规
|
||||
|
||||
#### P1-1 shared 层反向承载领域逻辑
|
||||
- **位置**:[onboarding-gate.tsx](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx) 整文件
|
||||
- **问题**:组件位于 `shared/components/`,但包含角色判断、班级代码、教师科目配置等强领域逻辑,并通过 fetch 调用业务 API。
|
||||
- **违反规则**:项目规则「shared 不得反向依赖 @/auth、@/proxy 或任何 modules/*」「shared 是被依赖方」。
|
||||
- **架构图标记**:004 文档 §2.1 已标记 P2-4。
|
||||
|
||||
#### P1-2 app 层 API 直接跨模块写表
|
||||
- **位置**:[complete/route.ts:6](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts#L6)
|
||||
- **问题**:`app/api/onboarding/complete/route.ts` 直接 import 并写入 `classes`、`classSubjectTeachers`、`subjects` 表,绕过 `modules/classes` 的 data-access 与权限校验。
|
||||
- **违反规则**:项目规则「app 只能调用 modules 的 Server Actions 和 data-access,不直接访问 DB」「modules 之间通过对方 data-access 通信」。
|
||||
|
||||
#### P1-3 角色推断双源不一致
|
||||
- **位置**:[status/route.ts:29-41](file:///e:/Desktop/CICD/src/app/api/onboarding/status/route.ts#L29-L41) vs [onboarding-gate.tsx:90-94](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx#L90-L94)
|
||||
- **问题**:status API 用 `roles.name` 推断角色(含 `grade_head/teaching_head → teacher` 归一化),组件又用权限点重新推断,两套逻辑可能不一致(如年级组长既有 EXAM_CREATE 又有其他权限,组件推断可能错位)。
|
||||
|
||||
### 🟡 P2 级:用户体验与可访问性
|
||||
|
||||
#### P2-1 全局 Dialog 模式缺陷
|
||||
- **问题**:
|
||||
- Dialog 不可关闭(`canClose = !required`),用户被强制锁定
|
||||
- 刷新页面丢失步骤状态(step 重置为 0)
|
||||
- 无独立 URL,无法分享/书签
|
||||
- 首屏无骨架屏,`useEffect` 拉取 status 期间会闪烁
|
||||
- 依赖 `session?.user?.name` 触发重复请求
|
||||
- **对比**:业界主流(Auth.js 官方、Clerk、Vercel 模板)均采用独立路由 `/onboarding` + middleware 重定向。
|
||||
|
||||
#### P2-2 表单校验粗糙
|
||||
- **问题**:电话仅校验非空(无手机号格式校验);姓名无长度限制;地址无长度限制;班级代码无格式预校验。
|
||||
|
||||
#### P2-3 国际化与可访问性
|
||||
- **问题**:中英文混合("Role"、"Select role" 英文,其余中文);Dialog 缺少 `aria-describedby`;进度条无 `aria-valuenow`;表单无 `required` 标记。
|
||||
|
||||
#### P2-4 进度条与步骤不一致
|
||||
- **问题**:admin 跳过 Step 2,但进度条仍渲染 4 段,视觉上 Step 2 永远亮起,造成困惑。
|
||||
|
||||
---
|
||||
|
||||
## 四、业界大仓(Monorepo)解决方案引用
|
||||
|
||||
### 4.1 Auth.js v5 官方推荐
|
||||
|
||||
- **状态标记**:`users.onboardedAt` 字段 + `jwt`/`session` 回调注入 session;完成时调 `update()` 刷新 token。
|
||||
- **强制方式**:**middleware 重定向**到独立 `/onboarding` 路由,而非客户端 Dialog。
|
||||
- 在 `middleware.ts` 用 `auth()` 读取 session,若 `user.onboardedAt` 为空且路径不在白名单(`/login`、`/api/auth`、`/onboarding`、静态资源),则 `NextResponse.redirect(new URL('/onboarding', req.url))`。
|
||||
- **结论**:客户端 Dialog 仅适合"非阻塞的偏好补全"(如头像、通知偏好);强制 onboarding 应等同未登录处理。
|
||||
|
||||
### 4.2 商业方案(Clerk / Supabase / Auth0)共性
|
||||
|
||||
三段式:**metadata 标记 + 强制重定向独立路由 + 服务端 Action 校验**。
|
||||
|
||||
- **角色等敏感字段放服务端可写的 metadata**(Clerk `privateMetadata` / Auth0 `appMetadata` / Supabase RLS-protected `profiles.role`),**禁止前端自写**。
|
||||
- onboarding 完成回调必须由服务端 Action 写入 metadata,前端不能直接改。
|
||||
- 未完成 onboarding 时 middleware/Action 层强制重定向。
|
||||
|
||||
### 4.3 shadcn/ui 生态
|
||||
|
||||
- 官方无内置 Stepper,但 `examples/forms` 与 `blocks` 范式明确:**独立路由页面 + `<Form>`(react-hook-form + zod)+ 父组件持 step state**。
|
||||
- 每步独立 zod schema 做渐进式校验,最后一步汇总写入。
|
||||
- 官方 `blocks/login-04` 等登录块均采用独立路由页面,而非全局 Dialog。
|
||||
|
||||
### 4.4 企业级 K12 教务系统(PowerSchool / Veracross / 国内智慧校园)
|
||||
|
||||
**铁律:角色由管理员预分配,用户不可自选。**
|
||||
|
||||
| 角色 | 首次登录采集字段 | 角色来源 |
|
||||
|------|------------------|----------|
|
||||
| 学生 | 学号(预分配不可改)、姓名、性别、出生日期、家长联系方式、紧急联系人 | 管理员批量导入 |
|
||||
| 教师 | 工号(预分配)、姓名、所教科目、任教班级、办公室、联系电话、学历资质 | 教务处预分配 |
|
||||
| 家长 | 与学生关系、学生学号(通过学校发放的 **Access ID + Access Password** 绑定)、本人姓名、电话、邮箱 | 学校发放凭证,家长绑定子女 |
|
||||
| 管理员 | 工号、姓名、职务、管理范围 | 学校 IT 创建 |
|
||||
|
||||
**原因**:
|
||||
1. **合规**:K12 数据受《个人信息保护法》《未成年人保护法》约束,学生身份必须由学校权威确认。
|
||||
2. **安全**:允许自选教师角色 = 任何人可创建考试、查看全班成绩。
|
||||
3. **数据一致性**:班级、学号、任课关系是教务核心数据,必须由教务处维护。
|
||||
|
||||
### 4.5 Monorepo(turborepo / nx)惯例
|
||||
|
||||
- **turborepo 官方模板**:跨模块"流程型"功能(onboarding、setup-wizard)作为**独立 module**,而非塞进 shared。
|
||||
- **nx feature-shell 模式**:onboarding 作为 `feature-onboarding` library,依赖 `data-access-user`、`data-access-class`。
|
||||
- **Vercel 自家项目**:`app/(app)/onboarding/[[...step]]/page.tsx` 路由组 + `modules/onboarding/` 模块。
|
||||
|
||||
---
|
||||
|
||||
## 五、重构方案建议(待讨论)
|
||||
|
||||
### 5.1 目标架构
|
||||
|
||||
```
|
||||
app/
|
||||
├─ (auth)/login/ # 登录页(middleware 白名单)
|
||||
├─ (onboarding)/onboarding/ # 新增独立路由
|
||||
│ └─ page.tsx # 服务端组件,读取 session.onboarded 决定渲染
|
||||
└─ middleware.ts # 新增/增强:未 onboarded 时重定向
|
||||
|
||||
modules/onboarding/ # 新建模块
|
||||
├─ actions.ts # completeOnboardingAction(Server Action + requirePermission)
|
||||
├─ data-access.ts # 仅操作 users.onboardedAt
|
||||
├─ schema.ts # Zod:name/phone/address/classCodes
|
||||
├─ types.ts
|
||||
└─ components/
|
||||
├─ OnboardingStepper.tsx # 客户端 stepper 容器
|
||||
├─ RoleConfirmStep.tsx # 只读展示管理员分配的角色
|
||||
├─ ProfileStep.tsx # 姓名/电话/住址
|
||||
└─ BindingStep.tsx # 学生:确认班级;教师:确认任课;家长:绑定子女
|
||||
|
||||
shared/
|
||||
└─ components/onboarding-gate.tsx # 删除
|
||||
```
|
||||
|
||||
### 5.2 关键改动点
|
||||
|
||||
1. **删除 `shared/components/onboarding-gate.tsx`**,从 `app/layout.tsx` 移除挂载。
|
||||
2. **新建 `modules/onboarding/`**,承载所有领域逻辑。
|
||||
3. **新建 `app/(onboarding)/onboarding/page.tsx`** 独立路由。
|
||||
4. **增强 `middleware.ts`**:读取 session.onboarded,未完成且非白名单路径 → 重定向到 `/onboarding`。
|
||||
5. **Auth.js 回调**:在 `jwt`/`session` 回调注入 `onboardedAt`,供 middleware 读取。
|
||||
6. **删除 `app/api/onboarding/*/route.ts`**,改为 `modules/onboarding/actions.ts` 的 Server Action。
|
||||
7. **角色只读化**:Step 0 改为"角色确认"——只读展示 `usersToRoles` 中的角色,用户不可改。
|
||||
8. **班级绑定改造**:
|
||||
- 学生:仅"确认"管理员预分配的班级,或输入邀请码(服务端校验有效性 + 用途)
|
||||
- 教师:仅"确认"管理员预分配的任课关系,**移除自填班级代码**
|
||||
- 家长:输入"子女学号 + 绑定码"绑定子女(参考 PowerSchool Access ID 模式)
|
||||
9. **事务化**:`completeOnboardingAction` 用 `db.transaction()` 包裹所有写入。
|
||||
10. **Zod 校验**:定义 `onboardingSchema`,phone 用 `z.string().regex(/^1\d{10}$/)`。
|
||||
|
||||
### 5.3 迁移兼容
|
||||
|
||||
- 已 onboarded 用户(`onboardedAt` 非空)不受影响,middleware 直接放行。
|
||||
- 未 onboarded 用户下次登录会被重定向到 `/onboarding`(而非弹 Dialog)。
|
||||
- 无需数据迁移,`users.onboardedAt` 字段保留。
|
||||
|
||||
---
|
||||
|
||||
## 六、待决策的开放问题
|
||||
|
||||
请就以下问题给出决策,以便进入实施阶段:
|
||||
|
||||
### Q1:角色分配策略
|
||||
- **方案 A**(推荐,符合 K12 铁律):onboarding 中角色完全只读,由管理员通过后台预分配;用户无法在 onboarding 中改变角色。
|
||||
- **方案 B**:保留角色选择,但服务端校验"用户已有该角色"才允许选择(即只能从已有角色中选一个主角色)。
|
||||
- **方案 C**:暂不改动角色选择,仅修复其他问题。
|
||||
|
||||
### Q2:教师任课关系绑定
|
||||
- **方案 A**(推荐):onboarding 中教师**仅确认**管理员预分配的任课关系,不自填班级代码。
|
||||
- **方案 B**:保留自填邀请码,但服务端强校验邀请码用途(teacher-assign)、有效期、使用次数。
|
||||
- **方案 C**:完全移除 onboarding 中的班级绑定,统一由管理员后台处理。
|
||||
|
||||
### Q3:家长绑定子女方式
|
||||
- **方案 A**(推荐,PowerSchool 模式):家长输入"子女学号 + 学校发放的 6 位绑定码"。
|
||||
- **方案 B**:家长输入"子女学号 + 子女生日"作为验证。
|
||||
- **方案 C**:暂不实现家长绑定,由管理员后台预绑定。
|
||||
|
||||
### Q4:onboarding 路由形态
|
||||
- **方案 A**(推荐):单页 `/onboarding` + 客户端 stepper(步骤状态用 query param 持久化)。
|
||||
- **方案 B**:嵌套路由 `/onboarding/role`、`/onboarding/profile`、`/onboarding/binding`(每步独立 Server Action)。
|
||||
- **方案 C**:保留全局 Dialog,仅修复安全与架构问题。
|
||||
|
||||
### Q5:实施范围
|
||||
- **方案 A**:一次性完成 P0 + P1 + P2 全部整改。
|
||||
- **方案 B**:先做 P0(安全/越权)+ P1(架构),P2(UX)后续迭代。
|
||||
- **方案 C**:仅做 P0 紧急修复,P1/P2 列入 backlog。
|
||||
|
||||
---
|
||||
|
||||
## 七、附录:问题与代码位置速查
|
||||
|
||||
| 问题 | 代码位置 | 风险 |
|
||||
|------|----------|------|
|
||||
| 用户自选角色 | [onboarding-gate.tsx:192-201](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx#L192-L201) | 🔴 P0 |
|
||||
| 信任前端 role 写入 | [complete/route.ts:32-35](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts#L32-L35) | 🔴 P0 |
|
||||
| 教师绑任意班级 | [complete/route.ts:95-130](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts#L95-L130) | 🔴 P0 |
|
||||
| 无权限校验/Zod/事务 | [complete/route.ts](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts) 整文件 | 🔴 P0 |
|
||||
| shared 反向承载领域逻辑 | [onboarding-gate.tsx](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx) 整文件 | 🟠 P1 |
|
||||
| app 层跨模块写表 | [complete/route.ts:6](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts#L6) | 🟠 P1 |
|
||||
| 角色推断双源不一致 | [status/route.ts:29-41](file:///e:/Desktop/CICD/src/app/api/onboarding/status/route.ts#L29-L41) vs [onboarding-gate.tsx:90-94](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx#L90-L94) | 🟠 P1 |
|
||||
| 全局 Dialog 缺陷 | [app/layout.tsx:41](file:///e:/Desktop/CICD/src/app/layout.tsx#L41) | 🟡 P2 |
|
||||
| 表单校验粗糙 | [onboarding-gate.tsx:88](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx#L88) | 🟡 P2 |
|
||||
Reference in New Issue
Block a user