import { Response, NextFunction } from 'express'; import { AuditAction } from '@prisma/client'; import { AuthRequest } from '../types/index.js'; import { createAuditLog } from '../services/audit.service.js'; import { getAuditContext, AuditContext } from './auditContext.js'; // Resource-Typ-Mapping basierend auf Route-Patterns const RESOURCE_MAPPING: Record string | undefined }> = { '/api/customers': { type: 'Customer', extractId: (req) => req.params.id || req.params.customerId }, '/api/customers/*/bank-cards': { type: 'BankCard', extractId: (req) => req.params.bankCardId }, '/api/customers/*/documents': { type: 'IdentityDocument', extractId: (req) => req.params.documentId }, '/api/customers/*/addresses': { type: 'Address', extractId: (req) => req.params.addressId }, '/api/customers/*/meters': { type: 'Meter', extractId: (req) => req.params.meterId }, '/api/customers/*/consents': { type: 'CustomerConsent', extractId: (req) => req.params.type }, '/api/contracts': { type: 'Contract', extractId: (req) => req.params.id }, '/api/contracts/*/history': { type: 'ContractHistoryEntry', extractId: (req) => req.params.entryId }, '/api/contracts/*/tasks': { type: 'ContractTask', extractId: (req) => req.params.taskId }, '/api/users': { type: 'User', extractId: (req) => req.params.id }, '/api/providers': { type: 'Provider', extractId: (req) => req.params.id }, '/api/tariffs': { type: 'Tariff', extractId: (req) => req.params.id }, '/api/platforms': { type: 'SalesPlatform', extractId: (req) => req.params.id }, '/api/contract-categories': { type: 'ContractCategory', extractId: (req) => req.params.id }, '/api/cancellation-periods': { type: 'CancellationPeriod', extractId: (req) => req.params.id }, '/api/contract-durations': { type: 'ContractDuration', extractId: (req) => req.params.id }, '/api/settings': { type: 'AppSetting', extractId: (req) => req.params.key }, '/api/email-providers': { type: 'EmailProviderConfig', extractId: (req) => req.params.id }, '/api/meters': { type: 'Meter', extractId: (req) => req.params.id || req.params.meterId }, '/api/upload': { type: 'Upload' }, '/api/email-logs': { type: 'EmailLog' }, '/api/auth': { type: 'Authentication' }, '/api/audit-logs': { type: 'AuditLog', extractId: (req) => req.params.id }, '/api/gdpr': { type: 'GDPR' }, }; // Routen die nicht geloggt werden sollen const EXCLUDED_ROUTES = [ '/api/health', '/api/uploads', ]; /** * Bestimmt die Aktion basierend auf HTTP-Methode und Erfolg */ function determineAction(method: string, path: string, success: boolean): AuditAction { // Spezielle Auth-Aktionen if (path.includes('/auth/login')) { return success ? 'LOGIN' : 'LOGIN_FAILED'; } if (path.includes('/auth/logout')) { return 'LOGOUT'; } // Standard CRUD-Aktionen switch (method.toUpperCase()) { case 'GET': return 'READ'; case 'POST': return 'CREATE'; case 'PUT': case 'PATCH': return 'UPDATE'; case 'DELETE': return 'DELETE'; default: return 'READ'; } } /** * Findet den passenden Resource-Typ für einen Pfad */ function findResourceMapping(path: string): { type: string; extractId?: (req: AuthRequest) => string | undefined } | null { // Exakte Matches zuerst prüfen for (const [pattern, mapping] of Object.entries(RESOURCE_MAPPING)) { // Konvertiere Pattern zu Regex const regexPattern = pattern .replace(/\*/g, '[^/]+') .replace(/\//g, '\\/'); const regex = new RegExp(`^${regexPattern}(?:/|$)`); if (regex.test(path)) { return mapping; } } return null; } /** * Extrahiert die betroffene Kunden-ID für DSGVO-Tracking */ function extractDataSubjectId(req: AuthRequest): number | undefined { // Aus Route-Parameter const customerId = req.params.customerId || req.params.id; if (customerId && req.path.includes('/customers')) { return parseInt(customerId); } // Aus Request-Body (bei Create) if (req.body?.customerId) { return parseInt(req.body.customerId); } // Bei Kundenportal-Zugriff if (req.user?.customerId) { return req.user.customerId; } return undefined; } /** * Extrahiert die IP-Adresse des Clients */ function getClientIp(req: AuthRequest): string { const forwarded = req.headers['x-forwarded-for']; if (typeof forwarded === 'string') { return forwarded.split(',')[0].trim(); } return req.socket.remoteAddress || 'unknown'; } // Menschenlesbare Bezeichnungen für Resource-Typen const RESOURCE_TYPE_LABELS: Record = { Customer: 'Kunde', Contract: 'Vertrag', BankCard: 'Bankverbindung', IdentityDocument: 'Ausweis', Address: 'Adresse', Meter: 'Zähler', MeterReading: 'Zählerstand', User: 'Benutzer', Provider: 'Anbieter', Tariff: 'Tarif', SalesPlatform: 'Vertriebsplattform', ContractCategory: 'Vertragskategorie', CancellationPeriod: 'Kündigungsfrist', ContractDuration: 'Vertragslaufzeit', EmailProviderConfig: 'E-Mail-Provider', AppSetting: 'Einstellung', CustomerConsent: 'Einwilligung', ContractTask: 'Aufgabe', ContractHistoryEntry: 'Vertragshistorie', GDPR: 'Datenschutz', Authentication: 'Anmeldung', AuditLog: 'Audit-Protokoll', StressfreiEmail: 'Stressfrei-E-Mail', CachedEmail: 'E-Mail', }; const ACTION_LABELS: Record = { CREATE: 'erstellt', READ: 'aufgerufen', UPDATE: 'aktualisiert', DELETE: 'gelöscht', EXPORT: 'exportiert', ANONYMIZE: 'anonymisiert', LOGIN: 'angemeldet', LOGOUT: 'abgemeldet', LOGIN_FAILED: 'Anmeldung fehlgeschlagen', }; /** * Erzeugt ein menschenlesbares Label für den Audit-Log-Eintrag */ function generateHumanLabel( action: AuditAction, resourceType: string, req: AuthRequest, responseBody: unknown ): string { const typeName = RESOURCE_TYPE_LABELS[resourceType] || resourceType; const actionName = ACTION_LABELS[action] || action; // Identifikator aus Response oder Request extrahieren let identifier = ''; if (responseBody && typeof responseBody === 'object' && 'data' in responseBody) { const data = (responseBody as { data: Record }).data; if (data) { identifier = (data.contractNumber as string) || (data.customerNumber as string) || (data.meterNumber as string) || (data.name as string) || (data.email as string) || (data.firstName && data.lastName ? `${data.firstName} ${data.lastName}` : '') || ''; } } // Spezial-Labels für bestimmte Endpunkte const path = req.path; // Auth if (path.includes('/auth/login') || path.includes('/auth/customer-login')) { const email = req.body?.email || ''; return action === 'LOGIN' ? `Benutzer ${email} hat sich angemeldet` : `Anmeldung fehlgeschlagen für ${email}`; } if (path.includes('/auth/logout')) return 'Benutzer hat sich abgemeldet'; // Kunden-Operationen if (resourceType === 'Customer') { if (action === 'CREATE') return `Kunde ${identifier} angelegt`; if (action === 'UPDATE') return `Kundendaten ${identifier} aktualisiert`; if (action === 'DELETE') return `Kunde ${identifier} gelöscht`; if (action === 'READ' && req.params.id) return `Kundendaten ${identifier} aufgerufen`; if (action === 'READ') return 'Kundenliste aufgerufen'; } // Verträge if (resourceType === 'Contract') { if (path.includes('/cockpit')) return 'Vertrags-Cockpit aufgerufen'; if (path.includes('/follow-up')) return `Folgevertrag für ${identifier} erstellt`; if (path.includes('/snooze')) return `Vertrag ${identifier} zurückgestellt`; if (path.includes('/password')) return `Passwort für Vertrag ${identifier} abgerufen`; if (path.includes('/sip-credentials')) return 'SIP-Zugangsdaten abgerufen'; if (path.includes('/internet-credentials')) return `Internet-Zugangsdaten für Vertrag ${identifier} abgerufen`; if (path.includes('/successor-meter')) return `Folgezähler zu Vertrag ${identifier} hinzugefügt`; if (action === 'CREATE') return `Vertrag ${identifier} angelegt`; if (action === 'UPDATE') return `Vertrag ${identifier} aktualisiert`; if (action === 'DELETE') return `Vertrag ${identifier} gelöscht`; if (action === 'READ' && req.params.id) return `Vertrag ${identifier} aufgerufen`; if (action === 'READ') return 'Vertragsliste aufgerufen'; } // Bankverbindungen if (resourceType === 'BankCard') { if (action === 'CREATE') return `Bankverbindung hinzugefügt`; if (action === 'UPDATE') return `Bankverbindung aktualisiert`; if (action === 'DELETE') return `Bankverbindung gelöscht`; } // Ausweise if (resourceType === 'IdentityDocument') { if (action === 'CREATE') return `Ausweis ${identifier} hinzugefügt`; if (action === 'UPDATE') return `Ausweis ${identifier} aktualisiert`; if (action === 'DELETE') return `Ausweis gelöscht`; } // Adressen if (resourceType === 'Address') { if (action === 'CREATE') return `Adresse hinzugefügt`; if (action === 'UPDATE') return `Adresse aktualisiert`; if (action === 'DELETE') return `Adresse gelöscht`; } // Zähler if (resourceType === 'Meter') { if (action === 'CREATE') return `Zähler ${identifier} angelegt`; if (action === 'UPDATE') return `Zähler ${identifier} aktualisiert`; if (action === 'DELETE') return `Zähler gelöscht`; } // Einwilligungen if (resourceType === 'CustomerConsent') { const consentType = req.params.consentType || ''; const consentLabels: Record = { DATA_PROCESSING: 'Datenverarbeitung', MARKETING_EMAIL: 'E-Mail-Marketing', MARKETING_PHONE: 'Telefonmarketing', DATA_SHARING_PARTNER: 'Datenweitergabe', }; const consentName = consentLabels[consentType] || consentType; if (action === 'UPDATE') { const status = req.body?.status; return status === 'GRANTED' ? `Einwilligung "${consentName}" erteilt` : `Einwilligung "${consentName}" widerrufen`; } if (action === 'READ') return 'Einwilligungen abgerufen'; } // Benutzer if (resourceType === 'User') { if (action === 'CREATE') return `Benutzer ${identifier} angelegt`; if (action === 'UPDATE') return `Benutzer ${identifier} aktualisiert`; if (action === 'DELETE') return `Benutzer ${identifier} gelöscht`; if (action === 'READ' && req.params.id) return `Benutzerdaten ${identifier} aufgerufen`; if (action === 'READ') return 'Benutzerliste aufgerufen'; } // Aufgaben if (resourceType === 'ContractTask') { if (path.includes('/complete')) return `Aufgabe als erledigt markiert`; if (action === 'CREATE') return `Aufgabe erstellt`; if (action === 'UPDATE') return `Aufgabe aktualisiert`; if (action === 'DELETE') return `Aufgabe gelöscht`; } // E-Mail-Provider if (resourceType === 'EmailProviderConfig') { if (path.includes('/test-connection')) return `E-Mail-Provider Verbindungstest`; if (path.includes('/provision')) return `E-Mail-Adresse provisioniert`; if (action === 'CREATE') return `E-Mail-Provider ${identifier} angelegt`; if (action === 'UPDATE') return `E-Mail-Provider ${identifier} aktualisiert`; if (action === 'DELETE') return `E-Mail-Provider ${identifier} gelöscht`; } // GDPR if (resourceType === 'GDPR') { if (path.includes('/dashboard')) return 'DSGVO-Dashboard aufgerufen'; if (path.includes('/export')) return 'Kundendaten exportiert (DSGVO Art. 15)'; if (path.includes('/privacy-policy')) { return action === 'UPDATE' ? 'Datenschutzerklärung aktualisiert' : 'Datenschutzerklärung aufgerufen'; } if (path.includes('/authorization-template')) { return action === 'UPDATE' ? 'Vollmacht-Vorlage aktualisiert' : 'Vollmacht-Vorlage aufgerufen'; } if (path.includes('/send-consent-link')) return 'Datenschutz-Link versendet'; if (path.includes('/authorizations') && path.includes('/send')) return 'Vollmacht-Anfrage versendet'; if (path.includes('/authorizations') && path.includes('/grant')) return 'Vollmacht erteilt'; if (path.includes('/authorizations') && path.includes('/withdraw')) return 'Vollmacht widerrufen'; if (path.includes('/authorizations') && path.includes('/upload')) return 'Vollmacht-PDF hochgeladen'; if (path.includes('/authorizations') && path.includes('/document') && action === 'DELETE') return 'Vollmacht-PDF gelöscht'; if (path.includes('/my-privacy')) return 'Eigene Datenschutzseite aufgerufen'; if (path.includes('/my-consent-status')) return 'Eigener Einwilligungsstatus geprüft'; if (path.includes('/my-authorizations')) return 'Eigene Vollmachten aufgerufen'; if (path.includes('/deletions')) { if (action === 'CREATE') return 'Löschanfrage erstellt'; if (path.includes('/process')) return 'Löschanfrage bearbeitet'; return 'Löschanfragen aufgerufen'; } if (path.includes('/consent-status')) return 'Einwilligungsstatus geprüft'; if (path.includes('/consents/overview')) return 'Einwilligungsübersicht aufgerufen'; } // Einstellungen if (resourceType === 'AppSetting') { if (action === 'UPDATE') return `Einstellung "${req.params.key || ''}" geändert`; if (action === 'READ') return 'Einstellungen aufgerufen'; } // Zähler-Readings if (path.includes('/readings')) { if (path.includes('/report')) return 'Zählerstand vom Kunden gemeldet'; if (path.includes('/transfer')) return 'Zählerstand als übertragen markiert'; if (action === 'CREATE') return 'Zählerstand erfasst'; if (action === 'UPDATE') return 'Zählerstand aktualisiert'; if (action === 'DELETE') return 'Zählerstand gelöscht'; } // Upload-Operationen if (path.includes('/upload') || path.includes('/privacy-policy')) { if (path.includes('/privacy-policy') && action === 'DELETE') return 'Datenschutzerklärung-PDF gelöscht'; if (path.includes('/privacy-policy')) return 'Datenschutzerklärung-PDF hochgeladen'; } // Standard-Fallback if (identifier) { return `${typeName} ${identifier} ${actionName}`; } return `${typeName} ${actionName}`; } /** * Audit Middleware - loggt alle API-Aufrufe asynchron */ export function auditMiddleware(req: AuthRequest, res: Response, next: NextFunction): void { const startTime = Date.now(); // Ausgeschlossene Routen überspringen if (EXCLUDED_ROUTES.some((route) => req.path.startsWith(route))) { next(); return; } // Resource-Mapping finden const mapping = findResourceMapping(req.path); if (!mapping) { // Unbekannte Route - trotzdem loggen mit generischem Typ next(); return; } // Original res.json überschreiben um Response zu capturen const originalJson = res.json.bind(res); let responseBody: unknown = null; let responseSuccess = true; res.json = function (body: unknown) { responseBody = body; if (typeof body === 'object' && body !== null && 'success' in body) { responseSuccess = (body as { success: boolean }).success; } return originalJson(body); }; // Response-Ende abfangen für Logging // Audit-Kontext hier erfassen (bevor AsyncLocalStorage den Kontext verliert) let capturedAuditContext: ReturnType | undefined; const origEnd = res.end; (res as any).end = function(chunk?: any, encoding?: any, cb?: any) { // Kontext VOR dem Ende erfassen capturedAuditContext = getAuditContext(); return origEnd.call(this, chunk, encoding, cb); }; res.on('finish', () => { // Async Logging - blockiert nicht die Response setImmediate(async () => { try { const durationMs = Date.now() - startTime; const action = determineAction(req.method, req.path, responseSuccess); // READ-Aktionen nicht loggen (nur Änderungen, Logins und Exporte) if (action === 'READ') return; // Routen die bereits gezielt via logChange() geloggt werden → nicht doppelt loggen const manuallyLoggedPaths = [ '/api/customers', '/api/contracts', '/api/meters', '/api/gdpr', '/api/upload', ]; // Login/Logout immer loggen if (action !== 'LOGIN' && action !== 'LOGOUT' && action !== 'LOGIN_FAILED') { if (manuallyLoggedPaths.some(p => req.originalUrl?.startsWith(p) || req.baseUrl?.startsWith(p))) return; } const resourceId = mapping.extractId?.(req); const dataSubjectId = extractDataSubjectId(req); // Audit-Kontext nutzen (wurde vor Response-Ende erfasst) const auditContext = capturedAuditContext; // Menschenlesbares Label generieren const resourceLabel = generateHumanLabel(action, mapping.type, req, responseBody); await createAuditLog({ userId: req.user?.userId, userEmail: req.user?.email || 'anonymous', userRole: req.user?.isCustomerPortal ? 'Kundenportal' : (req.user as any)?.roleName || 'Mitarbeiter', customerId: req.user?.customerId, isCustomerPortal: req.user?.isCustomerPortal, action, resourceType: mapping.type, resourceId, resourceLabel, endpoint: req.path, httpMethod: req.method, ipAddress: getClientIp(req), userAgent: req.headers['user-agent'], changesBefore: auditContext?.before, changesAfter: auditContext?.after, dataSubjectId, success: responseSuccess, errorMessage: !responseSuccess && responseBody && typeof responseBody === 'object' && 'error' in responseBody ? (responseBody as { error: string }).error : undefined, durationMs, }); } catch (error) { // Audit-Logging darf niemals die Anwendung beeinträchtigen console.error('[AuditMiddleware] Fehler beim Logging:', error); } }); }); next(); }