fix: code compliance audit and fix across all services
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"})`,
|
||||
|
||||
Reference in New Issue
Block a user