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

16 KiB
Raw Blame History

首次登录引导Onboarding重大问题讨论

创建日期2026-06-18 状态:讨论中,待决策 关联架构图:docs/architecture/004_architecture_impact_map.md §2.1 shared 层 / §3 已知问题 P2-4 关联代码:


一、背景与定位

按项目规则"先图后码",先从架构影响地图定位 Onboarding 相关节点:

  • shared 层components/onboarding-gate.tsx312 行)已被架构图标记为 ⚠️ P2-4「业务逻辑泄漏到 shared」
  • app 层/api/onboarding/status/api/onboarding/complete 两条路由
  • 数据层users.onboardedAtsrc/shared/db/schema.ts:41
  • 被调用模块modules/classes/data-access.tsenrollStudentByInvitationCode

当前 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 行)——用权限点反推角色:

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
  • 问题Step 0 允许任意登录用户从下拉框选择 student / teacher / parent 角色;complete/route.ts:32-35 直接信任前端 body.role 并写入 usersToRoles
  • 后果:任何注册用户可自封为 teacher从而获得 exam:createhomework:grade 等权限;可自封为 parent 查看他人成绩。这是 K12 教务系统的合规红线
  • 违反规则项目规则「Server Action 必须使用 requirePermission()」、K12 行业铁律「角色由管理员预分配」。

P0-2 教师可绑定任意班级+科目

  • 位置complete/route.ts:95-130
  • 问题:教师通过 classCodes6 位邀请码)可把自己写入任意班级的 classSubjectTeachers,且 teacherSubjects 由前端任意提交,服务端仅做"名称存在性"校验,不校验该教师是否被管理员分配到该班。
  • 后果:教师可越权查看任意班级学生名单、成绩;可篡改他人班级的任课关系。
  • 违反规则项目规则「modules 之间通过对方 data-access 通信,不直接查询对方 DB 表」——此处 app 层 API 直接 insert classSubjectTeachers

P0-3 无权限校验、无 Zod、无事务

  • 位置complete/route.ts 整文件
  • 问题
    • 仅检查 auth() 登录态,未调用 requirePermission()
    • String(body.role ?? "") 手动解析,无 Zod(架构图 005 声称"validation: Zod schema"与实际不符)
    • 5 次独立 DB 写入update users / insert usersToRoles / enrollStudent / insert classSubjectTeachers / update onboardedAtdb.transaction()
    • 运行时 db.insert(roles).values({ name: role }) 创建角色记录(第 66-68 行)——角色应在 seed 时创建,运行时创建属异常路径
  • 后果:中途失败导致数据不一致(如已绑定角色但 onboardedAt 仍为 null用户被反复弹窗越权写入。

🟠 P1 级:架构违规

P1-1 shared 层反向承载领域逻辑

  • 位置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
  • 问题app/api/onboarding/complete/route.ts 直接 import 并写入 classesclassSubjectTeacherssubjects 表,绕过 modules/classes 的 data-access 与权限校验。
  • 违反规则项目规则「app 只能调用 modules 的 Server Actions 和 data-access不直接访问 DB」「modules 之间通过对方 data-access 通信」。

P1-3 角色推断双源不一致

  • 位置status/route.ts:29-41 vs onboarding-gate.tsx:90-94
  • 问题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.tsauth() 读取 sessionuser.onboardedAt 为空且路径不在白名单(/login/api/auth/onboarding、静态资源),则 NextResponse.redirect(new URL('/onboarding', req.url))
  • 结论:客户端 Dialog 仅适合"非阻塞的偏好补全"(如头像、通知偏好);强制 onboarding 应等同未登录处理。

4.2 商业方案Clerk / Supabase / Auth0共性

三段式:metadata 标记 + 强制重定向独立路由 + 服务端 Action 校验

  • 角色等敏感字段放服务端可写的 metadataClerk privateMetadata / Auth0 appMetadata / Supabase RLS-protected profiles.role禁止前端自写
  • onboarding 完成回调必须由服务端 Action 写入 metadata前端不能直接改。
  • 未完成 onboarding 时 middleware/Action 层强制重定向。

4.3 shadcn/ui 生态

  • 官方无内置 Stepperexamples/formsblocks 范式明确:独立路由页面 + <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-userdata-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. 事务化completeOnboardingActiondb.transaction() 包裹所有写入。
  10. Zod 校验:定义 onboardingSchemaphone 用 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 🔴 P0
信任前端 role 写入 complete/route.ts:32-35 🔴 P0
教师绑任意班级 complete/route.ts:95-130 🔴 P0
无权限校验/Zod/事务 complete/route.ts 整文件 🔴 P0
shared 反向承载领域逻辑 onboarding-gate.tsx 整文件 🟠 P1
app 层跨模块写表 complete/route.ts:6 🟠 P1
角色推断双源不一致 status/route.ts:29-41 vs onboarding-gate.tsx:90-94 🟠 P1
全局 Dialog 缺陷 app/layout.tsx:41 🟡 P2
表单校验粗糙 onboarding-gate.tsx:88 🟡 P2