gdpr audit implemented, email log, vollmachten, pdf delete cancel data privacy and vollmachten, removed message no id card in engergy car, and other contracts that are not telecom contracts, added insert counter for engery
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
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, { type: string; extractId?: (req: AuthRequest) => 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<string, unknown> }).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();
|
||||
}
|
||||
Reference in New Issue
Block a user