114 lines
4.3 KiB
TypeScript
114 lines
4.3 KiB
TypeScript
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<void> {
|
|
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
|
|
bufferLogs: false,
|
|
});
|
|
|
|
const config = app.get(ConfigService);
|
|
const nodeEnv = config.get<string>('NODE_ENV', 'development');
|
|
const port = config.get<number>('PORT', 3000);
|
|
const corsOrigins = (config.get<string>('CORS_ORIGINS', 'http://localhost:4200,http://localhost:3000'))
|
|
.split(',')
|
|
.map((s) => s.trim())
|
|
.filter(Boolean);
|
|
const bodyLimit = config.get<string>('BODY_SIZE_LIMIT', '1mb');
|
|
const uploadDir = path.resolve(config.get<string>('UPLOAD_DIR', '/data/hipctf/uploads'));
|
|
const tlsEnabled = config.get<boolean>('TLS_ENABLED', false);
|
|
const frontendDist = path.resolve(config.get<string>('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<string, any>;
|
|
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);
|
|
}); |