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
142 lines
5.7 KiB
TypeScript
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();
|