Files
serienbrief/serienbrief-backend/src/server.js
T

144 lines
6.0 KiB
JavaScript
Raw Normal View History

2026-05-20 09:24:11 +02:00
'use strict';
/**
* Serienbrief-Backend — Express.js Entry Point
*
* Startreihenfolge:
* 1. Konfiguration validieren
* 2. Audit-DB Schema initialisieren
* 3. Express-App konfigurieren
* 4. HTTP-Server starten
* 5. Graceful-Shutdown registrieren
*/
const express = require('express');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const { pinoHttp } = require('pino-http');
const config = require('./config');
const logger = require('./services/logger');
const auditLog = require('./services/auditLog');
const { errorHandler } = require('./middleware/errorHandler');
// ── Routen ───────────────────────────────────────────────────────────────────
const renderRoute = require('./routes/render');
const templatesRoute = require('./routes/templates');
const recipientsRoute = require('./routes/recipients');
const auditRoute = require('./routes/audit');
// ── App-Setup ────────────────────────────────────────────────────────────────
const app = express();
// Trust Proxy (Traefik sitzt davor)
app.set('trust proxy', 1);
// ── Security-Header ──────────────────────────────────────────────────────────
app.use(helmet({
contentSecurityPolicy: false, // Wird vom Reverse Proxy gesetzt
crossOriginEmbedderPolicy: false,
}));
// ── Request-Logging (kein PII) ───────────────────────────────────────────────
app.use(pinoHttp({
logger,
// Keine Query-Parameter loggen (könnten PII enthalten)
customLogLevel: (req, res, err) => err ? 'error' : res.statusCode >= 400 ? 'warn' : 'info',
serializers: {
req(req) {
return { method: req.method, url: req.url.split('?')[0], id: req.id };
},
},
}));
// ── Body-Parser ───────────────────────────────────────────────────────────────
app.use(express.json({ limit: '64kb' })); // Kein großer JSON-Body nötig
// ── Globaler Rate-Limiter (alle Endpunkte) ────────────────────────────────────
app.use(rateLimit({
windowMs: 60_000,
max: 100,
standardHeaders: true,
legacyHeaders: false,
message: { error: 'Zu viele Anfragen. Bitte warten.' },
}));
// ── Health-Endpunkt (kein Auth, für Docker-Healthcheck und Monitoring) ────────
app.get('/health', (req, res) => {
res.json({
status: 'ok',
version: process.env.npm_package_version || '1.0.0',
uptime: Math.floor(process.uptime()),
renderer: process.env.RENDERER || 'gotenberg',
timestamp: new Date().toISOString(),
});
});
// ── API-Routen ────────────────────────────────────────────────────────────────
app.use('/api/sb/render', renderRoute);
app.use('/api/sb/templates', templatesRoute);
app.use('/api/sb/recipients', recipientsRoute);
app.use('/api/sb/audit', auditRoute);
// Interne Admin-Routen (kein Proxy-Routing nach außen)
app.post('/admin/reload-templates', (req, res) => {
const templateCache = require('./services/templateCache');
templateCache.invalidateAll();
res.json({ message: 'Template-Cache geleert.' });
});
// ── 404 ───────────────────────────────────────────────────────────────────────
app.use((req, res) => {
res.status(404).json({ error: 'Endpunkt nicht gefunden.' });
});
// ── Fehlerhandler ─────────────────────────────────────────────────────────────
app.use(errorHandler);
// ── Start ─────────────────────────────────────────────────────────────────────
async function start() {
try {
// Audit-Schema anlegen (idempotent)
await auditLog.initSchema();
logger.info('Audit-DB Schema bereit');
} catch (err) {
logger.error({ err }, 'Audit-DB Initialisierung fehlgeschlagen Server startet trotzdem');
// Kein harter Abbruch: Server soll starten, auch wenn Audit-DB kurz nicht erreichbar ist.
// Der Audit-Log-Service fängt Fehler intern ab.
}
const server = app.listen(config.server.port, () => {
logger.info({
port: config.server.port,
env: config.server.nodeEnv,
directus: config.directus.url,
renderer: process.env.RENDERER || 'gotenberg',
}, 'Serienbrief-Backend gestartet');
});
// ── Graceful Shutdown ────────────────────────────────────────────────────────
function shutdown(signal) {
logger.info({ signal }, 'Shutdown eingeleitet');
server.close(() => {
logger.info('HTTP-Server geschlossen');
process.exit(0);
});
// Erzwingter Exit nach 10 Sekunden
setTimeout(() => {
logger.error('Graceful Shutdown Timeout erzwungener Exit');
process.exit(1);
}, 10_000);
}
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
// Unbehandelte Promise-Rejections loggen (kein Crash in Produktion, aber Alarm)
process.on('unhandledRejection', (reason) => {
logger.error({ reason }, 'Unbehandelte Promise-Rejection');
});
}
start();