import 'reflect-metadata'; import { NestFactory } from '@nestjs/core'; import { ValidationPipe, Logger } from '@nestjs/common'; import { NestExpressApplication } from '@nestjs/platform-express'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { toOpenApi31 } from './common/utils/openapi31'; import { ConfigService } from '@nestjs/config'; import helmet from 'helmet'; import cookieParser from 'cookie-parser'; import * as fs from 'fs'; import * as path from 'path'; import * as express from 'express'; import { AppModule } from './app.module'; import { SpaFallbackMiddleware } from './frontend/spa.controller'; import { DatabaseInitService } from './database/database-init.service'; import { CsrfMiddleware } from './common/middleware/csrf.middleware'; async function bootstrap(): Promise { const app = await NestFactory.create(AppModule, { bufferLogs: false, }); const config = app.get(ConfigService); const nodeEnv = config.get('NODE_ENV', 'development'); const port = config.get('PORT', 3000); const corsOrigins = (config.get('CORS_ORIGINS', 'http://localhost:4200,http://localhost:3000')) .split(',') .map((s) => s.trim()) .filter(Boolean); const bodyLimit = config.get('BODY_SIZE_LIMIT', '1mb'); const uploadDir = path.resolve(config.get('UPLOAD_DIR', './data/uploads')); const tlsEnabled = config.get('TLS_ENABLED', false); const frontendDist = path.resolve(config.get('FRONTEND_DIST', '../frontend/dist')); if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true }); await app.get(DatabaseInitService).init(); app.use( helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'"], imgSrc: ["'self'", 'data:', 'blob:'], connectSrc: ["'self'"], fontSrc: ["'self'", 'data:'], objectSrc: ["'none'"], frameAncestors: ["'none'"], }, }, hsts: tlsEnabled ? { maxAge: 31536000, includeSubDomains: true, preload: true } : false, frameguard: { action: 'deny' }, noSniff: true, referrerPolicy: { policy: 'no-referrer' }, }), ); app.enableCors({ origin: corsOrigins, credentials: true, methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], }); // Body parsers MUST run before the CSRF middleware so that route handlers // can read `req.body` when the CsrfMiddleware short-circuits to next(). app.use(cookieParser()); app.use(express.json({ limit: bodyLimit })); app.use(express.urlencoded({ extended: true, limit: bodyLimit })); // CSRF: register globally via express so we have explicit ordering after // the body parsers. (Nest's configure()-time middleware is registered // before app.use middlewares, which would consume the body before the // CsrfMiddleware short-circuits to next().) const csrf = new CsrfMiddleware(config); app.use((req, res, next) => csrf.use(req as any, res as any, next)); app.use('/uploads', express.static(uploadDir)); app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: false, transform: true })); const swaggerConfig = new DocumentBuilder() .setTitle('HIPCTF API') .setDescription('HIPCTF REST API') .setVersion('1.0.0') .addBearerAuth() .addCookieAuth('rt') .build(); const rawDocument = SwaggerModule.createDocument(app, swaggerConfig) as Record; const document = toOpenApi31(rawDocument); SwaggerModule.setup('api/docs', app, document, { jsonDocumentUrl: 'api/docs-json', }); if (fs.existsSync(frontendDist)) { app.use(express.static(frontendDist, { index: false, fallthrough: true })); const browserDir = path.join(frontendDist, 'browser'); if (fs.existsSync(browserDir)) { app.use(express.static(browserDir, { index: false, fallthrough: true })); } } const spa = new SpaFallbackMiddleware(config); app.use((req, res, next) => spa.use(req as any, res as any, next)); await app.listen(port, '0.0.0.0'); Logger.log(`HIPCTF API listening on http://0.0.0.0:${port} (env=${nodeEnv})`, 'Bootstrap'); } bootstrap().catch((err) => { console.error('Fatal startup error:', err); process.exit(1); });