// 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 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'; /** * Creates and configures the Express application * This factory function allows the app to be created without starting the server * Useful for testing and modular architecture */ export const createApp = (): express.Express => { 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); return app; };