Files
NextEdu/docs/architecture/audit/archive/permissions-audit-report.md

44 KiB
Raw Blame History

用户权限模块审计报告

审计范围用户权限RBAC模块包括 shared/types/permissions.tsshared/lib/permissions.tsshared/lib/auth-guard.tsshared/hooks/use-permission.tsshared/lib/role-utils.tsshared/lib/session.tsauth.tsmodules/rbac/*modules/users/*(权限相关部分)、app/(dashboard)/admin/{roles,permissions,users}/*。 审计时间2026-06-24 审计依据:项目规则 .trae/rules/project_rules.md、架构影响地图 004/005


一、现有实现概要

1.1 文件分布与行数

文件 行数 职责
shared/types permissions.ts 223 67 个权限点常量 + Role/BuiltinRole/Permission/DataScope/AuthContext 类型
shared/lib permissions.ts 301 ROLE_PERMISSIONS_SEED6 内置角色种子)+ resolvePermissions()DB 查询 + 种子兜底)
shared/lib auth-guard.ts 176 getAuthContext() / requirePermission() / checkPermission() / resolveDataScope() / requireAuth()
shared/lib role-utils.ts 34 normalizeRole() / resolvePrimaryRole() 纯函数
shared/lib session.ts 35 getSession()(动态 import @/auth 避免循环依赖)
shared/hooks use-permission.ts 53 客户端 usePermission() HookhasPermission/hasAnyPermission/hasAllPermissions/hasRole
app/root auth.ts 234 NextAuth v5 配置Credentials + JWT + Session 回调 + 登录事件)
modules/rbac data-access.ts 215 角色 CRUD + getRolePermissions/setRolePermissions + ADMIN_ROLE_NAME 常量
modules/rbac data-access-assignments.ts 161 用户-角色分配(getUserRoleNames/assignRolesToUser/getUserRoleAssignments
modules/rbac actions.ts 382 8 个 Server ActionrequirePermission + Zod + 审计日志)
modules/rbac schema.ts 42 4 个 Zod schema
modules/rbac types.ts 56 RoleRecord/RoleWithStats/RoleDetail/CreateRoleInput/UpdateRoleInput/UserRoleAssignment/PaginatedResult
modules/rbac/lib permission-catalog.ts 268 PERMISSION_CATALOG24 分组)+ getAllPermissionMetas/getPermissionMeta
modules/rbac/components 7 个文件 ~600 RoleList/RoleFormDialog/RolePermissionMatrix/PermissionCatalogView/UserRoleAssignDialog/RoleManagementView/RoleDetailEditButton
modules/users data-access.ts 424 用户查询 + getCurrentStudentUser + getAdminUsers/getAdminUserRoles
modules/users actions.ts 218 updateUserProfile/importUsersAction/exportUsersAction/updateUserRoleAction(空实现)/deleteUserAction
app/(dashboard)/admin 4 个 page.tsx ~200 角色列表/角色详情/权限目录/用户管理页面

1.2 数据流

登录 (auth.ts authorize)
  └─▶ db.query.users → 校验密码 → 查询 usersToRoles → resolvePrimaryRole
  └─▶ JWT callback: resolvePermissions(allRoles) → 写入 token.permissions
  └─▶ Session callback: 透传到 session.user.permissions

请求 (Server Component / Server Action)
  └─▶ getAuthContext() → getSession() → resolveDataScope(userId, roles) → AuthContext
  └─▶ requirePermission(p) → 校验 ctx.permissions.includes(p)

客户端 (Client Component)
  └─▶ usePermission() → useSession() → hasPermission(p)

1.3 架构图记录情况

架构影响地图 004 和数据 JSON 005 已记录 rbac 模块JSON 第 16204-16603 行),包含:

  • 8 个 actions、11 个 data-access 函数、3 个 lib 常量/函数、8 个组件
  • 依赖关系:shared/dbshared/types/permissionsshared/lib/auth-guardshared/lib/audit-loggershared/lib/change-logger
  • 被使用方:admin/rolesadmin/permissionsadmin/usersmodules/users
  • i18n 命名空间 rbac,文件 zh-CN/rbac.json + en/rbac.json

架构图遗漏

  • shared/lib/permissions.tsresolvePermissions)未在 rbac 模块依赖中显式列出(实际被 auth.ts 使用)
  • shared/lib/auth-guard.tsresolveDataScope 内部直接查询 classes/classEnrollments/classSubjectTeachers/grades/parentStudentRelations 表,属于跨模块数据访问,未在架构图中标注
  • modules/users/data-access.tsgetCurrentStudentUser 直接调用 auth() 而非 getAuthContext(),未在架构图中标注此异常依赖
  • app/(dashboard)/admin/permissions/page.tsx 直接查询 rolePermissions/roles 表,未走 data-access架构图未标注此违规
  • modules/users/actions.tsdeleteUserAction 直接 db.delete(users),未走 data-access架构图未标注
  • app/(dashboard)/admin/rolesadmin/permissionsadmin/users(非 import 子目录)缺少 loading.tsx/error.tsx,架构图未标注

二、现存问题与原因分析

2.1 架构分层违规P0

问题 2.1.1 — app/ 直接访问数据库

  • 位置admin/permissions/page.tsx 第 28-32 行
  • 问题:页面组件直接 db.select(...).from(rolePermissions).innerJoin(roles, ...) 查询数据库,绕过 data-access 层。
  • 违反规则app/ 只能调用 modules/ 的 Server Actions 和 data-access不直接访问数据库(项目规则「架构分层规则」)。
  • 后果:权限统计查询逻辑散落在页面中,无法复用、无法测试、无法统一加缓存;若 rolePermissions 表结构变更需修改多处。

问题 2.1.2 — Server Action 直接访问数据库

  • 位置users/actions.ts 第 208 行 await db.delete(users).where(eq(users.id, userId))
  • 问题deleteUserAction 直接在 actions 层执行 DB 删除,未下沉到 data-access。
  • 违反规则app/ 只能调用 modules/ 的 Server Actions 和 data-access + 严格三层架构,依赖方向单向(项目规则「架构分层规则」)。
  • 后果:删除用户逻辑(应包含级联清理 sessions、usersToRoles、passwordSecurity 等)散落在 actions 层,易遗漏级联清理,造成孤儿数据。

问题 2.1.3 — 客户端组件直接调用 fetch API

  • 位置admin-users-view.tsx 第 120 行 fetch("/api/admin/users/" + deleteUserId, { method: "DELETE" })
  • 问题:客户端组件通过 fetch 调用 API 路由删除用户,而非调用 Server Action。
  • 违反规则app/ 只能调用 modules/ 的 Server Actions 和 data-access(项目规则「架构分层规则」)。
  • 后果:绕过 Server Action 的权限校验、Zod 验证、审计日志存在未授权调用风险删除逻辑出现两套Server Action + API 路由)。

问题 2.1.4 — getCurrentStudentUser 使用 auth() 而非 getAuthContext()

  • 位置users/data-access.ts 第 232 行 const session = await auth()
  • 问题:直接调用 auth() 获取 session而非使用项目统一的 getAuthContext()
  • 违反规则Authentication must use getAuthContext() and getCurrentStudentUser(); avoid mixing with other auth methods(项目记忆 Hard Constraints
  • 后果:认证入口不统一,getAuthContext 提供的 dataScope 等上下文信息丢失;后续若在 session 校验逻辑中增加 IP 限制、租户隔离等,此处不会自动生效。

问题 2.1.5 — resolveDataScope 跨模块直接查询数据库表

  • 位置auth-guard.ts 第 70-163 行
  • 问题resolveDataScope 直接查询 classesclassEnrollmentsclassSubjectTeachersgradesparentStudentRelations 表,这些表属于 schoolclassesparent 等模块。
  • 违反规则模块间只能通过对方 data-access 通信,禁止跨模块直接查询数据库表(项目规则「架构分层规则」)。
  • 后果shared/lib 反向依赖业务模块的表结构;若 classes 模块重构表结构,auth-guard 会编译报错;shared/ 不得反向依赖业务模块的硬约束被破坏。

2.2 权限校验缺失或硬编码P0/P1

问题 2.2.1 — updateUserRoleAction 是空实现

  • 位置users/actions.ts 第 177-196 行
  • 问题updateUserRoleAction 接收 userIdrole 参数后,仅 void userId; void role; 直接返回成功,未执行任何实际操作。
  • 违反规则所有 Server Action 必须调用 requirePermission() 进行权限校验(虽已调用 requirePermission,但逻辑为空)+ 功能完整性。
  • 后果:管理员在用户管理页点击「编辑」试图修改角色时,系统提示成功但实际未变更,属于功能性 Bug。

问题 2.2.2 — resolveDataScope 角色硬编码

  • 位置auth-guard.ts 第 64、69、81、113、136 行
  • 问题:使用 roleNames.includes("admin")roleNames.includes("grade_head")roleNames.includes("teacher")roleNames.includes("student")roleNames.includes("parent") 硬编码角色名。
  • 违反规则前端权限判断统一使用 usePermission().hasPermission(),严禁出现 role === "xxx" 硬编码(项目规则虽针对前端,但服务端 DataScope 解析也应避免硬编码角色名,以支持动态角色)。
  • 后果:新增自定义角色无法正确解析 DataScopegrade_head/teaching_head 被合并处理,无法差异化授权。

问题 2.2.3 — role-utils.tsgrade_head/teaching_head 折叠为 teacher

  • 位置role-utils.ts 第 17 行
  • 问题normalizeRolegrade_head/teaching_head 映射为 teacher,丢失了角色差异。
  • 后果auth.ts JWT 回调中 token.role = resolvePrimaryRole(allRoles) 后,年级主任/教务主任的 session.user.role 变为 teacher,前端若依赖 role 字段做路由跳转会丢失差异。

2.3 国际化遗漏P1

问题 2.3.1 — RBAC 组件大量硬编码英文

  • 位置
  • 违反规则所有用户可见文本必须适配 i18n使用 next-intl提取翻译键(项目规则「安全规范」+ 项目记忆 Hard Constraints
  • 后果:中文用户看到英文界面,体验不一致;无法切换语言。

问题 2.3.2 — admin-users-view.tsx 大量硬编码中文

  • 位置admin-users-view.tsx 第 142、148、159、169、172、180、190、203、211-216、222、227、247、252、260、273、284、291、305、307、311、317 行
  • 问题:与 2.3.1 相反,此处全部硬编码中文("用户管理"、"批量导入"、"搜索姓名或邮箱..."、"所有角色"、"搜索"、"重置"、"暂无用户"、"姓名"、"邮箱"、"角色"、"手机"、"注册时间"、"操作"、"编辑"、"分配角色"、"删除"、"确认删除用户?"等)。
  • 违反规则:同 2.3.1。
  • 后果:英文用户看到中文界面;与 RBAC 其他组件语言不一致(一半英文一半中文)。

问题 2.3.3 — rbac.json 缺少权限点标签

  • 位置zh-CN/rbac.json + en/rbac.json
  • 问题PERMISSION_CATALOG 中每个权限点都定义了 labelKey(如 rbac:permissions.exam.create)和 descriptionKey,但 i18n 文件中只有 permissions.group.* 分组标签,缺少 permissions.exam.createpermissions.exam.create.desc 等具体权限点的翻译。
  • 后果RolePermissionMatrixPermissionCatalogViewt.has(labelKey) ? t(labelKey) : perm.key 回退到显示原始 keyEXAM_CREATE),用户无法理解权限含义。

2.4 TypeScript 类型不安全P1

问题 2.4.1 — as 断言违规

  • 位置
  • 违反规则禁止 as 断言(除非从 unknown 转换或测试中,需注释原因)项目规则「TypeScript 规则」)。
  • 后果:类型安全被绕过,若 DB schema 或 session 结构变更,编译期不报错,运行时才暴露。

2.5 错误处理与边界缺失P1

问题 2.5.1 — 缺少 loading.tsx / error.tsx

  • 位置
    • app/(dashboard)/admin/roles/ — 无 loading.tsxerror.tsx
    • app/(dashboard)/admin/roles/[id]/ — 无 loading.tsxerror.tsx
    • app/(dashboard)/admin/permissions/ — 无 loading.tsxerror.tsx
    • app/(dashboard)/admin/users/ — 无 loading.tsxerror.tsx(仅 users/import/ 有)
  • 违反规则All student routes must include loading.tsx and error.tsx for error boundaries(项目记忆 Hard Constraints虽针对 student 路由,但 admin 路由也应遵循)。
  • 后果:页面加载时无骨架屏,体验差;requirePermission 抛出 PermissionDeniedError 时无友好错误页,显示 Next.js 默认错误。

问题 2.5.2 — 无 React Error Boundary 包裹独立数据区块

  • 位置:所有 RBAC 组件
  • 问题RoleListRolePermissionMatrixPermissionCatalogView 等组件未用 Error Boundary 包裹,单个组件抛错会导致整页崩溃。
  • 后果:权限矩阵加载失败时,角色列表也无法显示。

问题 2.5.3 — 无空数据/无权限/网络异常边界状态

  • 位置
    • RolePermissionMatrix 无空权限组处理
    • UserRoleAssignDialog 无网络异常重试
    • PermissionCatalogView 无加载骨架
  • 后果:异常状态下用户看到空白或卡顿。

2.6 组件不可复用 / 逻辑未抽取P2

问题 2.6.1 — admin-users-view.tsx 与 RBAC 组件重复实现

  • 位置admin-users-view.tsx
  • 问题AdminUsersView 自行实现了删除确认对话框、分页、搜索、角色筛选,未复用 RBAC 模块的 EmptyState、分页组件等;同时硬编码了 UserRoleAssignDialog 的调用。
  • 后果UI 不一致,维护成本高。

问题 2.6.2 — admin-users-view.tsx 包含死代码

  • 位置:第 246-248 行「编辑」菜单项
  • 问题DropdownMenuItem 点击无任何行为,未绑定 onClick
  • 后果:用户点击「编辑」无反应,体验差。

问题 2.6.3 — 纯逻辑与 UI 未分离

  • 位置RolePermissionMatrix 中的 setsEqual 函数、选中状态计算逻辑
  • 问题setsEqual 工具函数内联在组件文件底部,未抽取到 hooks 或 utils选中状态 diff 逻辑未抽取。
  • 后果:无法单测,无法复用。

2.7 可访问性缺失P2

问题 2.7.1 — 缺少 ARIA 属性与键盘导航

  • 位置:所有 RBAC 组件
  • 问题
    • RoleList 表格无 <caption> 描述
    • RolePermissionMatrix 的 checkbox 分组无 fieldset/legend 语义
    • UserRoleAssignDialog 的角色列表无 role="list"
    • 图标按钮(如 MoreHorizontal 触发器)虽有 sr-only 但无 aria-label
  • 违反规则可访问性a11y语义化标签、ARIA 属性、键盘导航(项目规则「企业级补充」)。
  • 后果:屏幕阅读器用户无法理解页面结构;键盘用户操作困难。

2.8 性能与监控缺失P2

问题 2.8.1 — 无 React Server Components 流式渲染

  • 位置:所有 admin 页面
  • 问题:页面使用 export const dynamic = "force-dynamic" 但未使用 loading.tsx 或 Suspense 流式渲染,整页阻塞等待所有数据。
  • 后果:首屏白屏时间长。

问题 2.8.2 — 无关键操作埋点

  • 位置assignUserRolesActionsetRolePermissionsAction
  • 问题:虽有 logAudit 审计日志,但无前端交互埋点(如权限变更成功率、耗时、失败原因分布)。
  • 后果:无法监控权限管理操作的健康度。

2.9 安全性隐患P1

问题 2.9.1 — deleteUserAction 未校验目标用户是否为内置角色持有者

  • 位置users/actions.ts 第 201-217 行
  • 问题:删除用户时未检查该用户是否为最后一个 admin 角色持有者,可能导致系统无管理员。
  • 后果:管理员误删后无法恢复。

问题 2.9.2 — assignRolesToUser 未校验角色是否为内置且未禁用

  • 位置data-access-assignments.ts 第 42-73 行
  • 问题assignRolesToUser 仅校验角色名存在,未校验角色是否 isEnabled,可将禁用角色分配给用户(虽 resolvePermissions 会过滤禁用角色,但数据不一致)。
  • 后果用户被分配禁用角色后UI 显示已分配但实际无权限,造成困惑。

三、行业差距对比

3.1 权限模型维度

能力 行业优秀实践(如 PowerSchool、Veracross、Alma 当前实现 差距影响
权限继承 支持权限继承(如 admin 继承 teacher 所有权限) 扁平权限,无继承 新增角色需手动勾选所有权限,易遗漏
权限模板 提供角色模板(如「班主任」「学科组长」)一键创建 无模板,从零配置 管理员配置成本高
权限预览 保存前预览「该角色将获得哪些菜单/操作」 仅显示权限点列表 管理员无法直观理解权限效果
权限差异对比 对比两个角色的权限差异 无法评估角色调整影响
权限使用统计 统计每个权限点被多少角色/用户使用 PermissionCatalogView 仅统计角色数,未统计用户数 无法识别「僵尸权限」
数据范围权限 支持「只能看自己班级」「只能看本年级」等行级权限 DataScope 已实现但硬编码角色 自定义角色无法配置数据范围
权限生效时间 支持权限定时生效/过期 临时授权需手动撤销

3.2 UI/UX 维度

能力 行业优秀实践 当前实现 差距影响
权限矩阵搜索 支持按权限名/模块搜索过滤 无搜索67 个权限点全展示 管理员查找特定权限困难
权限分组折叠 支持折叠/展开权限分组 分组固定展开 页面过长,滚动疲劳
批量角色分配 支持批量给多个用户分配角色 仅单个用户 批量入职时效率低
角色克隆 复制现有角色创建新角色 相似角色需重新勾选
权限变更审计可视化 时间轴展示角色权限变更历史 仅审计日志文本 无法直观追溯权限演进
权限影响范围分析 显示「修改此角色将影响 N 个用户」 RoleList 显示 userCount 但编辑时不提示 管理员不知修改影响范围
实时权限校验 前端实时显示当前用户是否有权限操作 usePermission 已实现但未在 UI 普遍使用 用户点击后才报无权限

3.3 多角色支持维度

能力 行业优秀实践 当前实现 差距影响
多角色叠加 用户多角色权限取并集 resolvePermissions 已合并 已实现
主角色判定 支持配置主角色(用于路由/默认视图) resolvePrimaryRole 硬编码优先级 无法自定义主角色
角色冲突检测 检测互斥角色(如 teacher + student 用户可同时拥有冲突角色
角色有效期 角色分配支持起止时间 临时角色无法自动撤销

3.4 安全合规维度

能力 行业优秀实践 当前实现 差距影响
最小权限原则校验 提示「该角色权限过大」 易授予过度权限
敏感权限二次确认 修改 admin/删除角色需二次确认 RoleList 有删除确认,但改权限无 误操作风险
权限变更通知 权限变更通知受影响用户 用户不知权限被调整
权限分离审计 审计日志记录操作者 IP/UA logAudit 未记录 IP/UA 无法追溯操作来源

四、改进优先级建议

P0 — 紧急(安全/架构违规,必须立即修复)

编号 问题 改进方向
P0-1 admin/permissions/page.tsx 直查 DB问题 2.1.1 将权限统计查询下沉到 modules/rbac/data-access.ts,新增 getPermissionRoleCounts() 函数
P0-2 deleteUserAction 直查 DB问题 2.1.2 将删除逻辑下沉到 modules/users/data-access.ts,新增 deleteUserById() 并处理级联清理
P0-3 admin-users-view.tsx 用 fetch 删除用户(问题 2.1.3 改为调用 deleteUserAction Server Action
P0-4 getCurrentStudentUserauth()(问题 2.1.4 改为调用 getAuthContext() 获取 userId
P0-5 updateUserRoleAction 空实现(问题 2.2.1 删除此死代码,角色分配统一走 rbac/actions.tsassignUserRolesAction
P0-6 缺少 loading.tsx/error.tsx(问题 2.5.1 admin/rolesadmin/roles/[id]admin/permissionsadmin/users 新增

P1 — 高优先级i18n/类型安全/错误处理)

编号 问题 改进方向
P1-1 RBAC 组件硬编码英文(问题 2.3.1 提取翻译键到 rbac.json,组件改用 useTranslations
P1-2 admin-users-view.tsx 硬编码中文(问题 2.3.2 提取翻译键到 users.json
P1-3 rbac.json 缺权限点标签(问题 2.3.3 补全 67 个权限点的 label + desc 翻译
P1-4 as 断言违规(问题 2.4.1 用类型守卫替代,如 isPermission(value): value is Permission
P1-5 resolveDataScope 跨模块查表(问题 2.1.5 改为调用 classes/data-accessparent/data-access 等模块的查询函数
P1-6 resolveDataScope 角色硬编码(问题 2.2.2 改为基于权限点判断(如 hasPermission(DASHBOARD_ADMIN_READ)all scope或配置驱动
P1-7 无 Error Boundary问题 2.5.2 RoleListRolePermissionMatrixPermissionCatalogView 包裹 Error Boundary
P1-8 deleteUserAction 未保护最后 admin问题 2.9.1 删除前校验目标用户是否为最后一个 admin
P1-9 assignRolesToUser 未校验角色启用状态(问题 2.9.2 分配前过滤 isEnabled = false 的角色

P2 — 中长期(体验/性能/可扩展性)

编号 问题 改进方向
P2-1 权限矩阵无搜索/折叠 新增搜索框 + 分组折叠交互
P2-2 无角色模板 预置「班主任」「学科组长」等模板
P2-3 无权限变更影响提示 编辑权限时显示「将影响 N 个用户」
P2-4 无权限使用统计 PermissionCatalogView 增加用户数统计
P2-5 无 a11y 属性 补充 ARIA、captionfieldset
P2-6 无 RSC 流式渲染 引入 Suspense + 骨架屏
P2-7 无关键操作埋点 预留 trackPermissionChange() 接口
P2-8 role-utils.ts 折叠角色(问题 2.2.3 保留 grade_head/teaching_head 差异
P2-9 无权限继承 支持角色继承父角色权限
P2-10 无权限变更通知 权限变更后通知受影响用户

五、架构图同步说明

本次审计发现架构图存在以下遗漏/不一致,需同步更新:

5.1 004_architecture_impact_map.md 需补充

  1. shared/lib/auth-guard.tsresolveDataScope 跨模块依赖

    • 当前依赖 classesclassEnrollmentsclassSubjectTeachersgradesparentStudentRelations
    • 应标注为「待重构:改为调用各模块 data-access」
  2. modules/users/data-access.tsgetCurrentStudentUser 异常依赖

    • 直接调用 auth() 而非 getAuthContext()
    • 应标注为「待修复P0-4」
  3. app/(dashboard)/admin/permissions/page.tsx 直查 DB 违规

    • 应标注为「待修复P0-1」
  4. modules/users/actions.tsdeleteUserAction 直查 DB

    • 应标注为「待修复P0-2」
  5. 缺失的 loading.tsx/error.tsx

    • admin/rolesadmin/roles/[id]admin/permissionsadmin/users 四个路由

5.2 005_architecture_data.json 需补充

  1. modules.rbac.dependencies.dependsOn 增加 shared/lib/permissionsresolvePermissionsauth.ts 使用,但 rbac 模块本身不直接依赖,需在 auth 节点标注)
  2. modules.rbac.exports.dataAccess 增加 getPermissionRoleCountsP0-1 新增函数)
  3. modules.users.exports.dataAccess 增加 deleteUserByIdP0-2 新增函数)
  4. modules.users.exports.actions 标注 updateUserRoleAction 为 deprecated/删除
  5. app 路由节点增加缺失的 loading.tsx/error.tsx 文件

六、重构方案设计

6.1 目标架构

app/(dashboard)/admin/
├─ roles/
│  ├─ page.tsx              # RSC: 调用 rbac/data-access.getRoles()
│  ├─ loading.tsx          # 骨架屏
│  ├─ error.tsx            # 错误边界
│  └─ [id]/
│     ├─ page.tsx           # RSC: 调用 rbac/data-access.getRoleById()
│     ├─ loading.tsx
│     └─ error.tsx
├─ permissions/
│  ├─ page.tsx              # RSC: 调用 rbac/data-access.getPermissionRoleCounts()
│  ├─ loading.tsx
│  └─ error.tsx
└─ users/
   ├─ page.tsx              # RSC: 调用 users/data-access.getAdminUsers()
   ├─ loading.tsx
   └─ error.tsx

modules/rbac/
├─ actions.ts               # Server Actions编排层
├─ data-access.ts           # 角色 CRUD + 权限统计
├─ data-access-assignments.ts # 用户-角色分配
├─ data-access-permissions.ts # 权限查询(新增 getPermissionRoleCounts
├─ schema.ts                # Zod 验证
├─ types.ts                 # 类型定义
├─ lib/
│  ├─ permission-catalog.ts # 权限目录常量
│  └─ permission-diff.ts    # 权限差异计算纯函数(新增)
├─ hooks/
│  ├─ use-role-permissions.ts # 选中状态管理 Hook新增
│  └─ use-permission-search.ts # 权限搜索过滤 Hook新增
└─ components/
   ├─ role-list.tsx
   ├─ role-form-dialog.tsx
   ├─ role-permission-matrix.tsx
   ├─ permission-catalog-view.tsx
   ├─ user-role-assign-dialog.tsx
   ├─ role-management-view.tsx
   ├─ role-detail-edit-button.tsx
   ├─ permission-search-bar.tsx  # 新增
   ├─ permission-impact-badge.tsx # 新增:显示影响用户数
   └─ error-boundary.tsx        # 新增RBAC 专用错误边界

modules/users/
├─ actions.ts               # 删除 updateUserRoleActiondeleteUserAction 改调 data-access
├─ data-access.ts           # 新增 deleteUserById
└─ components/
   └─ admin-users-view.tsx  # i18n 化,改用 Server Action

shared/lib/
├─ auth-guard.ts            # resolveDataScope 改调各模块 data-access
├─ permissions.ts           # 用类型守卫替代 as
├─ role-utils.ts            # 保留 grade_head/teaching_head 差异
└─ type-guards.ts           # 新增 isPermission/isRole 类型守卫

6.2 数据服务接口抽象(解耦)

// modules/rbac/types.ts 新增

/** 角色数据服务接口 — 供依赖注入使用 */
export interface RoleDataService {
  getRoles(): Promise<RoleWithStats[]>
  getRoleById(id: string): Promise<RoleDetail | null>
  createRole(input: CreateRoleInput): Promise<RoleRecord>
  updateRole(id: string, input: UpdateRoleInput): Promise<RoleRecord>
  deleteRole(id: string): Promise<void>
  setRoleEnabled(id: string, enabled: boolean): Promise<RoleRecord>
  getRolePermissions(roleId: string): Promise<Permission[]>
  setRolePermissions(roleId: string, permissions: Permission[]): Promise<void>
  getPermissionRoleCounts(): Promise<Record<string, number>>
}

/** 用户-角色分配数据服务接口 */
export interface UserRoleAssignmentService {
  getUserRoleNames(userId: string): Promise<string[]>
  assignRolesToUser(userId: string, roleNames: string[]): Promise<void>
  getUserRoleAssignments(params?: {
    page?: number
    pageSize?: number
    search?: string
    role?: string
  }): Promise<PaginatedResult<UserRoleAssignment>>
}

// modules/rbac/data-access.ts 实现 RoleDataService
// modules/rbac/data-access-assignments.ts 实现 UserRoleAssignmentService

6.3 组合优先的 UI 设计

// modules/rbac/components/role-permission-matrix.tsx 重构后

"use client"

import { useRolePermissions } from "../hooks/use-role-permissions"
import { usePermissionSearch } from "../hooks/use-permission-search"
import { PermissionSearchBar } from "./permission-search-bar"
import { PermissionImpactBadge } from "./permission-impact-badge"
import { ErrorBoundary } from "./error-boundary"

interface RolePermissionMatrixProps {
  roleId: string
  roleName: string
  currentPermissions: Permission[]
  userCount: number  // 新增:用于影响提示
  isLocked: boolean
}

export function RolePermissionMatrix({
  roleId,
  roleName,
  currentPermissions,
  userCount,
  isLocked,
}: RolePermissionMatrixProps) {
  const { selected, hasChanges, toggle, toggleGroup, reset } = useRolePermissions(currentPermissions)
  const { query, filteredCatalog, setQuery } = usePermissionSearch(PERMISSION_CATALOG)

  return (
    <ErrorBoundary fallback={<PermissionMatrixError />}>
      <Card>
        <CardHeader>
          <PermissionMatrixHeader
            roleName={roleName}
            isLocked={isLocked}
            selectedCount={selected.size}
            hasChanges={hasChanges}
            userCount={userCount}
          />
          {!isLocked && hasChanges && (
            <PermissionImpactBadge count={userCount} />
          )}
        </CardHeader>
        <CardContent>
          <PermissionSearchBar value={query} onChange={setQuery} />
          <PermissionGroups
            catalog={filteredCatalog}
            selected={selected}
            isLocked={isLocked}
            onToggle={toggle}
            onToggleGroup={toggleGroup}
          />
          <PermissionMatrixActions
            isLocked={isLocked}
            hasChanges={hasChanges}
            onReset={reset}
            onSave={() => handleSave(roleId, selected)}
          />
        </CardContent>
      </Card>
    </ErrorBoundary>
  )
}

6.4 i18n 翻译文件结构

// shared/i18n/messages/zh-CN/rbac.json补全后
{
  "roles": { ... },
  "permissions": {
    "title": "权限目录",
    "description": "系统中定义的所有权限点,按模块分组。",
    "group": { ... },
    "exam": {
      "create": { "label": "创建考试", "desc": "允许创建新考试" },
      "read": { "label": "查看考试", "desc": "允许查看考试列表和详情" }
      // ... 67 个权限点
    }
  },
  "matrix": {
    "search": "搜索权限...",
    "selected": "已选 {count} 项",
    "impact": "修改将影响 {count} 个用户",
    "save": "保存更改",
    "reset": "重置"
  },
  "errors": {
    "loadFailed": "加载失败,请重试",
    "saveFailed": "保存失败:{message}"
  }
}

6.5 错误与边界处理

// modules/rbac/components/error-boundary.tsx

"use client"

import { Component, type ReactNode } from "react"
import { Button } from "@/shared/components/ui/button"
import { AlertCircle } from "lucide-react"

interface Props {
  children: ReactNode
  fallback?: ReactNode
}

interface State {
  hasError: boolean
  error?: Error
}

export class ErrorBoundary extends Component<Props, State> {
  state: State = { hasError: false }

  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error }
  }

  render(): ReactNode {
    if (this.state.hasError) {
      return this.props.fallback ?? <DefaultErrorFallback error={this.state.error} />
    }
    return this.props.children
  }
}

function DefaultErrorFallback({ error }: { error?: Error }): ReactNode {
  return (
    <div role="alert" className="rounded-md border border-destructive/50 p-4">
      <AlertCircle className="h-5 w-5 text-destructive" aria-hidden="true" />
      <p className="mt-2 text-sm text-destructive">
        {error?.message ?? "加载失败"}
      </p>
      <Button variant="outline" size="sm" className="mt-2" onClick={() => window.location.reload()}>
        重试
      </Button>
    </div>
  )
}
// app/(dashboard)/admin/roles/loading.tsx

import { Skeleton } from "@/shared/components/ui/skeleton"

export default function Loading(): JSX.Element {
  return (
    <div className="flex h-full flex-col space-y-6 p-8">
      <div className="space-y-2">
        <Skeleton className="h-8 w-48" />
        <Skeleton className="h-4 w-72" />
      </div>
      <Skeleton className="h-96 w-full" />
    </div>
  )
}
// app/(dashboard)/admin/roles/error.tsx

"use client"

import { useEffect } from "react"
import { Button } from "@/shared/components/ui/button"
import { ShieldAlert } from "lucide-react"

export default function Error({ error, reset }: {
  error: Error & { digest?: string }
  reset: () => void
}): JSX.Element {
  useEffect(() => {
    console.error("Roles page error:", error)
  }, [error])

  return (
    <div className="flex h-full flex-col items-center justify-center space-y-4 p-8">
      <ShieldAlert className="h-12 w-12 text-destructive" aria-hidden="true" />
      <h2 className="text-xl font-semibold">角色管理加载失败</h2>
      <p className="text-sm text-muted-foreground">{error.message}</p>
      <Button onClick={reset}>重试</Button>
    </div>
  )
}

6.6 可测试性设计

// modules/rbac/lib/permission-diff.ts纯函数可单测

import type { Permission } from "@/shared/types/permissions"

export interface PermissionDiff {
  added: Permission[]
  removed: Permission[]
  unchanged: Permission[]
}

export function diffPermissions(
  before: Permission[],
  after: Permission[]
): PermissionDiff {
  const beforeSet = new Set(before)
  const afterSet = new Set(after)

  return {
    added: after.filter((p) => !beforeSet.has(p)),
    removed: before.filter((p) => !afterSet.has(p)),
    unchanged: after.filter((p) => beforeSet.has(p)),
  }
}

export function arePermissionsEqual(
  a: Permission[],
  b: Permission[]
): boolean {
  if (a.length !== b.length) return false
  const set = new Set(a)
  return b.every((p) => set.has(p))
}
// modules/rbac/hooks/use-role-permissions.ts纯逻辑 Hook

"use client"

import { useCallback, useMemo, useState } from "react"
import type { Permission } from "@/shared/types/permissions"
import { arePermissionsEqual } from "../lib/permission-diff"

export function useRolePermissions(initial: Permission[]) {
  const [selected, setSelected] = useState<Set<string>>(new Set(initial))

  const hasChanges = useMemo(
    () => !setsEqual(selected, new Set(initial)),
    [selected, initial]
  )

  const toggle = useCallback((permission: string, checked: boolean) => {
    setSelected((prev) => {
      const next = new Set(prev)
      if (checked) next.add(permission)
      else next.delete(permission)
      return next
    })
  }, [])

  const toggleGroup = useCallback((permissions: string[], checked: boolean) => {
    setSelected((prev) => {
      const next = new Set(prev)
      for (const p of permissions) {
        if (checked) next.add(p)
        else next.delete(p)
      }
      return next
    })
  }, [])

  const reset = useCallback(() => setSelected(new Set(initial)), [initial])

  return { selected, hasChanges, toggle, toggleGroup, reset }
}

function setsEqual<T>(a: Set<T>, b: Set<T>): boolean {
  if (a.size !== b.size) return false
  for (const v of a) if (!b.has(v)) return false
  return true
}

6.7 配置驱动的可扩展设计

// modules/rbac/lib/role-templates.ts新增

import type { Permission } from "@/shared/types/permissions"
import { Permissions } from "@/shared/types/permissions"

export interface RoleTemplate {
  id: string
  nameKey: string
  descriptionKey: string
  permissions: Permission[]
}

export const ROLE_TEMPLATES: RoleTemplate[] = [
  {
    id: "homeroom_teacher",
    nameKey: "rbac:templates.homeroom_teacher.name",
    descriptionKey: "rbac:templates.homeroom_teacher.desc",
    permissions: [
      Permissions.EXAM_READ,
      Permissions.HOMEWORK_CREATE,
      Permissions.HOMEWORK_GRADE,
      Permissions.CLASS_READ,
      Permissions.ATTENDANCE_MANAGE,
      Permissions.MESSAGE_SEND,
      Permissions.DASHBOARD_TEACHER_READ,
    ],
  },
  {
    id: "subject_teacher",
    nameKey: "rbac:templates.subject_teacher.name",
    descriptionKey: "rbac:templates.subject_teacher.desc",
    permissions: [
      Permissions.EXAM_CREATE,
      Permissions.QUESTION_CREATE,
      Permissions.HOMEWORK_GRADE,
      Permissions.GRADE_RECORD_MANAGE,
    ],
  },
  // ... 更多模板
]

6.8 监控埋点接口

// shared/lib/analytics.ts预留接口

export interface PermissionChangeMetrics {
  action: "role.create" | "role.update" | "role.delete" | "role.set_permissions" | "user.assign_roles"
  targetId: string
  targetType: "role" | "user"
  changes?: {
    added?: number
    removed?: number
  }
  affectedUsers?: number
  durationMs: number
  success: boolean
  errorMessage?: string
}

export async function trackPermissionChange(metrics: PermissionChangeMetrics): Promise<void> {
  // 预留实现:可接入 Sentry / PostHog / 自建埋点
  if (process.env.NODE_ENV === "development") {
    console.log("[permission-change]", metrics)
  }
}

七、实施计划

7.1 第一阶段P0 — 立即修复)

  1. P0-1admin/permissions/page.tsx 改用 data-access

    • modules/rbac/data-access-permissions.ts 新增 getPermissionRoleCounts()
    • 页面改为 import { getPermissionRoleCounts } from "@/modules/rbac/data-access-permissions"
  2. P0-2deleteUserAction 改用 data-access

    • modules/users/data-access.ts 新增 deleteUserById(userId: string): Promise<void>
    • 包含级联清理 sessions、usersToRoles、passwordSecurity
    • actions.ts 改调 deleteUserById
  3. P0-3admin-users-view.tsx 改用 Server Action

    • 删除 fetch("/api/admin/users/...") 逻辑
    • 改用 useActionMutation + deleteUserAction
  4. P0-4getCurrentStudentUser 改用 getAuthContext()

    • const ctx = await getAuthContext()ctx.userId
  5. P0-5:删除 updateUserRoleAction 空实现

  6. P0-6:新增 loading.tsx/error.tsx

    • 4 个路由各 2 个文件,共 8 个文件

7.2 第二阶段P1 — i18n + 类型安全)

  1. P1-1/P1-2/P1-3i18n 化

    • 补全 rbac.json 的 67 个权限点标签
    • RBAC 组件改用 useTranslations
    • admin-users-view.tsx 改用 useTranslations
  2. P1-4:类型守卫

    • shared/lib/type-guards.ts 新增 isPermission(value: unknown): value is Permission
    • 替换所有 as Permission 断言
  3. P1-5resolveDataScope 改调模块 data-access

    • classes 模块新增 getClassIdsForTeacher(userId)getClassIdsForStudent(userId)
    • parent 模块新增 getChildrenIdsForParent(userId)
    • school 模块新增 getGradeIdsForHead(userId)
  4. P1-6resolveDataScope 改为配置驱动

    • 新增 shared/lib/data-scope-resolver.ts
    • 基于权限点而非角色名判断 scope
  5. P1-7Error Boundary

    • 新增 modules/rbac/components/error-boundary.tsx
    • 包裹 RoleListRolePermissionMatrixPermissionCatalogView
  6. P1-8/P1-9:安全性增强

    • deleteUserAction 校验最后 admin
    • assignRolesToUser 过滤禁用角色

7.3 第三阶段P2 — 体验/性能/可扩展)

  1. P2-1:权限矩阵搜索 + 折叠
  2. P2-2:角色模板
  3. P2-3:权限变更影响提示
  4. P2-4:权限使用统计
  5. P2-5a11y 增强
  6. P2-6Suspense 流式渲染
  7. P2-7:监控埋点
  8. P2-8role-utils.ts 保留角色差异
  9. P2-9:权限继承
  10. P2-10:权限变更通知

八、合规性检查

约束 当前状态 重构后
三层架构 app → modules → shared app 直查 DB 全部走 data-access
app/ 不直接访问数据库 3 处违规 修复
模块间通过 data-access 通信 auth-guard 跨模块查表 改调 data-access
Server Action 调用 requirePermission() 已实现 保持
前端用 usePermission().hasPermission() 已实现 保持
i18n 适配 大量硬编码 全部提取翻译键
TypeScript 严格模式(无 any/as 多处 as 用类型守卫替代
单文件行数 ≤ 500/800/1000 当前均未超标 保持
loading.tsx/error.tsx 缺失 补全
架构图同步 有遗漏 同步更新

九、结论

用户权限模块的核心基础设施(Permissions 常量、resolvePermissionsrequirePermissionusePermission设计合理RBAC 模块的 actions/data-access 分层清晰。但存在 6 个 P0 级架构违规app 直查 DB、Server Action 直查 DB、客户端 fetch、认证入口不统一、空实现 Action、缺 loading/error9 个 P1 级问题i18n 遗漏、类型断言、跨模块查表、角色硬编码、错误边界缺失、安全隐患)和 10 个 P2 级改进(搜索/模板/统计/a11y/性能/监控等)。

建议按 P0 → P1 → P2 顺序实施P0 必须立即修复以消除安全与架构风险P1 在本迭代内完成P2 纳入后续迭代规划。