xpeditis2.0/apps/backend/src/main.ts
David ec0173483a
All checks were successful
Dev CI / Backend — Lint (push) Successful in 10m23s
Dev CI / Backend — Unit Tests (push) Successful in 10m17s
Dev CI / Frontend — Lint & Type-check (push) Successful in 11m3s
Dev CI / Frontend — Unit Tests (push) Successful in 10m33s
Dev CI / Notify Failure (push) Has been skipped
fix language
2026-04-21 18:04:02 +02:00

142 lines
5.7 KiB
TypeScript

import { NestFactory } from '@nestjs/core';
import { VersioningType } from '@nestjs/common';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { ConfigService } from '@nestjs/config';
import { I18nService, I18nValidationExceptionFilter, I18nValidationPipe } from 'nestjs-i18n';
import helmet from 'helmet';
import compression from 'compression';
import { AppModule } from './app.module';
import { Logger } from 'nestjs-pino';
import { helmetConfig, corsConfig } from './infrastructure/security/security.config';
import { DomainExceptionFilter } from './application/filters/domain-exception.filter';
import type { Request, Response, NextFunction } from 'express';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
bufferLogs: true,
// Enable rawBody for Stripe webhooks signature verification
rawBody: true,
});
// Get config service
const configService = app.get(ConfigService);
const port = configService.get<number>('PORT', 4000);
const apiPrefix = configService.get<string>('API_PREFIX', 'api/v1');
const isProduction = configService.get<string>('NODE_ENV') === 'production';
// Use Pino logger
app.useLogger(app.get(Logger));
// Security - Helmet with OWASP recommended headers
app.use(helmet(helmetConfig));
// Compression for API responses
app.use(compression());
// CORS with strict configuration
app.enableCors(corsConfig);
// Global prefix
app.setGlobalPrefix(apiPrefix);
// API versioning
app.enableVersioning({
type: VersioningType.URI,
});
// Global validation pipe — i18n-aware (messages translated to caller locale)
app.useGlobalPipes(
new I18nValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
transformOptions: {
enableImplicitConversion: true,
},
})
);
// Global exception filters — each filter declares its target via @Catch(),
// so they don't overlap: DomainExceptionFilter handles DomainException,
// I18nValidationExceptionFilter handles class-validator errors.
const i18nService = app.get(I18nService) as I18nService<Record<string, unknown>>;
app.useGlobalFilters(
new DomainExceptionFilter(i18nService),
new I18nValidationExceptionFilter({ detailedErrors: false })
);
// ─── Swagger documentation ────────────────────────────────────────────────
const swaggerUser = configService.get<string>('SWAGGER_USERNAME');
const swaggerPass = configService.get<string>('SWAGGER_PASSWORD');
const swaggerEnabled = !isProduction || (Boolean(swaggerUser) && Boolean(swaggerPass));
if (swaggerEnabled) {
// HTTP Basic Auth guard for Swagger routes when credentials are configured
if (swaggerUser && swaggerPass) {
const swaggerPaths = ['/api/docs', '/api/docs-json', '/api/docs-yaml'];
app.use(swaggerPaths, (req: Request, res: Response, next: NextFunction) => {
const authHeader = req.headers['authorization'];
if (!authHeader || !authHeader.startsWith('Basic ')) {
res.setHeader('WWW-Authenticate', 'Basic realm="Xpeditis API Docs"');
res.status(401).send('Authentication required');
return;
}
const decoded = Buffer.from(authHeader.slice(6), 'base64').toString('utf-8');
const colonIndex = decoded.indexOf(':');
const user = decoded.slice(0, colonIndex);
const pass = decoded.slice(colonIndex + 1);
if (user !== swaggerUser || pass !== swaggerPass) {
res.setHeader('WWW-Authenticate', 'Basic realm="Xpeditis API Docs"');
res.status(401).send('Invalid credentials');
return;
}
next();
});
}
const config = new DocumentBuilder()
.setTitle('Xpeditis API')
.setDescription(
'Maritime Freight Booking Platform - API for searching rates and managing bookings'
)
.setVersion('1.0')
.addBearerAuth()
.addApiKey({ type: 'apiKey', name: 'x-api-key', in: 'header' }, 'x-api-key')
.addTag('rates', 'Rate search and comparison')
.addTag('bookings', 'Booking management')
.addTag('auth', 'Authentication and authorization')
.addTag('users', 'User management')
.addTag('organizations', 'Organization management')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api/docs', app, document, {
customSiteTitle: 'Xpeditis API Documentation',
customfavIcon: 'https://xpeditis.com/favicon.ico',
customCss: '.swagger-ui .topbar { display: none }',
});
}
// ─────────────────────────────────────────────────────────────────────────
await app.listen(port);
const swaggerStatus = swaggerEnabled
? swaggerUser
? `http://localhost:${port}/api/docs (protected)`
: `http://localhost:${port}/api/docs (open — add SWAGGER_USERNAME/PASSWORD to secure)`
: 'disabled in production';
console.log(`
╔═══════════════════════════════════════════════╗
║ ║
║ 🚢 Xpeditis API Server Running ║
║ ║
║ API: http://localhost:${port}/${apiPrefix}
║ Docs: ${swaggerStatus}
║ ║
╚═══════════════════════════════════════════════╝
`);
}
bootstrap();