--- title: 角色与权限 description: 权限矩阵、实现位置,以及"前端隐藏 ≠ 权限"这条纪律。 --- # 角色与权限 ## 矩阵在哪 **唯一一份**在 `packages/shared/src/permissions.ts`: - 后端 `lib/access.ts` 复用它做断言; - 两端前端用它隐藏入口; - 验收脚本按它逐条断言。 所以不存在"前端一套后端一套"。改权限就改这一个文件(改完记得 `pnpm build:shared`)。 ## 角色判定 班级内的角色由「账号角色 + 与该班级的关系」共同决定: | 角色 | 判定方式 | 可见范围 | | ------------------ | ------------------------------ | -------------------------- | | `ADMIN` 管理员 | `User.role = ADMIN` | 全部班级 | | 班主任 `HEAD` | `Class.teacherId === 当前用户` | 本班 | | 科任老师 `SUBJECT` | `ClassTeacher` 中存在当前用户 | 被分配的班级 | | 学生 `STUDENT` | `User.classId` / 班级账号 | 自己的班级(只读业务数据) | | `NONE` | 与这个班没有任何关系 | 什么都看不到 | 判定函数是 `resolveClassRole()`。 ## 权限矩阵 | 操作 | 管理员 | 班主任 | 科任老师 | | -------------------------------------------------- | :----: | :----------: | :------: | | 班级创建 / 修改 / 删除 | ✅ | ❌ | ❌ | | 分配班主任与科任老师 | ✅ | ❌ | ❌ | | 学生名单管理(增删 / 导入) | ✅ | ❌ | ❌ | | 教师录入(新建 / 导入名单 / 修改密码) | ✅ | ❌ | ❌ | | 数据库管理(备份 / 导入导出 / 一键切换) | ✅ | ❌ | ❌ | | 课表管理(增删改 / 时间配置导入) | ✅ | ✅(仅本班) | ❌ | | 布置作业 | ✅ | ✅ | ✅ | | 叫人 | ✅ | ✅ | ✅ | | 发布通知 | ✅ | ✅ | ✅ | | 成绩录入 / 修改 / 导入(仅本班) | ✅ | ✅(仅本班) | ❌ | | 班级密码维护(`PATCH /classes/:id/class-account`) | ✅ | ❌ | ❌ | ## 实现要点 **路由级只做角色过滤**:`requireRole('ADMIN', 'TEACHER')`。 **班级维度的判断在服务层**:`src/lib/access.ts` 提供 ``` assertCanManageClasses / assertCanAssignTeachers / assertCanManageRoster assertCanManageSchedule / assertCanPublishContent / assertCanManageGrades ``` **列表数据用 `resolveClassScope()` 收敛**(`classId in [...]`),单条接口直接断言; 越权一律 403。 **鉴权中间件会回查数据库**(`middleware/auth.ts`):JWT 校验之后重新读一次用户与班级关系, 所以改了班级 / 角色会**立即生效**,不用等令牌过期。班级会话则回查 `Class` 表。 ## 前端隐藏 ≠ 权限 前端的隐藏只是"别让人看见入口": - 教师登录时侧边栏不显示「班级管理 / 学生管理 / 教师管理 / 数据库管理」; - 课表页的增删改按钮只对「管理员或本班班主任」显示; - 成绩页的录入 / 删除按钮同理。 但这些页面**直接输网址也进不去**(路由 `meta.roles` 会把非管理员挡回仪表盘), 而且**所有写入请求服务端都会再断言一次**。绕过菜单、绕过前端,一样 403。 > 新增视图时别忘了两处:路由的 `meta.roles` + 服务层对应 service 里的 `assertCanXxx`。 > 只做其中一个都等于没做。 ## 学生账号的两个反向约束 1. **学生不能登录**。`/auth/login` 遇到 `role=STUDENT` 直接 403; 且**角色判定在密码校验之前** —— 否则"密码错(401) / 密码对但学生账号已停用(403)"的差异, 会把学生账号密码变成可离线验证的预言机。 2. **学生没有任何密码接口**。`POST /students/:id/reset-password` 已删除(调用返回 404), 新建学生也不设密码(`passwordHash` 是空串占位)。