import express from 'express'; import cors from 'cors'; import helmet from 'helmet'; import path from 'path'; import dotenv from 'dotenv'; import authRoutes from './routes/auth.routes.js'; import customerRoutes from './routes/customer.routes.js'; import addressRoutes from './routes/address.routes.js'; import bankcardRoutes from './routes/bankcard.routes.js'; import documentRoutes from './routes/document.routes.js'; import meterRoutes from './routes/meter.routes.js'; import stressfreiEmailRoutes from './routes/stressfreiEmail.routes.js'; import contractRoutes from './routes/contract.routes.js'; import platformRoutes from './routes/platform.routes.js'; import cancellationPeriodRoutes from './routes/cancellation-period.routes.js'; import contractDurationRoutes from './routes/contract-duration.routes.js'; import providerRoutes from './routes/provider.routes.js'; import tariffRoutes from './routes/tariff.routes.js'; import userRoutes from './routes/user.routes.js'; import uploadRoutes from './routes/upload.routes.js'; import developerRoutes from './routes/developer.routes.js'; import contractCategoryRoutes from './routes/contractCategory.routes.js'; import contractTaskRoutes from './routes/contractTask.routes.js'; import appSettingRoutes from './routes/appSetting.routes.js'; import emailProviderRoutes from './routes/emailProvider.routes.js'; import cachedEmailRoutes from './routes/cachedEmail.routes.js'; import invoiceRoutes from './routes/invoice.routes.js'; import contractHistoryRoutes from './routes/contractHistory.routes.js'; import auditLogRoutes from './routes/auditLog.routes.js'; import gdprRoutes from './routes/gdpr.routes.js'; import consentPublicRoutes from './routes/consent-public.routes.js'; import emailLogRoutes from './routes/emailLog.routes.js'; import pdfTemplateRoutes from './routes/pdfTemplate.routes.js'; import birthdayRoutes from './routes/birthday.routes.js'; import factoryDefaultsRoutes from './routes/factoryDefaults.routes.js'; import { downloadFile } from './controllers/fileDownload.controller.js'; import { startBirthdayScheduler } from './services/birthdayScheduler.service.js'; import { startContractStatusScheduler } from './services/contractStatusScheduler.service.js'; import { startSecurityMonitorScheduler } from './services/securityAlert.service.js'; import monitoringRoutes from './routes/monitoring.routes.js'; import { auditContextMiddleware } from './middleware/auditContext.js'; import { auditMiddleware } from './middleware/audit.js'; import { authenticate } from './middleware/auth.js'; dotenv.config(); // ==================== SECURITY: Pflicht-Umgebungsvariablen prüfen ==================== if (!process.env.JWT_SECRET || process.env.JWT_SECRET.length < 32) { console.error('❌ JWT_SECRET ist nicht gesetzt oder zu kurz (min. 32 Zeichen)'); console.error(' Generiere mit: openssl rand -hex 64'); process.exit(1); } if (!process.env.ENCRYPTION_KEY || process.env.ENCRYPTION_KEY.length !== 64) { console.error('❌ ENCRYPTION_KEY ist nicht gesetzt oder hat nicht exakt 64 Hex-Zeichen (32 Byte)'); console.error(' Generiere mit: openssl rand -hex 32'); process.exit(1); } const app = express(); const PORT = process.env.PORT || 3001; // Hinter einem Reverse-Proxy (Nginx/Plesk) läuft der Server typisch auf localhost. // `trust proxy = 'loopback'` vertraut nur Connections von 127.0.0.1 / ::1 // (= lokaler Reverse-Proxy). Damit kann ein Angreifer mit DIREKTEM Zugriff // auf das Backend nicht via X-Forwarded-For den Rate-Limiter umgehen, // während gleichzeitig der lokale Reverse-Proxy die echte Client-IP liefern darf. // // WICHTIG für Production: Backend nur auf 127.0.0.1 lauschen lassen // (LISTEN_ADDR=127.0.0.1) – sonst kann ein direkter Connect von außen // trotzdem als loopback gelten, falls das Routing das so durchstellt. app.set('trust proxy', 'loopback'); // ==================== SECURITY MIDDLEWARE ==================== // HTTP Security Headers (X-Frame-Options, X-Content-Type-Options, HSTS, etc.) app.use( helmet({ // CSP ausschalten – wird bei SPA schwierig, frontend setzt eigene CSP via meta contentSecurityPolicy: false, // Cross-Origin-Resource-Policy: "same-site" für SPA mit gleicher Origin crossOriginResourcePolicy: { policy: 'same-site' }, }), ); // CORS: in Production nur explizit erlaubte Origins. In Dev: alles erlauben. const corsOrigins = process.env.CORS_ORIGINS ? process.env.CORS_ORIGINS.split(',').map((s) => s.trim()) : process.env.NODE_ENV === 'production' ? false // Gar kein Cross-Origin zulässig (Frontend wird unter gleicher Origin ausgeliefert) : true; // Dev: alles erlauben app.use( cors({ origin: corsOrigins, credentials: true, }), ); // JSON-Body-Limit: 5 MB (Uploads laufen über multer, brauchen kein json()) app.use(express.json({ limit: '5mb' })); // Audit-Logging Middleware (DSGVO-konform) app.use(auditContextMiddleware); app.use(auditMiddleware); // Datei-Download mit Per-File-Ownership-Check (ersetzt das alte // `/api/uploads/*` express.static). // Frontend-URLs gehen jetzt über GET /api/files/download?path=/uploads/... // Der Controller mappt den Pfad auf eine Resource (BankCard, Contract, etc.) // und prüft canAccessCustomer/canAccessContract – damit kann ein Portal-Kunde // nur seine eigenen Dateien laden, selbst wenn er fremde Filenames kennt. // // Kompatibilität: das alte /api/uploads/* bleibt erhalten, leitet aber jeden // Request über denselben Owner-Check (kein freier static-Handler mehr). // Authentifizierter Datei-Download mit Per-File-Ownership-Check. // Akzeptiert Pfade wie /uploads/bank-cards/ – egal ob als // Query-Parameter oder im Pfad-Suffix. Beide gehen über denselben Handler, // der DB-basiert prüft, ob der eingeloggte User die Resource sehen darf. app.get('/api/files/download', authenticate as any, downloadFile as any); // Backwards-compatibility shim: `/api/uploads/*` sieht weiter aus wie früher // für Bestandsclients/Bookmarks, ruft aber denselben Owner-Check-Handler. app.get('/api/uploads/*', authenticate as any, (req, res, next) => { // Pfad in Query-Param umschreiben, dann an downloadFile weiterreichen req.query.path = req.originalUrl.replace(/^\/api/, '').split('?')[0]; return (downloadFile as any)(req, res, next); }); // Öffentliche Routes (OHNE Authentifizierung) app.use('/api/public/consent', consentPublicRoutes); // Routes app.use('/api/auth', authRoutes); app.use('/api/customers', customerRoutes); app.use('/api/addresses', addressRoutes); app.use('/api/bank-cards', bankcardRoutes); app.use('/api/documents', documentRoutes); app.use('/api/meters', meterRoutes); app.use('/api/stressfrei-emails', stressfreiEmailRoutes); app.use('/api/contracts', contractRoutes); app.use('/api/platforms', platformRoutes); app.use('/api/cancellation-periods', cancellationPeriodRoutes); app.use('/api/contract-durations', contractDurationRoutes); app.use('/api/providers', providerRoutes); app.use('/api/tariffs', tariffRoutes); app.use('/api/users', userRoutes); app.use('/api/upload', uploadRoutes); app.use('/api/developer', developerRoutes); app.use('/api/contract-categories', contractCategoryRoutes); app.use('/api', contractTaskRoutes); app.use('/api/settings', appSettingRoutes); app.use('/api/email-providers', emailProviderRoutes); app.use('/api', cachedEmailRoutes); app.use('/api/energy-details', invoiceRoutes); app.use('/api', contractHistoryRoutes); app.use('/api/audit-logs', auditLogRoutes); app.use('/api/gdpr', gdprRoutes); app.use('/api/email-logs', emailLogRoutes); app.use('/api/pdf-templates', pdfTemplateRoutes); app.use('/api/birthdays', birthdayRoutes); app.use('/api/factory-defaults', factoryDefaultsRoutes); app.use('/api/monitoring', monitoringRoutes); // Health check app.get('/api/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }); }); // Production: Serve frontend static files if (process.env.NODE_ENV === 'production') { const publicPath = path.join(process.cwd(), 'public'); // Serve static files app.use(express.static(publicPath)); // SPA fallback: serve index.html for all non-API routes app.get('*', (req, res, next) => { // Skip API routes if (req.path.startsWith('/api')) { return next(); } res.sendFile(path.join(publicPath, 'index.html')); }); } // Error handling // body-parser wirft 413 (PayloadTooLargeError) bzw. 400 (SyntaxError) mit einem // `status`-Feld. Ohne Respektierung werden legitime Client-Fehler als 500 // kaschiert und landen als "Interner Serverfehler" beim User. app.use((err: Error & { status?: number; type?: string }, req: express.Request, res: express.Response, next: express.NextFunction) => { console.error(err.stack); const status = typeof err.status === 'number' && err.status >= 400 && err.status < 600 ? err.status : 500; let message = 'Interner Serverfehler'; if (status === 413) message = 'Anfrage zu groß'; else if (status === 400 && (err.type === 'entity.parse.failed' || err instanceof SyntaxError)) { message = 'Ungültiges JSON'; } res.status(status).json({ success: false, error: message }); }); // Listen-Adresse: in Production typischerweise 127.0.0.1 (nur lokaler // Reverse-Proxy soll connecten dürfen). LISTEN_ADDR per Env überschreibbar. const LISTEN_ADDR = process.env.LISTEN_ADDR || (process.env.NODE_ENV === 'production' ? '127.0.0.1' : '0.0.0.0'); app.listen(PORT as number, LISTEN_ADDR, () => { console.log(`Server läuft auf ${LISTEN_ADDR}:${PORT}`); // Hintergrund-Scheduler (Geburtstagsgrüße etc.) starten startBirthdayScheduler(); startContractStatusScheduler(); startSecurityMonitorScheduler(); });