feat(security): production bootstrap hardening

This commit is contained in:
2026-03-29 19:26:59 +08:00
parent 6793460d31
commit 765d4507c9
2 changed files with 56 additions and 27 deletions

View File

@@ -49,6 +49,7 @@
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"passport": "^0.7.0", "passport": "^0.7.0",
"passport-discord": "^0.1.4", "passport-discord": "^0.1.4",
"helmet": "^8.1.0",
"passport-google-oauth20": "^2.0.0", "passport-google-oauth20": "^2.0.0",
"passport-jwt": "^4.0.1", "passport-jwt": "^4.0.1",
"pg": "^8.16.3", "pg": "^8.16.3",

View File

@@ -2,6 +2,7 @@ import { NestFactory } from '@nestjs/core';
import { ValidationPipe, Logger } from '@nestjs/common'; import { ValidationPipe, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import helmet from 'helmet';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
import { AllExceptionsFilter } from './common/filters/all-exceptions.filter'; import { AllExceptionsFilter } from './common/filters/all-exceptions.filter';
import { RedisIoAdapter } from './ws/redis-io.adapter'; import { RedisIoAdapter } from './ws/redis-io.adapter';
@@ -10,12 +11,28 @@ async function bootstrap() {
const logger = new Logger('Bootstrap'); const logger = new Logger('Bootstrap');
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);
const configService = app.get(ConfigService); const configService = app.get(ConfigService);
const nodeEnv = configService.get<string>('NODE_ENV') || 'development';
const isProduction = nodeEnv === 'production';
app.enableShutdownHooks();
app.use(
helmet({
contentSecurityPolicy: false,
crossOriginEmbedderPolicy: false,
}),
);
// Configure Redis Adapter for horizontal scaling (if enabled) // Configure Redis Adapter for horizontal scaling (if enabled)
const redisIoAdapter = new RedisIoAdapter(app, configService); const redisIoAdapter = new RedisIoAdapter(app, configService);
await redisIoAdapter.connectToRedis(); await redisIoAdapter.connectToRedis();
app.useWebSocketAdapter(redisIoAdapter); app.useWebSocketAdapter(redisIoAdapter);
app.enableCors({
origin: true,
credentials: true,
});
// Enable global exception filter for consistent error responses // Enable global exception filter for consistent error responses
app.useGlobalFilters(new AllExceptionsFilter()); app.useGlobalFilters(new AllExceptionsFilter());
@@ -29,43 +46,54 @@ async function bootstrap() {
// Automatically transform payloads to DTO instances // Automatically transform payloads to DTO instances
transform: true, transform: true,
// Provide detailed error messages // Provide detailed error messages
disableErrorMessages: false, disableErrorMessages: isProduction,
}), }),
); );
// Configure Swagger documentation if (!isProduction) {
const config = new DocumentBuilder() const config = new DocumentBuilder()
.setTitle('Friendolls API') .setTitle('Friendolls API')
.setDescription( .setDescription(
'API for managing users in Friendolls application.\n\n' + 'API for managing users in Friendolls application.\n\n' +
'Authentication is handled via Passport.js social sign-in for desktop clients.\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' + 'Desktop clients exchange one-time SSO codes for Friendolls JWT tokens.\n\n' +
'Include the JWT token in the Authorization header as: `Bearer <token>`', 'Include the JWT token in the Authorization header as: `Bearer <token>`',
) )
.setVersion('1.0') .setVersion('1.0')
.addBearerAuth( .addBearerAuth(
{ {
type: 'http', type: 'http',
scheme: 'bearer', scheme: 'bearer',
bearerFormat: 'JWT', bearerFormat: 'JWT',
name: 'Authorization', name: 'Authorization',
description: 'Enter Friendolls JWT access token', description: 'Enter Friendolls JWT access token',
in: 'header', in: 'header',
}, },
'bearer', 'bearer',
) )
.addTag('users', 'User profile management endpoints') .addTag('users', 'User profile management endpoints')
.build(); .build();
const document = SwaggerModule.createDocument(app, config); const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document); SwaggerModule.setup('api', app, document);
}
const host = process.env.HOST ?? 'localhost'; const host = process.env.HOST ?? 'localhost';
const port = process.env.PORT ?? 3000; const port = process.env.PORT ?? 3000;
await app.listen(port); 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(`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(); void bootstrap();