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:
900
docs/architecture/audit/permissions-audit-report.md
Normal file
900
docs/architecture/audit/permissions-audit-report.md
Normal 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 # 删除 updateUserRoleAction,deleteUserAction 改调 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 纳入后续迭代规划。
|
||||
Reference in New Issue
Block a user