Files
goodgo-platform/apps/api/src/main.ts
Ho Ngoc Hai e5f370ced1 feat(security): add CSRF double-submit cookie protection
Add CSRF middleware with double-submit cookie pattern for all
state-changing requests. Integrate cookie-parser, update CORS
headers, and add client-side CSRF token handling.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-08 05:03:24 +07:00

105 lines
3.7 KiB
TypeScript

import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { LoggerService } from '@modules/shared';
import cookieParser from 'cookie-parser';
import helmet from 'helmet';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule, { bufferLogs: true });
const logger = app.get(LoggerService);
app.useLogger(logger);
// ── OpenAPI / Swagger ──
const swaggerConfig = new DocumentBuilder()
.setTitle('Goodgo Platform API')
.setDescription('Real-estate platform API — listings, search, payments, subscriptions, analytics')
.setVersion('1.0')
.addBearerAuth(
{ type: 'http', scheme: 'bearer', bearerFormat: 'JWT' },
'JWT',
)
.addTag('auth', 'Authentication & user profile')
.addTag('listings', 'Property listings CRUD & moderation')
.addTag('search', 'Full-text & geo search')
.addTag('payments', 'Payment processing & callbacks')
.addTag('subscriptions', 'Plans, billing & usage metering')
.addTag('admin', 'Admin panel operations')
.addTag('notifications', 'Notification history & preferences')
.addTag('analytics', 'Market reports & price analytics')
.build();
const document = SwaggerModule.createDocument(app, swaggerConfig);
SwaggerModule.setup('api/docs', app, document, {
swaggerOptions: { persistAuthorization: true },
});
// ── Security Headers (Helmet) ──
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:', 'blob:'],
connectSrc: ["'self'"],
fontSrc: ["'self'", 'data:'],
objectSrc: ["'none'"],
frameSrc: ["'none'"],
baseUri: ["'self'"],
formAction: ["'self'"],
},
},
crossOriginEmbedderPolicy: true,
crossOriginOpenerPolicy: true,
crossOriginResourcePolicy: { policy: 'same-origin' },
hsts: { maxAge: 31536000, includeSubDomains: true, preload: true },
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
}),
);
// ── Cookie Parser (required for CSRF double-submit pattern) ──
app.use(cookieParser());
// ── CORS ──
const allowedOrigins = (process.env['CORS_ORIGINS'] ?? 'http://localhost:3000')
.split(',')
.map((o) => o.trim());
app.enableCors({
origin: allowedOrigins,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Correlation-Id', 'X-CSRF-Token'],
exposedHeaders: ['X-Correlation-Id'],
credentials: true,
maxAge: 86400,
});
// ── Global Validation Pipe ──
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
transformOptions: { enableImplicitConversion: true },
disableErrorMessages: process.env['NODE_ENV'] === 'production',
}),
);
// ── Request Body Size Limit ──
// Express default is 100kb; explicitly set for clarity
const expressApp = app.getHttpAdapter().getInstance();
const { json, urlencoded } = await import('express');
expressApp.use(json({ limit: '1mb' }));
expressApp.use(urlencoded({ extended: true, limit: '1mb' }));
// ── Trust Proxy (for rate limiting behind reverse proxy) ──
expressApp.set('trust proxy', 1);
const port = process.env['PORT'] ?? 3001;
await app.listen(port);
logger.log(`API running on http://localhost:${port}`, 'Bootstrap');
}
bootstrap();