Files
serienbrief/serienbrief-backend/src/server.js
T
2026-05-20 09:24:11 +02:00

144 lines
6.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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();