'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();