94 lines
2.8 KiB
TypeScript
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;
|
|
}
|