fix: code compliance audit and fix across all services
Some checks failed
CI / quality-ts (push) Failing after 48s
CI / quality-go (push) Failing after 4s
CI / quality-proto (push) Failing after 2s
CI / deploy (push) Has been skipped

NestJS (6 services): implement @RequirePermission decorator with
SetMetadata+Reflector, register APP_GUARD globally, fix as assertions
to type guards, add explicit return types, fix import type for express,
fix /metrics implicit any, replace native Error with ApplicationError,
remove typeorm remnants, register LifecycleService.

teacher-bff: add logger, ApplicationError, GlobalErrorFilter, forward
real userId to downstream, log downstream failures, migrate health
controller to shared/health.

Go (2 services): interface to any, doc comments, CORS dev whitelist,
JWT secret fail-fast, push-gateway internal API auth, metrics and
readyz endpoints, remove dead code.

Python (2 services): lifespan return type, dev_mode to bool, data-ana
APIRouter, ai POST body model, ClickHouse async wrapping.
This commit is contained in:
SpecialX
2026-07-09 17:28:27 +08:00
parent b53a486c6e
commit 0a71b02e04
93 changed files with 5775 additions and 608 deletions

View File

@@ -1,10 +1,17 @@
import { Module } from "@nestjs/common";
import { APP_GUARD } from "@nestjs/core";
import { ExamsModule } from "./exams/exams.module.js";
import { HomeworkModule } from "./homework/homework.module.js";
import { GradesModule } from "./grades/grades.module.js";
import { HealthModule } from "./shared/health/health.module.js";
import { PermissionGuard } from "./middleware/permission.guard.js";
import { LifecycleService } from "./shared/lifecycle/lifecycle.service.js";
@Module({
imports: [ExamsModule, HomeworkModule, GradesModule, HealthModule],
providers: [
{ provide: APP_GUARD, useClass: PermissionGuard },
LifecycleService,
],
})
export class AppModule {}

View File

@@ -8,23 +8,32 @@ import {
Put,
Req,
} from "@nestjs/common";
import type { Request } from "express";
import {
ExamsService,
type CreateExamInput,
type UpdateExamInput,
} from "./exams.service.js";
import {
Permissions,
RequirePermission,
} from "../middleware/permission.guard.js";
import type { AuthenticatedRequest } from "../middleware/auth.middleware.js";
import { UnauthorizedError } from "../shared/errors/application-error.js";
@Controller("exams")
export class ExamsController {
constructor(private readonly examsService: ExamsService) {}
@Post()
@RequirePermission(Permissions.EXAM_CREATE)
async create(
@Body() body: CreateExamInput,
@Req() req: Request,
@Req() req: AuthenticatedRequest,
): Promise<{ success: true; data: { id: string } }> {
const userId = req.headers["x-user-id"] as string;
const userId = req.userId;
if (!userId) {
throw new UnauthorizedError("Missing x-user-id header");
}
const result = await this.examsService.createExam({
...body,
createdBy: userId,
@@ -33,6 +42,7 @@ export class ExamsController {
}
@Get(":id")
@RequirePermission(Permissions.EXAM_READ)
async findOne(@Param("id") id: string): Promise<{
success: true;
data: Awaited<ReturnType<ExamsService["getExam"]>>;
@@ -42,6 +52,7 @@ export class ExamsController {
}
@Get("class/:classId")
@RequirePermission(Permissions.EXAM_READ)
async listByClass(@Param("classId") classId: string): Promise<{
success: true;
data: Awaited<ReturnType<ExamsService["listExamsByClass"]>>;
@@ -51,6 +62,7 @@ export class ExamsController {
}
@Put(":id")
@RequirePermission(Permissions.EXAM_UPDATE)
async update(
@Param("id") id: string,
@Body() body: UpdateExamInput,
@@ -60,6 +72,7 @@ export class ExamsController {
}
@Delete(":id")
@RequirePermission(Permissions.EXAM_DELETE)
async remove(
@Param("id") id: string,
): Promise<{ success: true; data: { success: true } }> {

View File

@@ -1,17 +1,26 @@
import { Body, Controller, Get, Param, Post, Req } from "@nestjs/common";
import type { Request } from "express";
import { GradesService, type RecordGradeInput } from "./grades.service.js";
import {
Permissions,
RequirePermission,
} from "../middleware/permission.guard.js";
import type { AuthenticatedRequest } from "../middleware/auth.middleware.js";
import { UnauthorizedError } from "../shared/errors/application-error.js";
@Controller("grades")
export class GradesController {
constructor(private readonly gradesService: GradesService) {}
@Post()
@RequirePermission(Permissions.GRADE_CREATE)
async record(
@Body() body: RecordGradeInput,
@Req() req: Request,
@Req() req: AuthenticatedRequest,
): Promise<{ success: true; data: { id: string } }> {
const userId = req.headers["x-user-id"] as string;
const userId = req.userId;
if (!userId) {
throw new UnauthorizedError("Missing x-user-id header");
}
const result = await this.gradesService.recordGrade({
...body,
gradedBy: userId,
@@ -20,6 +29,7 @@ export class GradesController {
}
@Get(":id")
@RequirePermission(Permissions.GRADE_READ)
async findOne(@Param("id") id: string): Promise<{
success: true;
data: Awaited<ReturnType<GradesService["getGrade"]>>;
@@ -29,6 +39,7 @@ export class GradesController {
}
@Get("student/:studentId")
@RequirePermission(Permissions.GRADE_READ)
async listByStudent(@Param("studentId") studentId: string): Promise<{
success: true;
data: Awaited<ReturnType<GradesService["listByStudent"]>>;
@@ -38,6 +49,7 @@ export class GradesController {
}
@Get("exam/:examId")
@RequirePermission(Permissions.GRADE_READ)
async listByExam(@Param("examId") examId: string): Promise<{
success: true;
data: Awaited<ReturnType<GradesService["listByExam"]>>;
@@ -47,6 +59,7 @@ export class GradesController {
}
@Get("homework/:homeworkId")
@RequirePermission(Permissions.GRADE_READ)
async listByHomework(@Param("homeworkId") homeworkId: string): Promise<{
success: true;
data: Awaited<ReturnType<GradesService["listByHomework"]>>;

View File

@@ -1,20 +1,29 @@
import { Body, Controller, Get, Param, Post, Req } from "@nestjs/common";
import type { Request } from "express";
import {
HomeworkService,
type AssignHomeworkInput,
} from "./homework.service.js";
import {
Permissions,
RequirePermission,
} from "../middleware/permission.guard.js";
import type { AuthenticatedRequest } from "../middleware/auth.middleware.js";
import { UnauthorizedError } from "../shared/errors/application-error.js";
@Controller("homework")
export class HomeworkController {
constructor(private readonly homeworkService: HomeworkService) {}
@Post()
@RequirePermission(Permissions.HOMEWORK_CREATE)
async assign(
@Body() body: AssignHomeworkInput,
@Req() req: Request,
@Req() req: AuthenticatedRequest,
): Promise<{ success: true; data: { id: string } }> {
const userId = req.headers["x-user-id"] as string;
const userId = req.userId;
if (!userId) {
throw new UnauthorizedError("Missing x-user-id header");
}
const result = await this.homeworkService.assignHomework({
...body,
createdBy: userId,
@@ -23,6 +32,7 @@ export class HomeworkController {
}
@Get(":id")
@RequirePermission(Permissions.HOMEWORK_READ)
async findOne(@Param("id") id: string): Promise<{
success: true;
data: Awaited<ReturnType<HomeworkService["getHomework"]>>;
@@ -32,6 +42,7 @@ export class HomeworkController {
}
@Get("class/:classId")
@RequirePermission(Permissions.HOMEWORK_READ)
async listByClass(@Param("classId") classId: string): Promise<{
success: true;
data: Awaited<ReturnType<HomeworkService["listByClass"]>>;
@@ -41,6 +52,7 @@ export class HomeworkController {
}
@Post(":id/submit")
@RequirePermission(Permissions.HOMEWORK_SUBMIT)
async submit(
@Param("id") id: string,
): Promise<{ success: true; data: { success: true } }> {

View File

@@ -7,6 +7,7 @@ import { GlobalErrorFilter } from "./shared/errors/global-error.filter.js";
import { initTracer, shutdownTracer } from "./shared/observability/tracer.js";
import { logger } from "./shared/observability/logger.js";
import { registry } from "./shared/observability/metrics.js";
import type { Request, Response } from "express";
async function bootstrap(): Promise<void> {
initTracer();
@@ -16,8 +17,7 @@ async function bootstrap(): Promise<void> {
app.enableShutdownHooks();
// Prometheus 指标端点:不鉴权,供 Prometheus 抓取。
// 返回 register.metrics()Promise<string>,含 Content-Type text/plain; version=0.0.4; charset=utf-8
app.getHttpAdapter().get("/metrics", async (req, res) => {
app.getHttpAdapter().get("/metrics", async (_req: Request, res: Response) => {
res.set("Content-Type", registry.contentType);
res.end(await registry.metrics());
});

View File

@@ -2,39 +2,29 @@ import {
Injectable,
NestMiddleware,
UnauthorizedException,
} from '@nestjs/common';
import type { Request, Response, NextFunction } from 'express';
export interface AuthenticatedUser {
id: string;
role: string;
permissions: string[];
}
} from "@nestjs/common";
import type { Request, Response, NextFunction } from "express";
export interface AuthenticatedRequest extends Request {
user?: AuthenticatedUser;
userId?: string;
userRoles?: string[];
}
@Injectable()
export class AuthMiddleware implements NestMiddleware {
use(req: AuthenticatedRequest, _res: Response, next: NextFunction): void {
const userId = req.headers['x-user-id'] as string | undefined;
const role = req.headers['x-user-role'] as string | undefined;
const permissionsHeader = req.headers['x-user-permissions'] as
| string
| undefined;
const userIdHeader = req.headers["x-user-id"];
const userId = typeof userIdHeader === "string" ? userIdHeader : undefined;
const rolesHeaderRaw = req.headers["x-user-roles"];
const rolesHeader =
typeof rolesHeaderRaw === "string" ? rolesHeaderRaw : undefined;
if (!userId || !role) {
throw new UnauthorizedException(
'Missing authentication headers (x-user-id, x-user-role)',
);
if (!userId) {
throw new UnauthorizedException("Missing x-user-id header");
}
req.user = {
id: userId,
role,
permissions: permissionsHeader ? permissionsHeader.split(',') : [],
};
req.userId = userId;
req.userRoles = rolesHeader ? rolesHeader.split(",") : [];
next();
}
}

View File

@@ -1,48 +1,92 @@
import {
Injectable,
CanActivate,
ExecutionContext,
Injectable,
ForbiddenException,
} from '@nestjs/common';
import type { AuthenticatedRequest } from './auth.middleware.js';
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 = {
EXAM_CREATE: 'exam:create',
EXAM_READ: 'exam:read',
EXAM_UPDATE: 'exam:update',
EXAM_DELETE: 'exam:delete',
HOMEWORK_CREATE: 'homework:create',
HOMEWORK_READ: 'homework:read',
HOMEWORK_UPDATE: 'homework:update',
HOMEWORK_DELETE: 'homework:delete',
HOMEWORK_GRADE: 'homework:grade',
HOMEWORK_SUBMIT: 'homework:submit',
GRADE_CREATE: 'grade:create',
GRADE_READ: 'grade:read',
GRADE_UPDATE: 'grade:update',
GRADE_DELETE: 'grade:delete',
CLASS_MANAGE: 'class:manage',
CLASS_READ: 'class:read',
CLASS_TRANSFER: 'class:transfer',
EXAM_CREATE: "CORE_EDU_EXAM_CREATE" as const,
EXAM_READ: "CORE_EDU_EXAM_READ" as const,
EXAM_UPDATE: "CORE_EDU_EXAM_UPDATE" as const,
EXAM_DELETE: "CORE_EDU_EXAM_DELETE" as const,
HOMEWORK_CREATE: "CORE_EDU_HOMEWORK_CREATE" as const,
HOMEWORK_READ: "CORE_EDU_HOMEWORK_READ" as const,
HOMEWORK_UPDATE: "CORE_EDU_HOMEWORK_UPDATE" as const,
HOMEWORK_SUBMIT: "CORE_EDU_HOMEWORK_SUBMIT" as const,
GRADE_CREATE: "CORE_EDU_GRADE_CREATE" as const,
GRADE_READ: "CORE_EDU_GRADE_READ" 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.EXAM_CREATE,
Permissions.EXAM_READ,
Permissions.EXAM_UPDATE,
Permissions.EXAM_DELETE,
Permissions.HOMEWORK_CREATE,
Permissions.HOMEWORK_READ,
Permissions.HOMEWORK_UPDATE,
Permissions.HOMEWORK_SUBMIT,
Permissions.GRADE_CREATE,
Permissions.GRADE_READ,
],
teacher: [
Permissions.EXAM_CREATE,
Permissions.EXAM_READ,
Permissions.EXAM_UPDATE,
Permissions.HOMEWORK_CREATE,
Permissions.HOMEWORK_READ,
Permissions.HOMEWORK_UPDATE,
Permissions.HOMEWORK_SUBMIT,
Permissions.GRADE_CREATE,
Permissions.GRADE_READ,
],
student: [
Permissions.EXAM_READ,
Permissions.HOMEWORK_READ,
Permissions.HOMEWORK_SUBMIT,
Permissions.GRADE_READ,
],
};
@Injectable()
export class PermissionGuard implements CanActivate {
constructor(private readonly requiredPermission: Permission) {}
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 user = request.user;
if (!user) {
throw new ForbiddenException('User not authenticated');
const roles = request.userRoles ?? [];
for (const role of roles) {
const perms = ROLE_PERMISSIONS[role];
if (perms && requiredPermissions.some((p) => perms.includes(p))) {
return true;
}
}
if (!user.permissions.includes(this.requiredPermission)) {
throw new ForbiddenException(
`Missing permission: ${this.requiredPermission}`,
);
}
return true;
throw new PermissionDeniedError(requiredPermissions.join(", "));
}
}

View File

@@ -1,25 +1,45 @@
export enum CoreEduErrorCode {
VALIDATION_ERROR = 'CORE_EDU_VALIDATION_ERROR',
NOT_FOUND = 'CORE_EDU_NOT_FOUND',
UNAUTHORIZED = 'CORE_EDU_UNAUTHORIZED',
FORBIDDEN = 'CORE_EDU_FORBIDDEN',
CONFLICT = 'CORE_EDU_CONFLICT',
INTERNAL_ERROR = 'CORE_EDU_INTERNAL_ERROR',
EXAM_NOT_FOUND = 'CORE_EDU_EXAM_NOT_FOUND',
HOMEWORK_NOT_FOUND = 'CORE_EDU_HOMEWORK_NOT_FOUND',
GRADE_NOT_FOUND = 'CORE_EDU_GRADE_NOT_FOUND',
OUTBOX_PUBLISH_FAILED = 'CORE_EDU_OUTBOX_PUBLISH_FAILED',
VALIDATION_ERROR = "CORE_EDU_VALIDATION_ERROR",
NOT_FOUND = "CORE_EDU_NOT_FOUND",
UNAUTHORIZED = "CORE_EDU_UNAUTHORIZED",
FORBIDDEN = "CORE_EDU_FORBIDDEN",
CONFLICT = "CORE_EDU_CONFLICT",
INTERNAL_ERROR = "CORE_EDU_INTERNAL_ERROR",
EXAM_NOT_FOUND = "CORE_EDU_EXAM_NOT_FOUND",
HOMEWORK_NOT_FOUND = "CORE_EDU_HOMEWORK_NOT_FOUND",
GRADE_NOT_FOUND = "CORE_EDU_GRADE_NOT_FOUND",
OUTBOX_PUBLISH_FAILED = "CORE_EDU_OUTBOX_PUBLISH_FAILED",
}
export class ApplicationError extends Error {
readonly code: CoreEduErrorCode;
readonly statusCode: number;
readonly details?: unknown;
traceId?: string;
constructor(
public readonly code: CoreEduErrorCode,
code: CoreEduErrorCode,
message: string,
public readonly statusCode: number = 500,
public readonly details?: unknown,
statusCode: number = 500,
details?: unknown,
) {
super(message);
this.name = 'ApplicationError';
this.name = this.constructor.name;
this.code = code;
this.statusCode = statusCode;
this.details = details;
}
toJSON(): Record<string, unknown> {
return {
success: false,
error: {
code: this.code,
message: this.message,
details: this.details,
traceId: this.traceId,
},
};
}
}
@@ -36,17 +56,25 @@ export class NotFoundError extends ApplicationError {
}
export class UnauthorizedError extends ApplicationError {
constructor(message: string = 'Unauthorized') {
constructor(message: string = "Unauthorized") {
super(CoreEduErrorCode.UNAUTHORIZED, message, 401);
}
}
export class ForbiddenError extends ApplicationError {
constructor(message: string = 'Forbidden') {
constructor(message: string = "Forbidden") {
super(CoreEduErrorCode.FORBIDDEN, message, 403);
}
}
export class PermissionDeniedError extends ApplicationError {
constructor(permission: string) {
super(CoreEduErrorCode.FORBIDDEN, `Permission denied: ${permission}`, 403, {
permission,
});
}
}
export class ConflictError extends ApplicationError {
constructor(message: string, details?: unknown) {
super(CoreEduErrorCode.CONFLICT, message, 409, details);
@@ -54,7 +82,7 @@ export class ConflictError extends ApplicationError {
}
export class InternalError extends ApplicationError {
constructor(message: string = 'Internal server error', details?: unknown) {
constructor(message: string = "Internal server error", details?: unknown) {
super(CoreEduErrorCode.INTERNAL_ERROR, message, 500, details);
}
}

View File

@@ -1,63 +1,95 @@
import {
ExceptionFilter,
Catch,
ExceptionFilter,
ArgumentsHost,
HttpException,
HttpStatus,
} from '@nestjs/common';
import { ZodError } from 'zod';
import { ApplicationError, CoreEduErrorCode } from './application-error.js';
import { logger } from '../observability/logger.js';
Logger,
} from "@nestjs/common";
import type { Request, Response } from "express";
import { ZodError } from "zod";
import { ApplicationError } from "./application-error.js";
@Catch()
export class GlobalErrorFilter implements ExceptionFilter {
private readonly logger = new Logger(GlobalErrorFilter.name);
catch(exception: unknown, host: ArgumentsHost): void {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
let statusCode = HttpStatus.INTERNAL_SERVER_ERROR;
let code = CoreEduErrorCode.INTERNAL_ERROR;
let message = 'Internal server error';
let details: unknown;
const traceIdHeader = request.headers["x-request-id"];
const traceId =
typeof traceIdHeader === "string" ? traceIdHeader : "unknown";
let statusCode = 500;
let body: Record<string, unknown>;
if (exception instanceof ApplicationError) {
exception.traceId = traceId;
statusCode = exception.statusCode;
code = exception.code;
message = exception.message;
details = exception.details;
body = exception.toJSON();
} else if (exception instanceof ZodError) {
statusCode = HttpStatus.BAD_REQUEST;
code = CoreEduErrorCode.VALIDATION_ERROR;
message = 'Validation failed';
details = exception.flatten().fieldErrors;
statusCode = 400;
body = {
success: false,
error: {
code: "CORE_EDU_VALIDATION_ERROR",
message: "Validation failed",
details: exception.flatten(),
traceId,
},
};
} else if (exception instanceof HttpException) {
statusCode = exception.getStatus();
const resp = exception.getResponse();
message =
typeof resp === 'string'
? resp
: (resp as { message?: string }).message ?? exception.message;
} else if (exception instanceof Error) {
message = exception.message;
const res = exception.getResponse();
const message = this.extractHttpMessage(res, exception);
body = {
success: false,
error: {
code: "HTTP_ERROR",
message,
traceId,
},
};
} else {
this.logger.error(
`Unhandled exception: ${exception}`,
exception instanceof Error ? exception.stack : undefined,
);
body = {
success: false,
error: {
code: "INTERNAL_ERROR",
message: "An unexpected error occurred",
traceId,
},
};
}
logger.error(
this.logger.error(
{
err: exception,
path: request.url,
method: request.method,
code,
},
`Request failed: ${message}`,
`Request failed: ${request.method} ${request.url}`,
);
response.status(statusCode).json({
code,
message,
details,
timestamp: new Date().toISOString(),
path: request.url,
});
response.status(statusCode).json(body);
}
private extractHttpMessage(
res: string | object,
exception: HttpException,
): string {
if (typeof res === "string") {
return res;
}
if (res && typeof res === "object" && "message" in res) {
const msg = (res as { message: unknown }).message;
return typeof msg === "string" ? msg : exception.message;
}
return exception.message;
}
}

View File

@@ -1,12 +1,21 @@
import { Injectable, Logger } from "@nestjs/common";
import {
Injectable,
Logger,
OnApplicationShutdown,
OnModuleInit,
} from "@nestjs/common";
import { closeDb } from "../../config/database.js";
const SERVICE_NAME = "core-edu";
@Injectable()
export class LifecycleService {
export class LifecycleService implements OnModuleInit, OnApplicationShutdown {
private readonly logger = new Logger(LifecycleService.name);
onModuleInit(): void {
this.logger.log(`service ${SERVICE_NAME} module initialized`);
}
async onApplicationShutdown(signal?: string): Promise<void> {
this.logger.log(
`service ${SERVICE_NAME} shutting down (signal=${signal ?? "unknown"})`,