import cookieParser from 'cookie-parser'; import cors from 'cors'; import express from 'express'; import { fileURLToPath } from 'node:url'; import path from 'node:path'; import swaggerJsdoc from 'swagger-jsdoc'; import swaggerUi from 'swagger-ui-express'; import { errorHandler, notFoundHandler } from './middleware/errorHandler.js'; import { requireAdminAuth, requireAnyAuth, requireUserAuth } from './middleware/authMiddleware.js'; import adminRoutes from './routes/adminRoutes.js'; import authRoutes from './routes/authRoutes.js'; import configRoutes from './routes/configRoutes.js'; import healthRoutes from './routes/healthRoutes.js'; import lookupRoutes from './routes/lookupRoutes.js'; import reportRoutes from './routes/reportRoutes.js'; import templateRoutes from './routes/templateRoutes.js'; const app = express(); // Trust reverse proxy headers to get real client IP address app.set('trust proxy', true); /* * Swagger API documentation configuration. */ const swaggerOptions = { definition: { openapi: '3.0.0', info: { title: 'Check List PoC API', version: '0.2.0', description: 'Versioned PoC API for template, configuration, and report management.', }, servers: [ { url: '/', description: 'Local server' }, ], components: { securitySchemes: { bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT', }, cookieAuth: { type: 'apiKey', in: 'cookie', name: 'auth_token', }, }, }, security: [{ bearerAuth: [] }, { cookieAuth: [] }], tags: [ { name: 'Admin' }, { name: 'Admin: Categories' }, { name: 'Admin: Sub Categories' }, { name: 'Admin: Severities' }, { name: 'Admin: Statuses' }, { name: 'Admin: Handled By' }, { name: 'Admin: Projects' }, { name: 'Admin: Processes' }, { name: 'Admin: Users' }, { name: 'Admin: Sites' }, { name: 'Admin: CL Records' }, { name: 'Admin: CL Templates' }, { name: 'Admin: Tasks' }, { name: 'Configuration' }, { name: 'Templates' }, { name: 'Reports' }, { name: 'Authentication' }, { name: 'Health' }, ], }, apis: ['./src/routes/*.js'], }; const swaggerSpec = swaggerJsdoc(swaggerOptions); app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec, { customCss: '.swagger-ui .topbar { display: none }', customSiteTitle: 'Check List API Docs', })); /* * Request logging middleware for all API endpoints. */ app.use('/api', (req, res, next) => { const start = Date.now(); res.on('finish', () => { const duration = Date.now() - start; console.log(`${req.ip} ${req.method} ${req.originalUrl} ${res.statusCode} ${duration}ms`); }); next(); }); /* * The application serves two concerns from the same Express process: * 1. a versioned REST API (v1) used by the proof-of-concept frontend, * 2. static frontend assets for the chooser portal and the app shell itself. * * All API endpoints live under /api/v1/ so the contract can evolve without * breaking older clients. Keeping both concerns in one process keeps the PoC * easy to run in Docker and avoids introducing an additional frontend dev * server before the product shape is stable. */ const publicDir = fileURLToPath(new URL('../public', import.meta.url)); const userPagePath = path.join(publicDir, 'user.html'); const adminPagePath = path.join(publicDir, 'admin.html'); const portalPath = path.join(publicDir, 'portal.html'); const loginAdminPath = path.join(publicDir, 'login-admin.html'); const loginUserPath = path.join(publicDir, 'login-user.html'); app.use(cors()); app.use(cookieParser()); app.use(express.json({ limit: '50mb' })); /* * Prevent browsers from serving stale cached HTML pages. * * Without an explicit Cache-Control header, browsers apply heuristic freshness * based on the Last-Modified timestamp. Inside a Docker container the file * system mtime is frozen at image-build time, so browsers can cache HTML for * hours or days and users see the old version until they do a hard-refresh. * * `no-cache` means "always revalidate with the server before using a cached * copy". When the file has not changed Express returns a lightweight 304 Not * Modified (no body) thanks to the ETag it already sends, so the cost is just * one round-trip. When the file has changed the browser receives the new * content immediately. * * JS and CSS assets are deliberately excluded — their ETags already handle * freshness correctly and they are larger, so unnecessary revalidation would * add more overhead. */ function noCacheHtml(_req, res, next) { res.setHeader('Cache-Control', 'no-cache'); next(); } app.get('/api/v1', (_req, res) => { res.json({ service: 'check-list-poc-api', version: '0.2.0', description: 'Versioned PoC API for template, configuration, and report management.' }); }); /* * All API routes are grouped under /api/v1/. The version prefix ensures future * breaking changes can be introduced on /api/v2/ without disrupting existing * frontend deployments that still reference v1 contract shapes. */ app.use('/api/v1/health', healthRoutes); app.use('/api/v1/templates', templateRoutes); app.use('/api/v1/lookups', lookupRoutes); app.use('/api/v1/config', configRoutes); app.use('/api/v1/reports', requireAnyAuth, reportRoutes); app.use('/api/v1/admin', requireAnyAuth, adminRoutes); app.use('/api/v1/auth', authRoutes); /* * Login pages are served without authentication. */ app.get('/login-admin', noCacheHtml, (_req, res) => { res.sendFile(loginAdminPath); }); app.get('/login-user', noCacheHtml, (_req, res) => { res.sendFile(loginUserPath); }); /* * The root route intentionally serves a neutral portal page. This gives the * project distinct user and administrator entry points. */ app.get('/', noCacheHtml, (_req, res) => { res.sendFile(portalPath); }); /* * User and admin workspaces live in separate HTML files so each page only loads * the markup it needs. Authentication is required for both areas. */ app.get(['/user', '/user/'], requireUserAuth, noCacheHtml, (_req, res) => { res.sendFile(userPagePath); }); app.get(['/admin', '/admin/'], requireAdminAuth, noCacheHtml, (_req, res) => { res.sendFile(adminPagePath); }); /* * Serve static assets. HTML files get the same no-cache directive (covers * direct URL access like /user.html). JS/CSS/images use the default * ETag-based caching — conditional GETs (304) keep them efficient. */ app.use(express.static(publicDir, { setHeaders: (res, filePath) => { if (filePath.endsWith('.html')) { res.setHeader('Cache-Control', 'no-cache'); } } })); app.use(notFoundHandler); app.use(errorHandler); export default app;