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,504 @@
|
||||
import { AuditAction, AuditSensitivity, Prisma } from '@prisma/client';
|
||||
import crypto from 'crypto';
|
||||
import { encrypt, decrypt } from '../utils/encryption.js';
|
||||
import prisma from '../lib/prisma.js';
|
||||
|
||||
export interface CreateAuditLogData {
|
||||
userId?: number;
|
||||
userEmail: string;
|
||||
userRole?: string;
|
||||
customerId?: number;
|
||||
isCustomerPortal?: boolean;
|
||||
action: AuditAction;
|
||||
sensitivity?: AuditSensitivity;
|
||||
resourceType: string;
|
||||
resourceId?: string;
|
||||
resourceLabel?: string;
|
||||
endpoint: string;
|
||||
httpMethod: string;
|
||||
ipAddress: string;
|
||||
userAgent?: string;
|
||||
changesBefore?: Record<string, unknown>;
|
||||
changesAfter?: Record<string, unknown>;
|
||||
dataSubjectId?: number;
|
||||
legalBasis?: string;
|
||||
success?: boolean;
|
||||
errorMessage?: string;
|
||||
durationMs?: number;
|
||||
}
|
||||
|
||||
export interface AuditLogSearchParams {
|
||||
userId?: number;
|
||||
customerId?: number;
|
||||
dataSubjectId?: number;
|
||||
action?: AuditAction;
|
||||
sensitivity?: AuditSensitivity;
|
||||
resourceType?: string;
|
||||
resourceId?: string;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
success?: boolean;
|
||||
search?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert einen SHA-256 Hash für einen Audit-Log-Eintrag
|
||||
*/
|
||||
function generateHash(data: {
|
||||
userEmail: string;
|
||||
action: AuditAction;
|
||||
resourceType: string;
|
||||
resourceId?: string | null;
|
||||
endpoint: string;
|
||||
createdAt: Date;
|
||||
previousHash?: string | null;
|
||||
}): string {
|
||||
const content = JSON.stringify({
|
||||
userEmail: data.userEmail,
|
||||
action: data.action,
|
||||
resourceType: data.resourceType,
|
||||
resourceId: data.resourceId,
|
||||
endpoint: data.endpoint,
|
||||
createdAt: data.createdAt.toISOString(),
|
||||
previousHash: data.previousHash || '',
|
||||
});
|
||||
return crypto.createHash('sha256').update(content).digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Bestimmt die Sensitivität basierend auf dem Ressourcentyp
|
||||
*/
|
||||
function determineSensitivity(resourceType: string): AuditSensitivity {
|
||||
const sensitivityMap: Record<string, AuditSensitivity> = {
|
||||
// CRITICAL
|
||||
Authentication: 'CRITICAL',
|
||||
BankCard: 'CRITICAL',
|
||||
IdentityDocument: 'CRITICAL',
|
||||
// HIGH
|
||||
Customer: 'HIGH',
|
||||
User: 'HIGH',
|
||||
CustomerConsent: 'HIGH',
|
||||
DataDeletionRequest: 'HIGH',
|
||||
// MEDIUM
|
||||
Contract: 'MEDIUM',
|
||||
Address: 'MEDIUM',
|
||||
Meter: 'MEDIUM',
|
||||
MeterReading: 'MEDIUM',
|
||||
StressfreiEmail: 'MEDIUM',
|
||||
CachedEmail: 'MEDIUM',
|
||||
// LOW
|
||||
Provider: 'LOW',
|
||||
Tariff: 'LOW',
|
||||
SalesPlatform: 'LOW',
|
||||
AppSetting: 'LOW',
|
||||
ContractCategory: 'LOW',
|
||||
CancellationPeriod: 'LOW',
|
||||
ContractDuration: 'LOW',
|
||||
};
|
||||
return sensitivityMap[resourceType] || 'MEDIUM';
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob Änderungen verschlüsselt werden sollen
|
||||
*/
|
||||
function shouldEncryptChanges(resourceType: string): boolean {
|
||||
const encryptedTypes = [
|
||||
'BankCard',
|
||||
'IdentityDocument',
|
||||
'User',
|
||||
'Customer', // Enthält Portal-Passwörter
|
||||
];
|
||||
return encryptedTypes.includes(resourceType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt einen neuen Audit-Log-Eintrag mit Hash-Kette
|
||||
*/
|
||||
export async function createAuditLog(data: CreateAuditLogData): Promise<void> {
|
||||
try {
|
||||
// Letzten Hash abrufen für die Kette
|
||||
const lastLog = await prisma.auditLog.findFirst({
|
||||
orderBy: { id: 'desc' },
|
||||
select: { hash: true },
|
||||
});
|
||||
|
||||
const previousHash = lastLog?.hash || null;
|
||||
const createdAt = new Date();
|
||||
|
||||
// Sensitivität bestimmen falls nicht angegeben
|
||||
const sensitivity = data.sensitivity || determineSensitivity(data.resourceType);
|
||||
|
||||
// Änderungen serialisieren und ggf. verschlüsseln
|
||||
let changesBefore: string | null = null;
|
||||
let changesAfter: string | null = null;
|
||||
let changesEncrypted = false;
|
||||
|
||||
if (data.changesBefore || data.changesAfter) {
|
||||
changesEncrypted = shouldEncryptChanges(data.resourceType);
|
||||
|
||||
if (data.changesBefore) {
|
||||
const json = JSON.stringify(data.changesBefore);
|
||||
changesBefore = changesEncrypted ? encrypt(json) : json;
|
||||
}
|
||||
|
||||
if (data.changesAfter) {
|
||||
const json = JSON.stringify(data.changesAfter);
|
||||
changesAfter = changesEncrypted ? encrypt(json) : json;
|
||||
}
|
||||
}
|
||||
|
||||
// Hash generieren
|
||||
const hash = generateHash({
|
||||
userEmail: data.userEmail,
|
||||
action: data.action,
|
||||
resourceType: data.resourceType,
|
||||
resourceId: data.resourceId,
|
||||
endpoint: data.endpoint,
|
||||
createdAt,
|
||||
previousHash,
|
||||
});
|
||||
|
||||
// Eintrag erstellen
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
userId: data.userId,
|
||||
userEmail: data.userEmail,
|
||||
userRole: data.userRole,
|
||||
customerId: data.customerId,
|
||||
isCustomerPortal: data.isCustomerPortal || false,
|
||||
action: data.action,
|
||||
sensitivity,
|
||||
resourceType: data.resourceType,
|
||||
resourceId: data.resourceId,
|
||||
resourceLabel: data.resourceLabel,
|
||||
endpoint: data.endpoint,
|
||||
httpMethod: data.httpMethod,
|
||||
ipAddress: data.ipAddress,
|
||||
userAgent: data.userAgent,
|
||||
changesBefore,
|
||||
changesAfter,
|
||||
changesEncrypted,
|
||||
dataSubjectId: data.dataSubjectId,
|
||||
legalBasis: data.legalBasis,
|
||||
success: data.success ?? true,
|
||||
errorMessage: data.errorMessage,
|
||||
durationMs: data.durationMs,
|
||||
createdAt,
|
||||
hash,
|
||||
previousHash,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
// Audit-Logging darf niemals die Hauptoperation blockieren
|
||||
console.error('[AuditService] Fehler beim Erstellen des Audit-Logs:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sucht Audit-Logs mit Filtern und Paginierung
|
||||
*/
|
||||
export async function searchAuditLogs(params: AuditLogSearchParams) {
|
||||
const {
|
||||
userId,
|
||||
customerId,
|
||||
dataSubjectId,
|
||||
action,
|
||||
sensitivity,
|
||||
resourceType,
|
||||
resourceId,
|
||||
startDate,
|
||||
endDate,
|
||||
success,
|
||||
search,
|
||||
page = 1,
|
||||
limit = 50,
|
||||
} = params;
|
||||
|
||||
const where: Prisma.AuditLogWhereInput = {};
|
||||
|
||||
if (userId !== undefined) where.userId = userId;
|
||||
if (customerId !== undefined) where.customerId = customerId;
|
||||
if (dataSubjectId !== undefined) where.dataSubjectId = dataSubjectId;
|
||||
if (action) where.action = action;
|
||||
if (sensitivity) where.sensitivity = sensitivity;
|
||||
if (resourceType) where.resourceType = resourceType;
|
||||
if (resourceId) where.resourceId = resourceId;
|
||||
if (success !== undefined) where.success = success;
|
||||
|
||||
if (startDate || endDate) {
|
||||
where.createdAt = {};
|
||||
if (startDate) where.createdAt.gte = startDate;
|
||||
if (endDate) where.createdAt.lte = endDate;
|
||||
}
|
||||
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ userEmail: { contains: search } },
|
||||
{ resourceLabel: { contains: search } },
|
||||
{ endpoint: { contains: search } },
|
||||
];
|
||||
}
|
||||
|
||||
const [logs, total] = await Promise.all([
|
||||
prisma.auditLog.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * limit,
|
||||
take: limit,
|
||||
}),
|
||||
prisma.auditLog.count({ where }),
|
||||
]);
|
||||
|
||||
// Entschlüsselung der Änderungen wenn nötig
|
||||
const decryptedLogs = logs.map((log) => ({
|
||||
...log,
|
||||
changesBefore: log.changesBefore && log.changesEncrypted
|
||||
? JSON.parse(decrypt(log.changesBefore))
|
||||
: log.changesBefore ? JSON.parse(log.changesBefore) : null,
|
||||
changesAfter: log.changesAfter && log.changesEncrypted
|
||||
? JSON.parse(decrypt(log.changesAfter))
|
||||
: log.changesAfter ? JSON.parse(log.changesAfter) : null,
|
||||
}));
|
||||
|
||||
return {
|
||||
data: decryptedLogs,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Holt einen einzelnen Audit-Log-Eintrag
|
||||
*/
|
||||
export async function getAuditLogById(id: number) {
|
||||
const log = await prisma.auditLog.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!log) return null;
|
||||
|
||||
return {
|
||||
...log,
|
||||
changesBefore: log.changesBefore && log.changesEncrypted
|
||||
? JSON.parse(decrypt(log.changesBefore))
|
||||
: log.changesBefore ? JSON.parse(log.changesBefore) : null,
|
||||
changesAfter: log.changesAfter && log.changesEncrypted
|
||||
? JSON.parse(decrypt(log.changesAfter))
|
||||
: log.changesAfter ? JSON.parse(log.changesAfter) : null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Holt alle Audit-Logs für eine betroffene Person (DSGVO)
|
||||
*/
|
||||
export async function getAuditLogsByDataSubject(customerId: number) {
|
||||
const logs = await prisma.auditLog.findMany({
|
||||
where: { dataSubjectId: customerId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
return logs.map((log) => ({
|
||||
...log,
|
||||
changesBefore: log.changesBefore && log.changesEncrypted
|
||||
? JSON.parse(decrypt(log.changesBefore))
|
||||
: log.changesBefore ? JSON.parse(log.changesBefore) : null,
|
||||
changesAfter: log.changesAfter && log.changesEncrypted
|
||||
? JSON.parse(decrypt(log.changesAfter))
|
||||
: log.changesAfter ? JSON.parse(log.changesAfter) : null,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifiziert die Integrität der Hash-Kette
|
||||
*/
|
||||
export async function verifyIntegrity(fromId?: number, toId?: number): Promise<{
|
||||
valid: boolean;
|
||||
checkedCount: number;
|
||||
invalidEntries: number[];
|
||||
}> {
|
||||
const where: Prisma.AuditLogWhereInput = {};
|
||||
|
||||
if (fromId !== undefined) where.id = { gte: fromId };
|
||||
if (toId !== undefined) where.id = { ...(where.id as object || {}), lte: toId };
|
||||
|
||||
const logs = await prisma.auditLog.findMany({
|
||||
where,
|
||||
orderBy: { id: 'asc' },
|
||||
select: {
|
||||
id: true,
|
||||
userEmail: true,
|
||||
action: true,
|
||||
resourceType: true,
|
||||
resourceId: true,
|
||||
endpoint: true,
|
||||
createdAt: true,
|
||||
hash: true,
|
||||
previousHash: true,
|
||||
},
|
||||
});
|
||||
|
||||
const invalidEntries: number[] = [];
|
||||
|
||||
for (let i = 0; i < logs.length; i++) {
|
||||
const log = logs[i];
|
||||
|
||||
// Hash neu berechnen
|
||||
const expectedHash = generateHash({
|
||||
userEmail: log.userEmail,
|
||||
action: log.action,
|
||||
resourceType: log.resourceType,
|
||||
resourceId: log.resourceId,
|
||||
endpoint: log.endpoint,
|
||||
createdAt: log.createdAt,
|
||||
previousHash: log.previousHash,
|
||||
});
|
||||
|
||||
// Prüfen ob Hash übereinstimmt
|
||||
if (log.hash !== expectedHash) {
|
||||
invalidEntries.push(log.id);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Prüfen ob previousHash mit dem Hash des vorherigen Eintrags übereinstimmt
|
||||
if (i > 0) {
|
||||
const previousLog = logs[i - 1];
|
||||
if (log.previousHash !== previousLog.hash) {
|
||||
invalidEntries.push(log.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valid: invalidEntries.length === 0,
|
||||
checkedCount: logs.length,
|
||||
invalidEntries,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Exportiert Audit-Logs als JSON oder CSV
|
||||
*/
|
||||
export async function exportAuditLogs(
|
||||
params: AuditLogSearchParams,
|
||||
format: 'json' | 'csv' = 'json'
|
||||
): Promise<string> {
|
||||
// Alle Logs ohne Paginierung
|
||||
const result = await searchAuditLogs({ ...params, limit: 100000, page: 1 });
|
||||
const logs = result.data;
|
||||
|
||||
if (format === 'json') {
|
||||
return JSON.stringify(logs, null, 2);
|
||||
}
|
||||
|
||||
// CSV Export
|
||||
const headers = [
|
||||
'ID',
|
||||
'Zeitstempel',
|
||||
'Benutzer',
|
||||
'Aktion',
|
||||
'Ressource',
|
||||
'Ressource-ID',
|
||||
'Bezeichnung',
|
||||
'Endpoint',
|
||||
'IP-Adresse',
|
||||
'Erfolg',
|
||||
'Sensitivität',
|
||||
];
|
||||
|
||||
const rows = logs.map((log) => [
|
||||
log.id.toString(),
|
||||
log.createdAt.toISOString(),
|
||||
log.userEmail,
|
||||
log.action,
|
||||
log.resourceType,
|
||||
log.resourceId || '',
|
||||
log.resourceLabel || '',
|
||||
log.endpoint,
|
||||
log.ipAddress,
|
||||
log.success ? 'Ja' : 'Nein',
|
||||
log.sensitivity,
|
||||
]);
|
||||
|
||||
const csvContent = [
|
||||
headers.join(';'),
|
||||
...rows.map((row) => row.map((cell) => `"${cell.replace(/"/g, '""')}"`).join(';')),
|
||||
].join('\n');
|
||||
|
||||
return csvContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Löscht alte Audit-Logs basierend auf Retention-Policies
|
||||
* Hinweis: Diese Funktion sollte nur von einem autorisierten Admin-Prozess aufgerufen werden
|
||||
*/
|
||||
export async function runRetentionCleanup(): Promise<{
|
||||
deletedCount: number;
|
||||
policies: Array<{ resourceType: string; sensitivity: string | null; deletedCount: number }>;
|
||||
}> {
|
||||
const policies = await prisma.auditRetentionPolicy.findMany({
|
||||
where: { isActive: true },
|
||||
});
|
||||
|
||||
const results: Array<{ resourceType: string; sensitivity: string | null; deletedCount: number }> = [];
|
||||
let totalDeleted = 0;
|
||||
|
||||
for (const policy of policies) {
|
||||
const cutoffDate = new Date();
|
||||
cutoffDate.setDate(cutoffDate.getDate() - policy.retentionDays);
|
||||
|
||||
const where: Prisma.AuditLogWhereInput = {
|
||||
createdAt: { lt: cutoffDate },
|
||||
};
|
||||
|
||||
if (policy.resourceType !== '*') {
|
||||
where.resourceType = policy.resourceType;
|
||||
}
|
||||
|
||||
if (policy.sensitivity) {
|
||||
where.sensitivity = policy.sensitivity;
|
||||
}
|
||||
|
||||
const deleted = await prisma.auditLog.deleteMany({ where });
|
||||
|
||||
results.push({
|
||||
resourceType: policy.resourceType,
|
||||
sensitivity: policy.sensitivity,
|
||||
deletedCount: deleted.count,
|
||||
});
|
||||
|
||||
totalDeleted += deleted.count;
|
||||
}
|
||||
|
||||
return {
|
||||
deletedCount: totalDeleted,
|
||||
policies: results,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Holt die Retention-Policies
|
||||
*/
|
||||
export async function getRetentionPolicies() {
|
||||
return prisma.auditRetentionPolicy.findMany({
|
||||
orderBy: [{ resourceType: 'asc' }, { sensitivity: 'asc' }],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert eine Retention-Policy
|
||||
*/
|
||||
export async function updateRetentionPolicy(
|
||||
id: number,
|
||||
data: { retentionDays?: number; description?: string; legalBasis?: string; isActive?: boolean }
|
||||
) {
|
||||
return prisma.auditRetentionPolicy.update({
|
||||
where: { id },
|
||||
data,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user