Files
NextEdu/bugs/001_first_login_onboarding.md
SpecialX 49291fcc31 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.
2026-06-19 05:13:34 +08:00

259 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 首次登录引导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 | 角色选择 | rolestudent/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 Monorepoturborepo / 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 # completeOnboardingActionServer Action + requirePermission
├─ data-access.ts # 仅操作 users.onboardedAt
├─ schema.ts # Zodname/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**:暂不实现家长绑定,由管理员后台预绑定。
### Q4onboarding 路由形态
- **方案 A**(推荐):单页 `/onboarding` + 客户端 stepper步骤状态用 query param 持久化)。
- **方案 B**:嵌套路由 `/onboarding/role``/onboarding/profile``/onboarding/binding`(每步独立 Server Action
- **方案 C**:保留全局 Dialog仅修复安全与架构问题。
### Q5实施范围
- **方案 A**:一次性完成 P0 + P1 + P2 全部整改。
- **方案 B**:先做 P0安全/越权)+ P1架构P2UX后续迭代。
- **方案 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 |