// Copyright (c) 2025 Tudor Stanciu import express from 'express'; import cors from 'cors'; import helmet from 'helmet'; import compression from 'compression'; import path from 'path'; import { setTimeout } from 'timers'; import apiRoutes from './routes/api'; import apiKeyAuth from './middleware/auth'; import dynamicRateLimit from './middleware/rateLimit'; import { errorHandler } from './middleware/errorHandler'; import config from './services/config'; import logger from './services/logger'; import { generateRuntimeConfig } from './services/runtimeConfig'; import { healthCheckHandler } from './handlers/healthHandler'; import { paths } from './utils/paths'; const app = express(); // Security middleware app.use( helmet({ contentSecurityPolicy: config.enableHttpsSecurity ? { directives: { defaultSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'", 'https://unpkg.com'], scriptSrc: ["'self'"], imgSrc: ["'self'", 'data:', 'https:'], connectSrc: ["'self'"], fontSrc: ["'self'"], objectSrc: ["'none'"], mediaSrc: ["'self'"], frameSrc: ["'none'"], }, } : false, strictTransportSecurity: config.enableHttpsSecurity, crossOriginOpenerPolicy: config.enableHttpsSecurity, crossOriginResourcePolicy: config.enableHttpsSecurity, originAgentCluster: false, // Disable Origin-Agent-Cluster header to avoid browser warnings }) ); app.use( cors({ origin: process.env.NODE_ENV === 'production' ? false : true, credentials: true, }) ); app.use(compression()); app.use(express.json({ limit: '10mb' })); app.use(express.urlencoded({ extended: true, limit: '10mb' })); // Request logging app.use((req, res, next) => { logger.debug('Incoming request', { method: req.method, path: req.path, ip: req.ip, userAgent: req.headers['user-agent'], }); next(); }); // Apply base path if configured const basePath = config.basePath.endsWith('/') ? config.basePath.slice(0, -1) : config.basePath; // Serve static frontend files const frontendPath = paths.frontendDir; // Generate runtime configuration (env.js) at startup generateRuntimeConfig(frontendPath, basePath); // Health check endpoint at root (always accessible for Docker HEALTHCHECK) app.get('/api/health', healthCheckHandler); // API routes with authentication and rate limiting app.use(`${basePath}/api`, apiKeyAuth, dynamicRateLimit, apiRoutes); // Serve static frontend files app.use(basePath, express.static(frontendPath)); // Fallback to index.html for client-side routing (Express 5 syntax) // Named wildcard required in path-to-regexp v8: /*path matches any path app.get(`${basePath}/*path`, (_req, res): void => { res.sendFile(path.join(frontendPath, 'index.html')); }); // Global error handler (must be last middleware) app.use(errorHandler); // Start server const server = app.listen(config.port, () => { logger.info('Bitip GeoIP Service started', { port: config.port, basePath: config.basePath, environment: process.env.NODE_ENV || 'development', dbPath: config.maxmindDbPath, }); }); // Graceful shutdown let isShuttingDown = false; const gracefulShutdown = async (signal: string): Promise => { if (isShuttingDown) { logger.warn(`Shutdown already in progress, ignoring ${signal}`); return; } isShuttingDown = true; logger.info(`Received ${signal}, shutting down gracefully`); // Set timeout slightly less than Docker's default (10s) const forceExitTimeout = setTimeout(() => { logger.error('Forced shutdown after timeout'); process.exit(1); }, 8000); try { // Stop accepting new connections await new Promise((resolve, reject) => { server.close(err => { if (err) { reject(err); } else { logger.info('Server closed successfully'); resolve(); } }); }); // Flush logs before exit await logger.flush(); logger.info('Shutdown complete'); clearTimeout(forceExitTimeout); process.exit(0); } catch (error) { logger.error('Error during graceful shutdown', error as Error); clearTimeout(forceExitTimeout); process.exit(1); } }; // Docker sends SIGTERM for graceful shutdown process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); // Ctrl+C in development process.on('SIGINT', () => gracefulShutdown('SIGINT')); // Nodemon uses SIGUSR2 for restart process.on('SIGUSR2', () => gracefulShutdown('SIGUSR2')); // nodemon uses SIGUSR2 // Handle server errors server.on('error', (error: Error) => { logger.error('Server error', error); process.exit(1); }); export default app;