Files
NextEdu/docs/architecture/audit/permissions-audit-report.md
SpecialX 89b9e181d2 docs(audit): add audit reports for grades, homework, lesson-preparation, messaging, permissions, question-bank, settings, textbooks
- Add grades-audit-report

- Add homework-audit-report and homework-exams-audit-report

- Add lesson-preparation-audit-report-v3 and v4

- Add messaging-audit-report

- Add permissions-audit-report

- Add question-bank-audit-report

- Add settings-profile-audit-report-v3

- Add textbooks-audit-report-v3
2026-07-03 10:23:34 +08:00

901 lines
44 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.
# 用户权限模块审计报告
> 审计范围用户权限RBAC模块包括 `shared/types/permissions.ts`、`shared/lib/permissions.ts`、`shared/lib/auth-guard.ts`、`shared/hooks/use-permission.ts`、`shared/lib/role-utils.ts`、`shared/lib/session.ts`、`auth.ts`、`modules/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_SEED`6 内置角色种子)+ `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()` Hook`hasPermission`/`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 Action`requirePermission` + 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_CATALOG`24 分组)+ `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/db``shared/types/permissions``shared/lib/auth-guard``shared/lib/audit-logger``shared/lib/change-logger`
- 被使用方:`admin/roles``admin/permissions``admin/users``modules/users`
- i18n 命名空间 `rbac`,文件 `zh-CN/rbac.json` + `en/rbac.json`
**架构图遗漏**
- `shared/lib/permissions.ts``resolvePermissions`)未在 rbac 模块依赖中显式列出(实际被 `auth.ts` 使用)
- `shared/lib/auth-guard.ts``resolveDataScope` 内部直接查询 `classes`/`classEnrollments`/`classSubjectTeachers`/`grades`/`parentStudentRelations` 表,属于跨模块数据访问,未在架构图中标注
- `modules/users/data-access.ts``getCurrentStudentUser` 直接调用 `auth()` 而非 `getAuthContext()`,未在架构图中标注此异常依赖
- `app/(dashboard)/admin/permissions/page.tsx` 直接查询 `rolePermissions`/`roles` 表,未走 data-access架构图未标注此违规
- `modules/users/actions.ts``deleteUserAction` 直接 `db.delete(users)`,未走 data-access架构图未标注
- `app/(dashboard)/admin/roles``admin/permissions``admin/users`(非 import 子目录)缺少 `loading.tsx`/`error.tsx`,架构图未标注
---
## 二、现存问题与原因分析
### 2.1 架构分层违规P0
#### 问题 2.1.1 — `app/` 直接访问数据库
- **位置**[admin/permissions/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/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](file:///e:/Desktop/CICD/src/modules/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](file:///e:/Desktop/CICD/src/modules/users/components/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](file:///e:/Desktop/CICD/src/modules/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](file:///e:/Desktop/CICD/src/shared/lib/auth-guard.ts) 第 70-163 行
- **问题**`resolveDataScope` 直接查询 `classes``classEnrollments``classSubjectTeachers``grades``parentStudentRelations` 表,这些表属于 `school``classes``parent` 等模块。
- **违反规则**`模块间只能通过对方 data-access 通信,禁止跨模块直接查询数据库表`(项目规则「架构分层规则」)。
- **后果**`shared/lib` 反向依赖业务模块的表结构;若 `classes` 模块重构表结构,`auth-guard` 会编译报错;`shared/` 不得反向依赖业务模块的硬约束被破坏。
### 2.2 权限校验缺失或硬编码P0/P1
#### 问题 2.2.1 — `updateUserRoleAction` 是空实现
- **位置**[users/actions.ts](file:///e:/Desktop/CICD/src/modules/users/actions.ts) 第 177-196 行
- **问题**`updateUserRoleAction` 接收 `userId``role` 参数后,仅 `void userId; void role;` 直接返回成功,未执行任何实际操作。
- **违反规则**`所有 Server Action 必须调用 requirePermission() 进行权限校验`(虽已调用 `requirePermission`,但逻辑为空)+ 功能完整性。
- **后果**:管理员在用户管理页点击「编辑」试图修改角色时,系统提示成功但实际未变更,属于功能性 Bug。
#### 问题 2.2.2 — `resolveDataScope` 角色硬编码
- **位置**[auth-guard.ts](file:///e:/Desktop/CICD/src/shared/lib/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 解析也应避免硬编码角色名,以支持动态角色)。
- **后果**:新增自定义角色无法正确解析 DataScope`grade_head`/`teaching_head` 被合并处理,无法差异化授权。
#### 问题 2.2.3 — `role-utils.ts` 将 `grade_head`/`teaching_head` 折叠为 `teacher`
- **位置**[role-utils.ts](file:///e:/Desktop/CICD/src/shared/lib/role-utils.ts) 第 17 行
- **问题**`normalizeRole``grade_head`/`teaching_head` 映射为 `teacher`,丢失了角色差异。
- **后果**`auth.ts` JWT 回调中 `token.role = resolvePrimaryRole(allRoles)` 后,年级主任/教务主任的 `session.user.role` 变为 `teacher`,前端若依赖 `role` 字段做路由跳转会丢失差异。
### 2.3 国际化遗漏P1
#### 问题 2.3.1 — RBAC 组件大量硬编码英文
- **位置**
- [role-list.tsx](file:///e:/Desktop/CICD/src/modules/rbac/components/role-list.tsx) 第 84、88、98、102、140、151、153、173、179、190、216、227 行("No roles yet"、"Create Role"、"Roles"、"System"、"Custom"、"Enabled"、"Disabled"、"Edit"、"Enable"、"Disable"、"Delete"、"Deleting..."
- [role-form-dialog.tsx](file:///e:/Desktop/CICD/src/modules/rbac/components/role-form-dialog.tsx) 第 74、78-79、89、99、109、125 行("Create new role"、"Edit role"、"Role name"、"Description"、"Cancel"、"Saving..."
- [user-role-assign-dialog.tsx](file:///e:/Desktop/CICD/src/modules/rbac/components/user-role-assign-dialog.tsx) 第 102、104-107、113、137、151、153、173 行("Assign roles"、"Save roles"、"Cancel"、"Saving..."
- [permission-catalog-view.tsx](file:///e:/Desktop/CICD/src/modules/rbac/components/permission-catalog-view.tsx) 第 35、44 行("Permission Catalog"、"All permission points..."
- [admin/roles/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/roles/page.tsx) 第 28-31 行
- [admin/roles/[id]/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/roles/[id]/page.tsx) 第 53、56、63、65、78-80 行
- [admin/permissions/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/permissions/page.tsx) 第 43-44 行
- **违反规则**`所有用户可见文本必须适配 i18n使用 next-intl提取翻译键`(项目规则「安全规范」+ 项目记忆 Hard Constraints
- **后果**:中文用户看到英文界面,体验不一致;无法切换语言。
#### 问题 2.3.2 — `admin-users-view.tsx` 大量硬编码中文
- **位置**[admin-users-view.tsx](file:///e:/Desktop/CICD/src/modules/users/components/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](file:///e:/Desktop/CICD/src/shared/i18n/messages/zh-CN/rbac.json) + [en/rbac.json](file:///e:/Desktop/CICD/src/shared/i18n/messages/en/rbac.json)
- **问题**`PERMISSION_CATALOG` 中每个权限点都定义了 `labelKey`(如 `rbac:permissions.exam.create`)和 `descriptionKey`,但 i18n 文件中只有 `permissions.group.*` 分组标签,缺少 `permissions.exam.create``permissions.exam.create.desc` 等具体权限点的翻译。
- **后果**`RolePermissionMatrix``PermissionCatalogView``t.has(labelKey) ? t(labelKey) : perm.key` 回退到显示原始 key`EXAM_CREATE`),用户无法理解权限含义。
### 2.4 TypeScript 类型不安全P1
#### 问题 2.4.1 — `as` 断言违规
- **位置**
- [permissions.ts](file:///e:/Desktop/CICD/src/shared/lib/permissions.ts) 第 279、296 行 `row.permission as Permission`
- [auth-guard.ts](file:///e:/Desktop/CICD/src/shared/lib/auth-guard.ts) 第 27-28 行 `session.user.roles as Role[]``session.user.permissions as Permission[]`
- [auth.ts](file:///e:/Desktop/CICD/src/auth.ts) 第 146 行 `user as { id: string; ... }`、第 196 行 `token.permissions as typeof token.permissions`、第 217-223 行 `message as { ... }`
- [data-access.ts](file:///e:/Desktop/CICD/src/modules/rbac/data-access.ts) 第 81、186 行 `row.permission as Permission`
- [data-access-assignments.ts](file:///e:/Desktop/CICD/src/modules/rbac/data-access-assignments.ts) 第 149 行 `params.role as string`
- [admin/permissions/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/permissions/page.tsx) 第 36 行 `row.permission as Permission`
- [users/actions.ts](file:///e:/Desktop/CICD/src/modules/users/actions.ts) 第 183-184、207 行 `formData.get(...) as string`
- [admin-users-view.tsx](file:///e:/Desktop/CICD/src/modules/users/components/admin-users-view.tsx) 第 128 行 `e as Error`
- **违反规则**`禁止 as 断言(除非从 unknown 转换或测试中,需注释原因)`项目规则「TypeScript 规则」)。
- **后果**:类型安全被绕过,若 DB schema 或 session 结构变更,编译期不报错,运行时才暴露。
### 2.5 错误处理与边界缺失P1
#### 问题 2.5.1 — 缺少 `loading.tsx` / `error.tsx`
- **位置**
- `app/(dashboard)/admin/roles/` — 无 `loading.tsx``error.tsx`
- `app/(dashboard)/admin/roles/[id]/` — 无 `loading.tsx``error.tsx`
- `app/(dashboard)/admin/permissions/` — 无 `loading.tsx``error.tsx`
- `app/(dashboard)/admin/users/` — 无 `loading.tsx``error.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 组件
- **问题**`RoleList``RolePermissionMatrix``PermissionCatalogView` 等组件未用 Error Boundary 包裹,单个组件抛错会导致整页崩溃。
- **后果**:权限矩阵加载失败时,角色列表也无法显示。
#### 问题 2.5.3 — 无空数据/无权限/网络异常边界状态
- **位置**
- `RolePermissionMatrix` 无空权限组处理
- `UserRoleAssignDialog` 无网络异常重试
- `PermissionCatalogView` 无加载骨架
- **后果**:异常状态下用户看到空白或卡顿。
### 2.6 组件不可复用 / 逻辑未抽取P2
#### 问题 2.6.1 — `admin-users-view.tsx` 与 RBAC 组件重复实现
- **位置**[admin-users-view.tsx](file:///e:/Desktop/CICD/src/modules/users/components/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 — 无关键操作埋点
- **位置**`assignUserRolesAction``setRolePermissionsAction`
- **问题**:虽有 `logAudit` 审计日志,但无前端交互埋点(如权限变更成功率、耗时、失败原因分布)。
- **后果**:无法监控权限管理操作的健康度。
### 2.9 安全性隐患P1
#### 问题 2.9.1 — `deleteUserAction` 未校验目标用户是否为内置角色持有者
- **位置**[users/actions.ts](file:///e:/Desktop/CICD/src/modules/users/actions.ts) 第 201-217 行
- **问题**:删除用户时未检查该用户是否为最后一个 admin 角色持有者,可能导致系统无管理员。
- **后果**:管理员误删后无法恢复。
#### 问题 2.9.2 — `assignRolesToUser` 未校验角色是否为内置且未禁用
- **位置**[data-access-assignments.ts](file:///e:/Desktop/CICD/src/modules/rbac/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 | `getCurrentStudentUser``auth()`(问题 2.1.4 | 改为调用 `getAuthContext()` 获取 userId |
| P0-5 | `updateUserRoleAction` 空实现(问题 2.2.1 | 删除此死代码,角色分配统一走 `rbac/actions.ts``assignUserRolesAction` |
| P0-6 | 缺少 `loading.tsx`/`error.tsx`(问题 2.5.1 | 为 `admin/roles``admin/roles/[id]``admin/permissions``admin/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-access``parent/data-access` 等模块的查询函数 |
| P1-6 | `resolveDataScope` 角色硬编码(问题 2.2.2 | 改为基于权限点判断(如 `hasPermission(DASHBOARD_ADMIN_READ)``all` scope或配置驱动 |
| P1-7 | 无 Error Boundary问题 2.5.2 | 为 `RoleList``RolePermissionMatrix``PermissionCatalogView` 包裹 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、`caption``fieldset` |
| 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.ts``resolveDataScope` 跨模块依赖**
- 当前依赖 `classes``classEnrollments``classSubjectTeachers``grades``parentStudentRelations`
- 应标注为「待重构:改为调用各模块 data-access」
2. **`modules/users/data-access.ts``getCurrentStudentUser` 异常依赖**
- 直接调用 `auth()` 而非 `getAuthContext()`
- 应标注为「待修复P0-4」
3. **`app/(dashboard)/admin/permissions/page.tsx` 直查 DB 违规**
- 应标注为「待修复P0-1」
4. **`modules/users/actions.ts``deleteUserAction` 直查 DB**
- 应标注为「待修复P0-2」
5. **缺失的 `loading.tsx`/`error.tsx`**
- `admin/roles``admin/roles/[id]``admin/permissions``admin/users` 四个路由
### 5.2 `005_architecture_data.json` 需补充
1. `modules.rbac.dependencies.dependsOn` 增加 `shared/lib/permissions``resolvePermissions``auth.ts` 使用,但 rbac 模块本身不直接依赖,需在 `auth` 节点标注)
2. `modules.rbac.exports.dataAccess` 增加 `getPermissionRoleCounts`P0-1 新增函数)
3. `modules.users.exports.dataAccess` 增加 `deleteUserById`P0-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 数据服务接口抽象(解耦)
```typescript
// 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 设计
```tsx
// 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 翻译文件结构
```json
// 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 错误与边界处理
```tsx
// 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>
)
}
```
```tsx
// 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>
)
}
```
```tsx
// 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 可测试性设计
```typescript
// 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))
}
```
```typescript
// 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 配置驱动的可扩展设计
```typescript
// 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 监控埋点接口
```typescript
// 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-1**`admin/permissions/page.tsx` 改用 data-access
-`modules/rbac/data-access-permissions.ts` 新增 `getPermissionRoleCounts()`
- 页面改为 `import { getPermissionRoleCounts } from "@/modules/rbac/data-access-permissions"`
2. **P0-2**`deleteUserAction` 改用 data-access
-`modules/users/data-access.ts` 新增 `deleteUserById(userId: string): Promise<void>`
- 包含级联清理 sessions、usersToRoles、passwordSecurity
- actions.ts 改调 `deleteUserById`
3. **P0-3**`admin-users-view.tsx` 改用 Server Action
- 删除 `fetch("/api/admin/users/...")` 逻辑
- 改用 `useActionMutation` + `deleteUserAction`
4. **P0-4**`getCurrentStudentUser` 改用 `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-3**i18n 化
- 补全 `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-5**`resolveDataScope` 改调模块 data-access
- `classes` 模块新增 `getClassIdsForTeacher(userId)``getClassIdsForStudent(userId)`
- `parent` 模块新增 `getChildrenIdsForParent(userId)`
- `school` 模块新增 `getGradeIdsForHead(userId)`
4. **P1-6**`resolveDataScope` 改为配置驱动
- 新增 `shared/lib/data-scope-resolver.ts`
- 基于权限点而非角色名判断 scope
5. **P1-7**Error Boundary
- 新增 `modules/rbac/components/error-boundary.tsx`
- 包裹 `RoleList``RolePermissionMatrix``PermissionCatalogView`
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-5**a11y 增强
6. **P2-6**Suspense 流式渲染
7. **P2-7**:监控埋点
8. **P2-8**`role-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` 常量、`resolvePermissions``requirePermission``usePermission`设计合理RBAC 模块的 actions/data-access 分层清晰。但存在 **6 个 P0 级架构违规**app 直查 DB、Server Action 直查 DB、客户端 fetch、认证入口不统一、空实现 Action、缺 loading/error、**9 个 P1 级问题**i18n 遗漏、类型断言、跨模块查表、角色硬编码、错误边界缺失、安全隐患)和 **10 个 P2 级改进**(搜索/模板/统计/a11y/性能/监控等)。
建议按 P0 → P1 → P2 顺序实施P0 必须立即修复以消除安全与架构风险P1 在本迭代内完成P2 纳入后续迭代规划。