/** * Workflow Portal Backend - Modular Architecture * * Punkt 1: Modularized from monolithic server.js into route modules * Punkt 2: Removed unused Prisma (no longer needed) * Punkt 3: Proper migration tracking via _migrations table * Punkt 4: Session tokens hashed with SHA-256 * Punkt 5: Register returns correct 'inaktiv' status * Punkt 6: better-sqlite3 (synchronous, no callback hell) * Punkt 7: Single aggregated stats query * Punkt 8: Transactions for template updates and task creation * Punkt 9: LDAP sync lock * Punkt 10: express-async-errors for global error handling * Punkt 19: Configurable CORS via env * Punkt 22: Prisma removed (was unused) * Punkt 23: User-level rate limiting */ const express = require('express'); require('express-async-errors'); const cors = require('cors'); const helmet = require('helmet'); const cookieParser = require('cookie-parser'); const path = require('path'); // Initialize database (better-sqlite3, WAL mode, migrations) const db = require('./db'); const { initDatabase } = require('./migrations'); // Auth middleware const { authMiddleware, csrfMiddleware } = require('./middleware/auth'); // Rate limiters const rateLimit = require('express-rate-limit'); const { apiLimiter } = require('./middleware/rateLimit'); // Route modules const authRoutes = require('./routes/auth'); const usersRoutes = require('./routes/users'); const templatesRoutes = require('./routes/templates'); const tasksRoutes = require('./routes/tasks'); const adRoutes = require('./routes/ad'); const statsRoutes = require('./routes/stats'); const uploadRoutes = require('./routes/upload'); // LDAP sync const { startLDAPSync, isLDAPConfigured } = require('./ldapSync'); const app = express(); const PORT = process.env.PORT || 5000; // Trust proxy for correct IP in rate limiting (Docker/Reverse Proxy) app.set('trust proxy', 1); // ============ Security Middleware ============ // P14: Validate CORS_ORIGIN - filter empty/invalid entries before using in CSP const rawCorsOrigin = process.env.CORS_ORIGIN || ''; const validCorsOrigins = rawCorsOrigin .split(',') .map(o => o.trim()) .filter(o => o && /^https?:\/\/.+/.test(o)); const cspConnectSrc = ["'self'", ...validCorsOrigins]; // M1: HSTS only makes sense over HTTPS. When serving plain HTTP (e.g. without // a TLS-terminating proxy), HSTS is ignored by browsers and can even cause // issues. Allow disabling it via HSTS_ENABLED=false (default: enabled in prod // when COOKIE_SECURE is true, i.e. when TLS is expected). const { COOKIE_SECURE } = require('./middleware/auth'); const hstsEnabled = process.env.HSTS_ENABLED !== undefined ? process.env.HSTS_ENABLED === 'true' : COOKIE_SECURE; app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'"], imgSrc: ["'self'", "data:"], connectSrc: cspConnectSrc, fontSrc: ["'self'", "data:"], }, }, // P18: HSTS - enforce HTTPS in production (only effective over HTTPS) hsts: hstsEnabled ? { maxAge: 31536000, includeSubDomains: true, preload: true, } : false, crossOriginEmbedderPolicy: false, })); // Punkt 19: Configurable CORS via env variable // Single container: Frontend served from same origin, CORS only needed for external access const allowedOrigins = validCorsOrigins.length > 0 ? validCorsOrigins : ['http://localhost:5000', 'http://localhost:5173', 'http://127.0.0.1:5000', 'http://127.0.0.1:5173']; app.use(cors({ origin: allowedOrigins, credentials: true })); // Punkt 14: Body-Size-Limit to prevent DoS via large payloads // N4: 10MB to accommodate file uploads (matches UPLOAD_MAX_MB) app.use(express.json({ limit: process.env.BODY_LIMIT || '10mb' })); // Punkt 8: Cookie-Parser für HttpOnly-Cookie Auth app.use(cookieParser()); // P4: CSRF protection for state-changing requests (Double-Submit-Cookie) // Skip CSRF check for login/register (no session yet, no CSRF token available) app.use('/api', (req, res, next) => { if (req.path.startsWith('/auth/login') || req.path.startsWith('/auth/register') || req.path.startsWith('/v1/auth/login') || req.path.startsWith('/v1/auth/register')) { return next(); } csrfMiddleware(req, res, next); }); // ============ Rate Limiting ============ app.use('/api', apiLimiter); // ============ Health Check (Punkt 6) ============ // V5: Rate-limit /health to prevent DoS/amplification abuse const healthLimiter = rateLimit({ windowMs: 60 * 1000, max: 30, standardHeaders: true, legacyHeaders: false, message: { error: 'Zu viele Health-Check-Anfragen.' }, }); app.get('/health', healthLimiter, (req, res) => { res.json({ status: 'ok', uptime: Math.floor(process.uptime()), timestamp: new Date().toISOString() }); }); // ============ Auth Middleware for all /api/ routes except /api/auth/ ============ app.use('/api', (req, res, next) => { // Skip auth for login, register, and status endpoints if (req.path.startsWith('/auth/') || req.path === '/ad/status') { return next(); } authMiddleware(req, res, next); }); // ============ Routes (Punkt 13: API-Versionierung /api/v1) ============ app.use('/api/v1/auth', authRoutes); app.use('/api/v1/users', usersRoutes); app.use('/api/v1/templates', templatesRoutes); app.use('/api/v1/tasks', tasksRoutes); app.use('/api/v1/ad', adRoutes); app.use('/api/v1', statsRoutes); app.use('/api/v1/upload', uploadRoutes); // ============ Backward Compatibility: /api/ → /api/v1/ ============ app.use('/api/auth', authRoutes); app.use('/api/users', usersRoutes); app.use('/api/templates', templatesRoutes); app.use('/api/tasks', tasksRoutes); app.use('/api/ad', adRoutes); app.use('/api', statsRoutes); app.use('/api/upload', uploadRoutes); // ============ Serve Frontend (Single Container) ============ const frontendPath = path.join(__dirname, 'frontend', 'dist'); app.use(express.static(frontendPath)); // SPA fallback: serve index.html for all non-API routes app.get('*', (req, res, next) => { if (req.path.startsWith('/api') || req.path.startsWith('/health')) return next(); res.sendFile(path.join(frontendPath, 'index.html')); }); // ============ Global Error Handler (Punkt 10) ============ app.use((err, req, res, next) => { console.error('[ERROR]', req.method, req.path, '-', err.message); if (res.headersSent) return next(err); res.status(500).json({ error: 'Interner Serverfehler.' }); }); // ============ Initialize & Start ============ async function start() { try { await initDatabase(); startLDAPSync(db); const server = app.listen(PORT, () => { console.log(`Workflow Portal Backend gestartet auf Port ${PORT}`); }); // ============ Graceful Shutdown (Punkt 4) ============ function gracefulShutdown(signal) { console.log(`\n[SHUTDOWN] ${signal} empfangen, fahre herunter...`); // Stop LDAP sync timer const { stopLDAPSync } = require('./ldapSync'); stopLDAPSync(); // Stop accepting new connections server.close(async () => { console.log('[SHUTDOWN] HTTP-Server gestoppt.'); // Close database connection try { if (db._type === 'postgres') { await db.close(); } else { db.close(); } console.log('[SHUTDOWN] Datenbankverbindung geschlossen.'); } catch (err) { console.error('[SHUTDOWN] Fehler beim Schließen der Datenbank:', err.message); } console.log('[SHUTDOWN] Erfolgreich heruntergefahren.'); process.exit(0); }); // Force shutdown after 10 seconds if connections don't close setTimeout(() => { console.error('[SHUTDOWN] Erzwinge Shutdown nach Timeout.'); process.exit(1); }, 10000); } process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); process.on('SIGINT', () => gracefulShutdown('SIGINT')); } catch (err) { console.error('[FATAL] Start fehlgeschlagen:', err.message); process.exit(1); } } start();