Some checks failed
CD Preprod / Unit Tests (${{ matrix.app }}) (backend) (push) Blocked by required conditions
CD Preprod / Unit Tests (${{ matrix.app }}) (frontend) (push) Blocked by required conditions
CD Preprod / Integration Tests (push) Blocked by required conditions
CD Preprod / Build & Push Backend (push) Blocked by required conditions
CD Preprod / Build & Push Frontend (push) Blocked by required conditions
CD Preprod / Deploy to Preprod (push) Blocked by required conditions
CD Preprod / Smoke Tests (push) Blocked by required conditions
CD Preprod / Deployment Summary (push) Blocked by required conditions
CD Preprod / Notify Success (push) Blocked by required conditions
CD Preprod / Notify Failure (push) Blocked by required conditions
CD Preprod / Quality (${{ matrix.app }}) (backend) (push) Has been cancelled
CD Preprod / Quality (${{ matrix.app }}) (frontend) (push) Has been cancelled
Aligns preprod with the complete application codebase (cicd branch). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
131 lines
5.1 KiB
TypeScript
131 lines
5.1 KiB
TypeScript
import { NestFactory } from '@nestjs/core';
|
|
import { ValidationPipe, VersioningType } from '@nestjs/common';
|
|
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
|
import { ConfigService } from '@nestjs/config';
|
|
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 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
|
|
app.useGlobalPipes(
|
|
new ValidationPipe({
|
|
whitelist: true,
|
|
forbidNonWhitelisted: true,
|
|
transform: true,
|
|
transformOptions: {
|
|
enableImplicitConversion: true,
|
|
},
|
|
})
|
|
);
|
|
|
|
// ─── 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();
|