diff --git a/apps/bff/src/app/bootstrap.ts b/apps/bff/src/app/bootstrap.ts new file mode 100644 index 00000000..0f08c4ee --- /dev/null +++ b/apps/bff/src/app/bootstrap.ts @@ -0,0 +1,152 @@ +import { type INestApplication, ValidationPipe } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +import { NestFactory } from "@nestjs/core"; +import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger"; +import { Logger } from "nestjs-pino"; +import helmet from "helmet"; +import cookieParser from "cookie-parser"; +import * as express from "express"; + +import { GlobalExceptionFilter } from "@bff/core/http/http-exception.filter"; + +import { AppModule } from "../app.module"; + +export async function bootstrap(): Promise { + const app = await NestFactory.create(AppModule, { + bufferLogs: true, + // bodyParser is enabled by default in NestJS + rawBody: true, // Enable raw body access for debugging + }); + + // Set Pino as the logger + app.useLogger(app.get(Logger)); + + const configService = app.get(ConfigService); + const logger = app.get(Logger); + + // Enhanced Security Headers + app.use( + helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + styleSrc: ["'self'", "'unsafe-inline'"], + scriptSrc: ["'self'"], + imgSrc: ["'self'", "data:", "https:"], + connectSrc: ["'self'"], + fontSrc: ["'self'"], + objectSrc: ["'none'"], + mediaSrc: ["'self'"], + frameSrc: ["'none'"], + }, + }, + crossOriginEmbedderPolicy: false, + crossOriginResourcePolicy: { policy: "cross-origin" }, + }) + ); + + // Disable x-powered-by header + const expressInstance = app.getHttpAdapter().getInstance() as { + disable?: (name: string) => void; + }; + if (typeof expressInstance?.disable === "function") { + expressInstance.disable("x-powered-by"); + } + + // Configure JSON body parser with proper limits + app.use(express.json({ limit: "10mb" })); + app.use(express.urlencoded({ extended: true, limit: "10mb" })); + + // Enhanced cookie parser with security options + app.use(cookieParser()); + + // Trust proxy configuration for reverse proxies + if (configService.get("TRUST_PROXY", "false") === "true") { + const httpAdapter = app.getHttpAdapter(); + const instance = httpAdapter.getInstance() as { + set?: (key: string, value: unknown) => void; + }; + if (typeof instance?.set === "function") { + instance.set("trust proxy", 1); + } + } + + // Enhanced CORS configuration + const corsOrigin = configService.get("CORS_ORIGIN"); + app.enableCors({ + origin: corsOrigin ? [corsOrigin] : false, + credentials: true, + methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], + allowedHeaders: [ + "Origin", + "X-Requested-With", + "Content-Type", + "Accept", + "Authorization", + "X-API-Key", + ], + exposedHeaders: ["X-Total-Count", "X-Page-Count"], + maxAge: 86400, // 24 hours + }); + + // Global validation pipe with enhanced security + const exposeValidation = configService.get("EXPOSE_VALIDATION_ERRORS", "false") === "true"; + app.useGlobalPipes( + new ValidationPipe({ + transform: true, + whitelist: true, + forbidNonWhitelisted: true, + forbidUnknownValues: true, + disableErrorMessages: !exposeValidation && configService.get("NODE_ENV") === "production", + validationError: { + target: false, + value: false, + }, + }) + ); + + // Global exception filter + app.useGlobalFilters(new GlobalExceptionFilter(app.get(Logger))); + + // Global authentication guard will be registered via APP_GUARD provider in AuthModule + + // Rely on Nest's built-in shutdown hooks. External orchestrator will send signals. + app.enableShutdownHooks(); + + // Swagger documentation (only in non-production) - SETUP BEFORE GLOBAL PREFIX + if (configService.get("NODE_ENV") !== "production") { + const config = new DocumentBuilder() + .setTitle("Customer Portal API") + .setDescription("Backend for Frontend API for customer portal") + .setVersion("1.0") + .addBearerAuth() + .addCookieAuth("auth-cookie") + .addServer("http://localhost:4000", "Development server") + .addServer("https://api.yourdomain.com", "Production server") + .build(); + + const document = SwaggerModule.createDocument(app, config); + SwaggerModule.setup("docs", app, document); + } + + // API routing prefix is applied via RouterModule in AppModule for clarity and modern routing. + + const port = Number(configService.get("BFF_PORT", 4000)); + + await app.listen(port, "0.0.0.0"); + + // Enhanced startup information + logger.log(`🚀 BFF API running on: http://localhost:${port}/api`); + logger.log(`🌐 Frontend Portal: http://localhost:${configService.get("NEXT_PORT", 3000)}`); + logger.log( + `🗄️ Database: ${configService.get("DATABASE_URL", "postgresql://dev:dev@localhost:5432/portal_dev")}` + ); + logger.log(`🔗 Prisma Studio: http://localhost:5555`); + logger.log(`🔴 Redis: ${configService.get("REDIS_URL", "redis://localhost:6379")}`); + + if (configService.get("NODE_ENV") !== "production") { + logger.log(`📚 API Documentation: http://localhost:${port}/docs`); + } + + return app; +} diff --git a/apps/bff/src/main.ts b/apps/bff/src/main.ts index 2b2d8f81..47d24966 100644 --- a/apps/bff/src/main.ts +++ b/apps/bff/src/main.ts @@ -1,151 +1,45 @@ -import { NestFactory } from "@nestjs/core"; -import { ValidationPipe } from "@nestjs/common"; -import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger"; -import { ConfigService } from "@nestjs/config"; -import { Logger } from "nestjs-pino"; -import helmet from "helmet"; -import cookieParser from "cookie-parser"; -import * as express from "express"; +import { Logger, type INestApplication } from "@nestjs/common"; -import { AppModule } from "./app.module"; -import { GlobalExceptionFilter } from "@bff/core/http/http-exception.filter"; +import { bootstrap } from "./app/bootstrap"; -async function bootstrap() { - const app = await NestFactory.create(AppModule, { - bufferLogs: true, - // bodyParser is enabled by default in NestJS - rawBody: true, // Enable raw body access for debugging - }); +const logger = new Logger("Main"); +let app: INestApplication | null = null; - // Set Pino as the logger - app.useLogger(app.get(Logger)); +const signals: NodeJS.Signals[] = ["SIGINT", "SIGTERM"]; +for (const signal of signals) { + process.once(signal, async () => { + logger.log(`Received ${signal}. Closing Nest application...`); - const configService = app.get(ConfigService); - const logger = app.get(Logger); - - // Enhanced Security Headers - app.use( - helmet({ - contentSecurityPolicy: { - directives: { - defaultSrc: ["'self'"], - styleSrc: ["'self'", "'unsafe-inline'"], - scriptSrc: ["'self'"], - imgSrc: ["'self'", "data:", "https:"], - connectSrc: ["'self'"], - fontSrc: ["'self'"], - objectSrc: ["'none'"], - mediaSrc: ["'self'"], - frameSrc: ["'none'"], - }, - }, - crossOriginEmbedderPolicy: false, - crossOriginResourcePolicy: { policy: "cross-origin" }, - }) - ); - - // Disable x-powered-by header - const expressInstance = app.getHttpAdapter().getInstance() as { - disable?: (name: string) => void; - }; - if (typeof expressInstance?.disable === "function") { - expressInstance.disable("x-powered-by"); - } - - // Configure JSON body parser with proper limits - app.use(express.json({ limit: "10mb" })); - app.use(express.urlencoded({ extended: true, limit: "10mb" })); - - // Enhanced cookie parser with security options - app.use(cookieParser()); - - // Trust proxy configuration for reverse proxies - if (configService.get("TRUST_PROXY", "false") === "true") { - const httpAdapter = app.getHttpAdapter(); - const instance = httpAdapter.getInstance() as { - set?: (key: string, value: unknown) => void; - }; - if (typeof instance?.set === "function") { - instance.set("trust proxy", 1); + if (!app) { + logger.warn("Nest application not initialized. Exiting immediately."); + process.exit(0); + return; } - } - // Enhanced CORS configuration - const corsOrigin = configService.get("CORS_ORIGIN"); - app.enableCors({ - origin: corsOrigin ? [corsOrigin] : false, - credentials: true, - methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], - allowedHeaders: [ - "Origin", - "X-Requested-With", - "Content-Type", - "Accept", - "Authorization", - "X-API-Key", - ], - exposedHeaders: ["X-Total-Count", "X-Page-Count"], - maxAge: 86400, // 24 hours + try { + await app.close(); + logger.log("Nest application closed gracefully."); + } catch (error) { + const resolvedError = error as Error; + logger.error( + `Error during Nest application shutdown: ${resolvedError.message}`, + resolvedError.stack + ); + } finally { + process.exit(0); + } }); - - // Global validation pipe with enhanced security - const exposeValidation = configService.get("EXPOSE_VALIDATION_ERRORS", "false") === "true"; - app.useGlobalPipes( - new ValidationPipe({ - transform: true, - whitelist: true, - forbidNonWhitelisted: true, - forbidUnknownValues: true, - disableErrorMessages: !exposeValidation && configService.get("NODE_ENV") === "production", - validationError: { - target: false, - value: false, - }, - }) - ); - - // Global exception filter - app.useGlobalFilters(new GlobalExceptionFilter(app.get(Logger))); - - // Global authentication guard will be registered via APP_GUARD provider in AuthModule - - // Rely on Nest's built-in shutdown hooks. External orchestrator will send signals. - app.enableShutdownHooks(); - - // Swagger documentation (only in non-production) - SETUP BEFORE GLOBAL PREFIX - if (configService.get("NODE_ENV") !== "production") { - const config = new DocumentBuilder() - .setTitle("Customer Portal API") - .setDescription("Backend for Frontend API for customer portal") - .setVersion("1.0") - .addBearerAuth() - .addCookieAuth("auth-cookie") - .addServer("http://localhost:4000", "Development server") - .addServer("https://api.yourdomain.com", "Production server") - .build(); - - const document = SwaggerModule.createDocument(app, config); - SwaggerModule.setup("docs", app, document); - } - - // API routing prefix is applied via RouterModule in AppModule for clarity and modern routing. - - const port = Number(configService.get("BFF_PORT", 4000)); - - await app.listen(port, "0.0.0.0"); - - // Enhanced startup information - logger.log(`🚀 BFF API running on: http://localhost:${port}/api`); - logger.log(`🌐 Frontend Portal: http://localhost:${configService.get("NEXT_PORT", 3000)}`); - logger.log( - `🗄️ Database: ${configService.get("DATABASE_URL", "postgresql://dev:dev@localhost:5432/portal_dev")}` - ); - logger.log(`🔗 Prisma Studio: http://localhost:5555`); - logger.log(`🔴 Redis: ${configService.get("REDIS_URL", "redis://localhost:6379")}`); - - if (configService.get("NODE_ENV") !== "production") { - logger.log(`📚 API Documentation: http://localhost:${port}/docs`); - } } -void bootstrap(); +void bootstrap() + .then((startedApp) => { + app = startedApp; + }) + .catch((error) => { + const resolvedError = error as Error; + logger.error( + `Failed to bootstrap the Nest application: ${resolvedError.message}`, + resolvedError.stack + ); + process.exit(1); + });