diff --git a/package.json b/package.json index 613e45a8..61dd1002 100644 --- a/package.json +++ b/package.json @@ -135,6 +135,7 @@ "csurf": "^1.2.2", "dataloader": "^2.2.3", "express": "^5.2.1", + "express-openapi-validator": "^5.3.9", "express-session": "^1.19.0", "fast-xml-parser": "^5.2.5", "fluent-ffmpeg": "^2.1.3", diff --git a/src/app.controller.ts b/src/app.controller.ts index 8ed9f082..e28dab99 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -19,6 +19,10 @@ export class AppController { }, }) getStatus() { - return { message: 'TeachLink API is running', timestamp: new Date().toISOString() }; + return { + success: true, + message: 'TeachLink API is running', + data: { timestamp: new Date().toISOString() }, + }; } } diff --git a/src/main.ts b/src/main.ts index ad3bdec5..b13d7f0e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -9,6 +9,8 @@ import { RedisStore } from 'connect-redis'; import Redis from 'ioredis'; import { AppModule } from './app.module'; +import * as OpenApiValidator from 'express-openapi-validator'; +import { join } from 'path'; import './tracing/opentelemetry'; import { CorrelationIdMiddleware } from './middleware/correlation-id'; @@ -311,6 +313,45 @@ async function bootstrapWorker(): Promise { // ========================= app.useGlobalInterceptors(new LocaleInterceptor(), new PaginationInterceptor()); + // ========================= + // OPENAPI VALIDATION + // ========================= + const apiSpecPath = join(process.cwd(), 'docs/api/openapi-spec.json'); + app.use( + OpenApiValidator.middleware({ + apiSpec: apiSpecPath, + validateRequests: true, + validateResponses: process.env.NODE_ENV !== 'production', + ignorePaths: /.*\/api\/docs.*/, // ignore swagger docs + }), + ); + + app.use((err: any, req: Request, res: Response, next: NextFunction) => { + if (err.status === 400 && err.errors) { + return res.status(400).json({ + success: false, + message: 'Validation failed', + errors: err.errors.map((e: any) => ({ + field: e.path, + message: e.message, + })), + }); + } + if ( + err.status === 500 && + err.errors && + typeof err.message === 'string' && + err.message.toLowerCase().includes('response') + ) { + logger.warn(`Response validation deviation: ${JSON.stringify(err.errors)}`); + return res.status(500).json({ + success: false, + message: 'Internal server error', + }); + } + next(err); + }); + // ========================= // SWAGGER // ========================= diff --git a/src/workers/base/base.worker.ts b/src/workers/base/base.worker.ts index e4f58cec..70f16101 100644 --- a/src/workers/base/base.worker.ts +++ b/src/workers/base/base.worker.ts @@ -1,4 +1,4 @@ -import { Logger, Inject } from '@nestjs/common'; +import { Logger } from '@nestjs/common'; import { Job } from 'bull'; import Redis from 'ioredis'; import { getSharedRedisClient } from '../../config/cache.config'; diff --git a/src/workers/orchestration/worker-orchestration.service.ts b/src/workers/orchestration/worker-orchestration.service.ts index 91275f5d..59eb3c13 100644 --- a/src/workers/orchestration/worker-orchestration.service.ts +++ b/src/workers/orchestration/worker-orchestration.service.ts @@ -1,4 +1,4 @@ -import { Injectable, Logger, OnModuleInit, OnModuleDestroy, Inject } from '@nestjs/common'; +import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { Job } from 'bull'; import { BaseWorker } from '../base/base.worker';