新增 3 个业务域(10 RPC): - ElectiveService: 选修课列表/学生选课记录/选课/退课(含容量与重复校验) - LessonPlanService: 教师备课列表/学生备课列表(仅 published)/详情 - CoursePlanService: 学生课程计划列表/详情 - KnowledgeGraphService.GetKnowledgePath: 与 GetLearningPath 同实现 新增 4 张 MySQL 表(elective_courses/selections/lesson_plans/course_plans),含完整索引。 新增 11 个权限点,覆盖 admin/teacher/student/parent 四角色。 proto 由 4 Service/22 RPC 扩展至 7 Service/32 RPC,v1 全部 RPC 保持向后兼容。 修复 logger.ts pino 导入: default import 在 NodeNext ESM 下不可调用, 改用 named import(与 iam/msg/core-edu 对齐)。 Docker 本地测试全部通过(HTTP + gRPC 双协议),健康检查、 Elective/LessonPlan/CoursePlan CRUD、4 个新 gRPC Service 全部验证通过。 nextstep-v2.md 已创建,记录上下游依赖与 6 项联调待办。
147 lines
5.7 KiB
TypeScript
147 lines
5.7 KiB
TypeScript
import {
|
|
Injectable,
|
|
CanActivate,
|
|
ExecutionContext,
|
|
SetMetadata,
|
|
} from "@nestjs/common";
|
|
import { Reflector } from "@nestjs/core";
|
|
import { PermissionDeniedError } from "../shared/errors/application-error.js";
|
|
import type { AuthenticatedRequest } from "./auth.middleware.js";
|
|
|
|
export const Permissions = {
|
|
CONTENT_TEXTBOOK_CREATE: "CONTENT_TEXTBOOK_CREATE" as const,
|
|
CONTENT_TEXTBOOK_READ: "CONTENT_TEXTBOOK_READ" as const,
|
|
CONTENT_TEXTBOOK_UPDATE: "CONTENT_TEXTBOOK_UPDATE" as const,
|
|
CONTENT_TEXTBOOK_DELETE: "CONTENT_TEXTBOOK_DELETE" as const,
|
|
CONTENT_CHAPTER_CREATE: "CONTENT_CHAPTER_CREATE" as const,
|
|
CONTENT_CHAPTER_READ: "CONTENT_CHAPTER_READ" as const,
|
|
CONTENT_CHAPTER_UPDATE: "CONTENT_CHAPTER_UPDATE" as const,
|
|
CONTENT_CHAPTER_DELETE: "CONTENT_CHAPTER_DELETE" as const,
|
|
CONTENT_QUESTION_CREATE: "CONTENT_QUESTION_CREATE" as const,
|
|
CONTENT_QUESTION_READ: "CONTENT_QUESTION_READ" as const,
|
|
CONTENT_QUESTION_UPDATE: "CONTENT_QUESTION_UPDATE" as const,
|
|
CONTENT_QUESTION_DELETE: "CONTENT_QUESTION_DELETE" as const,
|
|
CONTENT_KNOWLEDGE_POINT_CREATE: "CONTENT_KNOWLEDGE_POINT_CREATE" as const,
|
|
CONTENT_KNOWLEDGE_POINT_READ: "CONTENT_KNOWLEDGE_POINT_READ" as const,
|
|
CONTENT_KNOWLEDGE_POINT_UPDATE: "CONTENT_KNOWLEDGE_POINT_UPDATE" as const,
|
|
CONTENT_KNOWLEDGE_POINT_DELETE: "CONTENT_KNOWLEDGE_POINT_DELETE" as const,
|
|
// v2 新增:选修课 / 备课计划 / 课程计划
|
|
CONTENT_ELECTIVE_CREATE: "CONTENT_ELECTIVE_CREATE" as const,
|
|
CONTENT_ELECTIVE_READ: "CONTENT_ELECTIVE_READ" as const,
|
|
CONTENT_ELECTIVE_SELECT: "CONTENT_ELECTIVE_SELECT" as const,
|
|
CONTENT_LESSON_PLAN_CREATE: "CONTENT_LESSON_PLAN_CREATE" as const,
|
|
CONTENT_LESSON_PLAN_READ: "CONTENT_LESSON_PLAN_READ" as const,
|
|
CONTENT_LESSON_PLAN_UPDATE: "CONTENT_LESSON_PLAN_UPDATE" as const,
|
|
CONTENT_LESSON_PLAN_DELETE: "CONTENT_LESSON_PLAN_DELETE" as const,
|
|
CONTENT_COURSE_PLAN_CREATE: "CONTENT_COURSE_PLAN_CREATE" as const,
|
|
CONTENT_COURSE_PLAN_READ: "CONTENT_COURSE_PLAN_READ" as const,
|
|
CONTENT_COURSE_PLAN_UPDATE: "CONTENT_COURSE_PLAN_UPDATE" as const,
|
|
CONTENT_COURSE_PLAN_DELETE: "CONTENT_COURSE_PLAN_DELETE" as const,
|
|
} as const;
|
|
|
|
export type Permission = (typeof Permissions)[keyof typeof Permissions];
|
|
|
|
export const PERMISSIONS_KEY = "permissions";
|
|
export const RequirePermission = (...permissions: Permission[]) =>
|
|
SetMetadata(PERMISSIONS_KEY, permissions);
|
|
|
|
const ROLE_PERMISSIONS: Record<string, Permission[]> = {
|
|
admin: [
|
|
Permissions.CONTENT_TEXTBOOK_CREATE,
|
|
Permissions.CONTENT_TEXTBOOK_READ,
|
|
Permissions.CONTENT_TEXTBOOK_UPDATE,
|
|
Permissions.CONTENT_TEXTBOOK_DELETE,
|
|
Permissions.CONTENT_CHAPTER_CREATE,
|
|
Permissions.CONTENT_CHAPTER_READ,
|
|
Permissions.CONTENT_CHAPTER_UPDATE,
|
|
Permissions.CONTENT_CHAPTER_DELETE,
|
|
Permissions.CONTENT_QUESTION_CREATE,
|
|
Permissions.CONTENT_QUESTION_READ,
|
|
Permissions.CONTENT_QUESTION_UPDATE,
|
|
Permissions.CONTENT_QUESTION_DELETE,
|
|
Permissions.CONTENT_KNOWLEDGE_POINT_CREATE,
|
|
Permissions.CONTENT_KNOWLEDGE_POINT_READ,
|
|
Permissions.CONTENT_KNOWLEDGE_POINT_UPDATE,
|
|
Permissions.CONTENT_KNOWLEDGE_POINT_DELETE,
|
|
Permissions.CONTENT_ELECTIVE_CREATE,
|
|
Permissions.CONTENT_ELECTIVE_READ,
|
|
Permissions.CONTENT_ELECTIVE_SELECT,
|
|
Permissions.CONTENT_LESSON_PLAN_CREATE,
|
|
Permissions.CONTENT_LESSON_PLAN_READ,
|
|
Permissions.CONTENT_LESSON_PLAN_UPDATE,
|
|
Permissions.CONTENT_LESSON_PLAN_DELETE,
|
|
Permissions.CONTENT_COURSE_PLAN_CREATE,
|
|
Permissions.CONTENT_COURSE_PLAN_READ,
|
|
Permissions.CONTENT_COURSE_PLAN_UPDATE,
|
|
Permissions.CONTENT_COURSE_PLAN_DELETE,
|
|
],
|
|
teacher: [
|
|
Permissions.CONTENT_TEXTBOOK_READ,
|
|
Permissions.CONTENT_CHAPTER_CREATE,
|
|
Permissions.CONTENT_CHAPTER_READ,
|
|
Permissions.CONTENT_CHAPTER_UPDATE,
|
|
Permissions.CONTENT_QUESTION_CREATE,
|
|
Permissions.CONTENT_QUESTION_READ,
|
|
Permissions.CONTENT_QUESTION_UPDATE,
|
|
Permissions.CONTENT_KNOWLEDGE_POINT_CREATE,
|
|
Permissions.CONTENT_KNOWLEDGE_POINT_READ,
|
|
Permissions.CONTENT_KNOWLEDGE_POINT_UPDATE,
|
|
Permissions.CONTENT_ELECTIVE_CREATE,
|
|
Permissions.CONTENT_ELECTIVE_READ,
|
|
Permissions.CONTENT_LESSON_PLAN_CREATE,
|
|
Permissions.CONTENT_LESSON_PLAN_READ,
|
|
Permissions.CONTENT_LESSON_PLAN_UPDATE,
|
|
Permissions.CONTENT_COURSE_PLAN_READ,
|
|
],
|
|
student: [
|
|
Permissions.CONTENT_TEXTBOOK_READ,
|
|
Permissions.CONTENT_CHAPTER_READ,
|
|
Permissions.CONTENT_QUESTION_READ,
|
|
Permissions.CONTENT_KNOWLEDGE_POINT_READ,
|
|
Permissions.CONTENT_ELECTIVE_READ,
|
|
Permissions.CONTENT_ELECTIVE_SELECT,
|
|
Permissions.CONTENT_LESSON_PLAN_READ,
|
|
Permissions.CONTENT_COURSE_PLAN_READ,
|
|
],
|
|
parent: [
|
|
Permissions.CONTENT_TEXTBOOK_READ,
|
|
Permissions.CONTENT_CHAPTER_READ,
|
|
Permissions.CONTENT_KNOWLEDGE_POINT_READ,
|
|
Permissions.CONTENT_LESSON_PLAN_READ,
|
|
Permissions.CONTENT_COURSE_PLAN_READ,
|
|
Permissions.CONTENT_ELECTIVE_READ,
|
|
],
|
|
};
|
|
|
|
@Injectable()
|
|
export class PermissionGuard implements CanActivate {
|
|
constructor(private readonly reflector: Reflector) {}
|
|
|
|
canActivate(context: ExecutionContext): boolean {
|
|
if (process.env.DEV_MODE === "true") {
|
|
return true;
|
|
}
|
|
|
|
const requiredPermissions = this.reflector.getAllAndOverride<Permission[]>(
|
|
PERMISSIONS_KEY,
|
|
[context.getHandler(), context.getClass()],
|
|
);
|
|
|
|
if (!requiredPermissions || requiredPermissions.length === 0) {
|
|
return true;
|
|
}
|
|
|
|
const request = context.switchToHttp().getRequest<AuthenticatedRequest>();
|
|
const roles = request.userRoles ?? [];
|
|
|
|
for (const role of roles) {
|
|
const perms = ROLE_PERMISSIONS[role];
|
|
if (perms && requiredPermissions.some((p) => perms.includes(p))) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
throw new PermissionDeniedError(requiredPermissions.join(", "));
|
|
}
|
|
}
|