209 lines
6.7 KiB
JavaScript
209 lines
6.7 KiB
JavaScript
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;
|