feat(teacher-bff): 完整实现 teacher-bff GraphQL 聚合层

包含 clients/graphql/middleware、health probes、shared-ts contracts 等
This commit is contained in:
SpecialX
2026-07-10 19:10:07 +08:00
parent b82593aac2
commit 99155a5ea1
37 changed files with 2862 additions and 324 deletions

View File

@@ -0,0 +1,123 @@
// AuthorizationGuard 越权防御B4 + president §2.7/§2.9
// P2 实现DEV_MODE 放行 + 日志告警;生产拒绝(保守策略)
// P3+ 替换为真实 gRPC 校验core-edu.GetClassesByTeacher 比对 teacherId
//
// 3 类越权错误码president §2.7
// - BFF_TEACHER_UNAUTHORIZED (401): x-user-id 缺失或无效
// - BFF_TEACHER_FORBIDDEN_RESOURCE (403): teacherId 与资源无归属关系(场景 A
// - BFF_TEACHER_IDENTITY_MISMATCH (403): JWT teacherId 与 body teacherId 不一致(场景 B
import { Injectable, Inject } from "@nestjs/common";
import { env } from "../config/env.js";
import { logger } from "../shared/observability/logger.js";
import {
ForbiddenResourceError,
IdentityMismatchError,
} from "../shared/errors/application-error.js";
import { IAM_CLIENT } from "../clients/iam/iam-client.interface.js";
import type { IamClient } from "../clients/iam/iam-client.interface.js";
/**
* AuthorizationGuard 接口president §2.9
* P2: DEV_MODE 放行P3+: 真实 gRPC 校验
*/
export interface AuthorizationGuard {
/** 校验教师是否有权访问指定班级(场景 AteacherId 与资源归属) */
canAccessClass(userId: string, classId: string): Promise<boolean>;
/** 校验教师是否有权访问指定考试 */
canAccessExam(userId: string, examId: string): Promise<boolean>;
/** 校验教师是否有权访问指定作业 */
canAccessHomework(userId: string, homeworkId: string): Promise<boolean>;
/** 校验身份一致性(场景 BJWT teacherId 与 body teacherId */
assertIdentityMatch(jwtTeacherId: string, bodyTeacherId: string): void;
}
export const AUTHORIZATION_GUARD = Symbol("AUTHORIZATION_GUARD");
/**
* P2 实现DEV_MODE 放行 + 日志告警
* 生产环境DEV_MODE=false对所有资源校验返回 false保守拒绝
* P3+ 替换为 AuthorizationGuardGrpcImpl接入 core-edu gRPC 真实校验)
*/
@Injectable()
export class AuthorizationGuardImpl implements AuthorizationGuard {
constructor(@Inject(IAM_CLIENT) private readonly iam: IamClient) {}
async canAccessClass(userId: string, classId: string): Promise<boolean> {
if (env.TEACHER_BFF_DEV_MODE) {
logger.warn(
{ userId, classId, devMode: true },
"AuthorizationGuard DEV_MODE: access allowed without real check",
);
return true;
}
// P2 生产模式保守拒绝P3+ 接入 core-edu.GetClassesByTeacher 真实校验)
logger.error(
{ userId, classId, devMode: false },
"AuthorizationGuard production mode not implemented yet (P3+)",
);
throw new ForbiddenResourceError(
`User ${userId} has no access to class ${classId}`,
{ userId, classId, reason: "production_check_not_implemented" },
);
}
async canAccessExam(userId: string, examId: string): Promise<boolean> {
if (env.TEACHER_BFF_DEV_MODE) {
logger.warn(
{ userId, examId, devMode: true },
"AuthorizationGuard DEV_MODE: access allowed without real check",
);
return true;
}
logger.error(
{ userId, examId, devMode: false },
"AuthorizationGuard production mode not implemented yet (P3+)",
);
throw new ForbiddenResourceError(
`User ${userId} has no access to exam ${examId}`,
{ userId, examId, reason: "production_check_not_implemented" },
);
}
async canAccessHomework(
userId: string,
homeworkId: string,
): Promise<boolean> {
if (env.TEACHER_BFF_DEV_MODE) {
logger.warn(
{ userId, homeworkId, devMode: true },
"AuthorizationGuard DEV_MODE: access allowed without real check",
);
return true;
}
logger.error(
{ userId, homeworkId, devMode: false },
"AuthorizationGuard production mode not implemented yet (P3+)",
);
throw new ForbiddenResourceError(
`User ${userId} has no access to homework ${homeworkId}`,
{ userId, homeworkId, reason: "production_check_not_implemented" },
);
}
/**
* 校验身份一致性(场景 Bpresident §2.7
* JWT 中的 teacherId 必须与请求 body 中的 teacherId 一致
* 此校验在所有模式DEV_MODE / 生产)下都强制执行
*/
assertIdentityMatch(jwtTeacherId: string, bodyTeacherId: string): void {
if (jwtTeacherId !== bodyTeacherId) {
logger.error(
{ jwtTeacherId, bodyTeacherId },
"Identity mismatch: JWT teacherId != body teacherId",
);
throw new IdentityMismatchError(
`JWT teacherId (${jwtTeacherId}) does not match body teacherId (${bodyTeacherId})`,
{ jwtTeacherId, bodyTeacherId },
);
}
}
}

View File

@@ -0,0 +1,19 @@
// 中间件模块AuthorizationGuard 越权防御B4 + president §2.9
import { Module } from "@nestjs/common";
import { ClientsModule } from "../clients/clients.module.js";
import {
AUTHORIZATION_GUARD,
AuthorizationGuardImpl,
} from "./authorization.guard.js";
@Module({
imports: [ClientsModule],
providers: [
{
provide: AUTHORIZATION_GUARD,
useClass: AuthorizationGuardImpl,
},
],
exports: [AUTHORIZATION_GUARD],
})
export class MiddlewareModule {}