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
This commit is contained in:
SpecialX
2026-07-03 10:23:34 +08:00
parent 365c36d97b
commit 89b9e181d2
29 changed files with 13009 additions and 230 deletions

View File

@@ -0,0 +1,900 @@
# 用户权限模块审计报告
> 审计范围用户权限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 纳入后续迭代规划。