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/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'; } /** * 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 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); const resourceId = mapping.extractId?.(req); const dataSubjectId = extractDataSubjectId(req); // Audit-Kontext abrufen (enthält Before/After-Werte von Prisma Middleware) const auditContext = getAuditContext(); // Label für bessere Lesbarkeit generieren let resourceLabel: string | undefined; if (responseBody && typeof responseBody === 'object' && 'data' in responseBody) { const data = (responseBody as { data: Record }).data; if (data) { // Versuche verschiedene Label-Felder resourceLabel = (data.contractNumber as string) || (data.customerNumber as string) || (data.name as string) || (data.email as string) || (data.firstName && data.lastName ? `${data.firstName} ${data.lastName}` : undefined); } } await createAuditLog({ userId: req.user?.userId, userEmail: req.user?.email || 'anonymous', userRole: req.user?.permissions?.join(', '), 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(); }