initial commit
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
'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();
|
||||
Reference in New Issue
Block a user