2026-01-16 01:33:43 +00:00

94 lines
2.8 KiB
TypeScript

/**
* Express app factory (useful for tests).
*/
import express from 'express';
import cors from 'cors';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { apiRouter } from './api/index.js';
const __dirname = dirname(fileURLToPath(import.meta.url));
function buildBasicAuthMiddleware() {
const BASIC_AUTH_USER = process.env.BASIC_AUTH_USER || '';
const BASIC_AUTH_PASSWORD = process.env.BASIC_AUTH_PASSWORD || '';
const basicAuthEnabled = BASIC_AUTH_USER.length > 0 && BASIC_AUTH_PASSWORD.length > 0;
function isAuthorized(req: express.Request): boolean {
if (!basicAuthEnabled) return true;
const authHeader = req.headers.authorization || '';
if (!authHeader.startsWith('Basic ')) return false;
const encoded = authHeader.slice('Basic '.length).trim();
let decoded = '';
try {
decoded = Buffer.from(encoded, 'base64').toString('utf-8');
} catch {
return false;
}
const separatorIndex = decoded.indexOf(':');
if (separatorIndex === -1) return false;
const user = decoded.slice(0, separatorIndex);
const pass = decoded.slice(separatorIndex + 1);
return user === BASIC_AUTH_USER && pass === BASIC_AUTH_PASSWORD;
}
function requiresAuth(method: string): boolean {
return !['GET', 'HEAD', 'OPTIONS'].includes(method.toUpperCase());
}
return (req: express.Request, res: express.Response, next: express.NextFunction) => {
if (!basicAuthEnabled || !requiresAuth(req.method)) return next();
if (isAuthorized(req)) return next();
res.setHeader('WWW-Authenticate', 'Basic realm="Job Ops"');
res.status(401).send('Authentication required');
};
}
export function createApp() {
const app = express();
app.use(cors());
app.use(express.json());
// Logging middleware
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log(`${req.method} ${req.path} - ${res.statusCode} (${duration}ms)`);
});
next();
});
// Optional Basic Auth for write access (read-only by default)
app.use(buildBasicAuthMiddleware());
// API routes
app.use('/api', apiRouter);
// Serve static files for generated PDFs
const pdfDir = process.env.DATA_DIR
? join(process.env.DATA_DIR, 'pdfs')
: join(__dirname, '../../data/pdfs');
app.use('/pdfs', express.static(pdfDir));
// Health check
app.get('/health', (_req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// Serve client app in production
if (process.env.NODE_ENV === 'production') {
const clientDir = join(__dirname, '../../dist/client');
app.use(express.static(clientDir));
// SPA fallback
app.get('*', (_req, res) => {
res.sendFile(join(clientDir, 'index.html'));
});
}
return app;
}