From 765d4507c987741195bf487f0590623bafdbec52 Mon Sep 17 00:00:00 2001 From: Wind-Explorer Date: Sun, 29 Mar 2026 19:26:59 +0800 Subject: [PATCH] feat(security): production bootstrap hardening --- package.json | 1 + src/main.ts | 82 +++++++++++++++++++++++++++++++++++----------------- 2 files changed, 56 insertions(+), 27 deletions(-) diff --git a/package.json b/package.json index 85066a0..883b976 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "jsonwebtoken": "^9.0.2", "passport": "^0.7.0", "passport-discord": "^0.1.4", + "helmet": "^8.1.0", "passport-google-oauth20": "^2.0.0", "passport-jwt": "^4.0.1", "pg": "^8.16.3", diff --git a/src/main.ts b/src/main.ts index 3494653..e4d48ec 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,6 +2,7 @@ import { NestFactory } from '@nestjs/core'; import { ValidationPipe, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; +import helmet from 'helmet'; import { AppModule } from './app.module'; import { AllExceptionsFilter } from './common/filters/all-exceptions.filter'; import { RedisIoAdapter } from './ws/redis-io.adapter'; @@ -10,12 +11,28 @@ async function bootstrap() { const logger = new Logger('Bootstrap'); const app = await NestFactory.create(AppModule); const configService = app.get(ConfigService); + const nodeEnv = configService.get('NODE_ENV') || 'development'; + const isProduction = nodeEnv === 'production'; + + app.enableShutdownHooks(); + + app.use( + helmet({ + contentSecurityPolicy: false, + crossOriginEmbedderPolicy: false, + }), + ); // Configure Redis Adapter for horizontal scaling (if enabled) const redisIoAdapter = new RedisIoAdapter(app, configService); await redisIoAdapter.connectToRedis(); app.useWebSocketAdapter(redisIoAdapter); + app.enableCors({ + origin: true, + credentials: true, + }); + // Enable global exception filter for consistent error responses app.useGlobalFilters(new AllExceptionsFilter()); @@ -29,43 +46,54 @@ async function bootstrap() { // Automatically transform payloads to DTO instances transform: true, // Provide detailed error messages - disableErrorMessages: false, + disableErrorMessages: isProduction, }), ); - // Configure Swagger documentation - const config = new DocumentBuilder() - .setTitle('Friendolls API') - .setDescription( - 'API for managing users in Friendolls application.\n\n' + - 'Authentication is handled via Passport.js social sign-in for desktop clients.\n' + - 'Desktop clients exchange one-time SSO codes for Friendolls JWT tokens.\n\n' + - 'Include the JWT token in the Authorization header as: `Bearer `', - ) - .setVersion('1.0') - .addBearerAuth( - { - type: 'http', - scheme: 'bearer', - bearerFormat: 'JWT', - name: 'Authorization', - description: 'Enter Friendolls JWT access token', - in: 'header', - }, - 'bearer', - ) - .addTag('users', 'User profile management endpoints') - .build(); + if (!isProduction) { + const config = new DocumentBuilder() + .setTitle('Friendolls API') + .setDescription( + 'API for managing users in Friendolls application.\n\n' + + 'Authentication is handled via Passport.js social sign-in for desktop clients.\n' + + 'Desktop clients exchange one-time SSO codes for Friendolls JWT tokens.\n\n' + + 'Include the JWT token in the Authorization header as: `Bearer `', + ) + .setVersion('1.0') + .addBearerAuth( + { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + name: 'Authorization', + description: 'Enter Friendolls JWT access token', + in: 'header', + }, + 'bearer', + ) + .addTag('users', 'User profile management endpoints') + .build(); - const document = SwaggerModule.createDocument(app, config); - SwaggerModule.setup('api', app, document); + const document = SwaggerModule.createDocument(app, config); + SwaggerModule.setup('api', app, document); + } const host = process.env.HOST ?? 'localhost'; const port = process.env.PORT ?? 3000; await app.listen(port); + const httpServer = app.getHttpServer() as { + once?: (event: 'close', listener: () => void) => void; + } | null; + httpServer?.once?.('close', () => { + void redisIoAdapter.close(); + }); logger.log(`Application is running on: http://${host}:${port}`); - logger.log(`Swagger documentation available at: http://${host}:${port}/api`); + if (!isProduction) { + logger.log( + `Swagger documentation available at: http://${host}:${port}/api`, + ); + } } void bootstrap();