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:
@@ -9,6 +9,9 @@ const DEFAULT_SETTINGS: Record<string, string> = {
|
||||
deadlineCriticalDays: '14', // Rot: Kritisch
|
||||
deadlineWarningDays: '42', // Gelb: Warnung (6 Wochen)
|
||||
deadlineOkDays: '90', // Grün: OK (3 Monate)
|
||||
// Ausweis-Ablauf: Fristenschwellen (in Tagen)
|
||||
documentExpiryCriticalDays: '30', // Rot: Kritisch (Standard 30 Tage)
|
||||
documentExpiryWarningDays: '90', // Gelb: Warnung (Standard 90 Tage)
|
||||
};
|
||||
|
||||
export async function getSetting(key: string): Promise<string | null> {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -283,6 +283,9 @@ export async function getUserById(id: number) {
|
||||
lastName: user.lastName,
|
||||
isActive: user.isActive,
|
||||
customerId: user.customerId,
|
||||
whatsappNumber: user.whatsappNumber,
|
||||
telegramUsername: user.telegramUsername,
|
||||
signalNumber: user.signalNumber,
|
||||
roles: user.roles.map((ur) => ur.role.name),
|
||||
permissions: Array.from(permissions),
|
||||
isCustomerPortal: false,
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
import prisma from '../lib/prisma.js';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* Vollmachten für einen Kunden abrufen (wer darf diesen Kunden einsehen?)
|
||||
*/
|
||||
export async function getAuthorizationsForCustomer(customerId: number) {
|
||||
return prisma.representativeAuthorization.findMany({
|
||||
where: { customerId },
|
||||
include: {
|
||||
representative: {
|
||||
select: { id: true, customerNumber: true, firstName: true, lastName: true },
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Vollmachten die ein Vertreter erhalten hat (welche Kunden darf er einsehen?)
|
||||
*/
|
||||
export async function getAuthorizationsForRepresentative(representativeId: number) {
|
||||
return prisma.representativeAuthorization.findMany({
|
||||
where: { representativeId },
|
||||
include: {
|
||||
customer: {
|
||||
select: { id: true, customerNumber: true, firstName: true, lastName: true },
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob ein Vertreter eine Vollmacht für einen Kunden hat
|
||||
*/
|
||||
export async function hasAuthorization(customerId: number, representativeId: number): Promise<boolean> {
|
||||
const auth = await prisma.representativeAuthorization.findUnique({
|
||||
where: {
|
||||
customerId_representativeId: { customerId, representativeId },
|
||||
},
|
||||
});
|
||||
|
||||
return auth?.isGranted === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vollmacht erteilen oder aktualisieren
|
||||
*/
|
||||
export async function grantAuthorization(
|
||||
customerId: number,
|
||||
representativeId: number,
|
||||
data: { source?: string; documentPath?: string; notes?: string }
|
||||
) {
|
||||
return prisma.representativeAuthorization.upsert({
|
||||
where: {
|
||||
customerId_representativeId: { customerId, representativeId },
|
||||
},
|
||||
update: {
|
||||
isGranted: true,
|
||||
grantedAt: new Date(),
|
||||
withdrawnAt: null,
|
||||
source: data.source,
|
||||
documentPath: data.documentPath ?? undefined,
|
||||
notes: data.notes ?? undefined,
|
||||
},
|
||||
create: {
|
||||
customerId,
|
||||
representativeId,
|
||||
isGranted: true,
|
||||
grantedAt: new Date(),
|
||||
source: data.source || 'crm-backend',
|
||||
documentPath: data.documentPath,
|
||||
notes: data.notes,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Vollmacht widerrufen + PDF löschen falls vorhanden
|
||||
*/
|
||||
export async function withdrawAuthorization(customerId: number, representativeId: number) {
|
||||
// Erst prüfen ob eine PDF vorhanden ist
|
||||
const existing = await prisma.representativeAuthorization.findUnique({
|
||||
where: { customerId_representativeId: { customerId, representativeId } },
|
||||
select: { documentPath: true },
|
||||
});
|
||||
|
||||
// PDF vom Filesystem löschen
|
||||
if (existing?.documentPath) {
|
||||
try {
|
||||
const filePath = path.join(process.cwd(), existing.documentPath);
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Löschen der Vollmacht-PDF:', err);
|
||||
}
|
||||
}
|
||||
|
||||
return prisma.representativeAuthorization.update({
|
||||
where: {
|
||||
customerId_representativeId: { customerId, representativeId },
|
||||
},
|
||||
data: {
|
||||
isGranted: false,
|
||||
withdrawnAt: new Date(),
|
||||
documentPath: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Vollmacht-Dokument (PDF) hochladen
|
||||
*/
|
||||
export async function updateAuthorizationDocument(
|
||||
customerId: number,
|
||||
representativeId: number,
|
||||
documentPath: string
|
||||
) {
|
||||
// Wenn Dokument hochgeladen wird, gilt das als Vollmacht erteilen
|
||||
return prisma.representativeAuthorization.upsert({
|
||||
where: {
|
||||
customerId_representativeId: { customerId, representativeId },
|
||||
},
|
||||
update: {
|
||||
documentPath,
|
||||
isGranted: true,
|
||||
grantedAt: new Date(),
|
||||
withdrawnAt: null,
|
||||
source: 'papier',
|
||||
},
|
||||
create: {
|
||||
customerId,
|
||||
representativeId,
|
||||
documentPath,
|
||||
isGranted: true,
|
||||
grantedAt: new Date(),
|
||||
source: 'papier',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Vollmacht-Dokument löschen
|
||||
*/
|
||||
export async function deleteAuthorizationDocument(customerId: number, representativeId: number) {
|
||||
return prisma.representativeAuthorization.update({
|
||||
where: {
|
||||
customerId_representativeId: { customerId, representativeId },
|
||||
},
|
||||
data: {
|
||||
documentPath: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Alle genehmigten Vertreter-IDs für einen Kunden
|
||||
* (Welche Vertreter dürfen die Verträge dieses Kunden sehen?)
|
||||
*/
|
||||
export async function getAuthorizedRepresentativeIds(customerId: number): Promise<number[]> {
|
||||
const auths = await prisma.representativeAuthorization.findMany({
|
||||
where: { customerId, isGranted: true },
|
||||
select: { representativeId: true },
|
||||
});
|
||||
return auths.map((a) => a.representativeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alle Kunden-IDs für die ein Vertreter eine Vollmacht hat
|
||||
*/
|
||||
export async function getAuthorizedCustomerIds(representativeId: number): Promise<number[]> {
|
||||
const auths = await prisma.representativeAuthorization.findMany({
|
||||
where: { representativeId, isGranted: true },
|
||||
select: { customerId: true },
|
||||
});
|
||||
return auths.map((a) => a.customerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt fehlende Vollmacht-Einträge für bestehende Vertreterbeziehungen
|
||||
* (wird aufgerufen wenn man den Tab aufruft)
|
||||
*/
|
||||
export async function ensureAuthorizationEntries(customerId: number) {
|
||||
// Alle aktiven Vertreter für diesen Kunden
|
||||
const representatives = await prisma.customerRepresentative.findMany({
|
||||
where: { customerId, isActive: true },
|
||||
select: { representativeId: true },
|
||||
});
|
||||
|
||||
for (const rep of representatives) {
|
||||
// Erstelle Eintrag falls nicht vorhanden
|
||||
await prisma.representativeAuthorization.upsert({
|
||||
where: {
|
||||
customerId_representativeId: { customerId, representativeId: rep.representativeId },
|
||||
},
|
||||
update: {}, // Nichts ändern wenn schon vorhanden
|
||||
create: {
|
||||
customerId,
|
||||
representativeId: rep.representativeId,
|
||||
isGranted: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
import { ConsentType, ConsentStatus } from '@prisma/client';
|
||||
import crypto from 'crypto';
|
||||
import prisma from '../lib/prisma.js';
|
||||
import * as consentService from './consent.service.js';
|
||||
import * as appSettingService from './appSetting.service.js';
|
||||
import PDFDocument from 'pdfkit';
|
||||
|
||||
/**
|
||||
* Kunden-Lookup per consentHash
|
||||
*/
|
||||
export async function getCustomerByConsentHash(hash: string) {
|
||||
const customer = await prisma.customer.findUnique({
|
||||
where: { consentHash: hash },
|
||||
select: {
|
||||
id: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
customerNumber: true,
|
||||
salutation: true,
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!customer) return null;
|
||||
|
||||
const consents = await consentService.getCustomerConsents(customer.id);
|
||||
|
||||
return { customer, consents };
|
||||
}
|
||||
|
||||
/**
|
||||
* Alle 4 Einwilligungen über den öffentlichen Link erteilen
|
||||
*/
|
||||
export async function grantAllConsentsPublic(hash: string, ipAddress: string) {
|
||||
const customer = await prisma.customer.findUnique({
|
||||
where: { consentHash: hash },
|
||||
select: { id: true, firstName: true, lastName: true },
|
||||
});
|
||||
|
||||
if (!customer) {
|
||||
throw new Error('Ungültiger Link');
|
||||
}
|
||||
|
||||
const results = [];
|
||||
for (const type of Object.values(ConsentType)) {
|
||||
const result = await consentService.updateConsent(customer.id, type, {
|
||||
status: ConsentStatus.GRANTED,
|
||||
source: 'public-link',
|
||||
ipAddress,
|
||||
createdBy: `${customer.firstName} ${customer.lastName} (Public-Link)`,
|
||||
});
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* consentHash generieren falls nicht vorhanden
|
||||
*/
|
||||
export async function ensureConsentHash(customerId: number): Promise<string> {
|
||||
const customer = await prisma.customer.findUnique({
|
||||
where: { id: customerId },
|
||||
select: { consentHash: true },
|
||||
});
|
||||
|
||||
if (!customer) {
|
||||
throw new Error('Kunde nicht gefunden');
|
||||
}
|
||||
|
||||
if (customer.consentHash) {
|
||||
return customer.consentHash;
|
||||
}
|
||||
|
||||
const hash = crypto.randomUUID();
|
||||
await prisma.customer.update({
|
||||
where: { id: customerId },
|
||||
data: { consentHash: hash },
|
||||
});
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Platzhalter in Text ersetzen
|
||||
*/
|
||||
function replacePlaceholders(html: string, customer: {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
customerNumber: string;
|
||||
salutation?: string | null;
|
||||
email?: string | null;
|
||||
}): string {
|
||||
return html
|
||||
.replace(/\{\{vorname\}\}/gi, customer.firstName || '')
|
||||
.replace(/\{\{nachname\}\}/gi, customer.lastName || '')
|
||||
.replace(/\{\{kundennummer\}\}/gi, customer.customerNumber || '')
|
||||
.replace(/\{\{anrede\}\}/gi, customer.salutation || '')
|
||||
.replace(/\{\{email\}\}/gi, customer.email || '')
|
||||
.replace(/\{\{datum\}\}/gi, new Date().toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Datenschutzerklärung als HTML abrufen (mit Platzhaltern ersetzt)
|
||||
*/
|
||||
export async function getPrivacyPolicyHtml(customerId?: number): Promise<string> {
|
||||
const html = await appSettingService.getSetting('privacyPolicyHtml');
|
||||
|
||||
if (!html) {
|
||||
return '<p>Keine Datenschutzerklärung hinterlegt.</p>';
|
||||
}
|
||||
|
||||
if (!customerId) return html;
|
||||
|
||||
const customer = await prisma.customer.findUnique({
|
||||
where: { id: customerId },
|
||||
select: {
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
customerNumber: true,
|
||||
salutation: true,
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!customer) return html;
|
||||
|
||||
return replacePlaceholders(html, customer);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML zu Plain-Text konvertieren (für PDF)
|
||||
*/
|
||||
function htmlToText(html: string): string {
|
||||
return html
|
||||
.replace(/<h[1-6][^>]*>(.*?)<\/h[1-6]>/gi, '\n$1\n')
|
||||
.replace(/<br\s*\/?>/gi, '\n')
|
||||
.replace(/<\/p>/gi, '\n\n')
|
||||
.replace(/<li[^>]*>(.*?)<\/li>/gi, ' • $1\n')
|
||||
.replace(/<[^>]+>/g, '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/ /g, ' ')
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Datenschutzerklärung als PDF generieren
|
||||
*/
|
||||
export async function generateConsentPdf(customerId: number): Promise<Buffer> {
|
||||
const html = await getPrivacyPolicyHtml(customerId);
|
||||
const text = htmlToText(html);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const doc = new PDFDocument({ size: 'A4', margin: 50 });
|
||||
const chunks: Buffer[] = [];
|
||||
|
||||
doc.on('data', (chunk: Buffer) => chunks.push(chunk));
|
||||
doc.on('end', () => resolve(Buffer.concat(chunks)));
|
||||
doc.on('error', reject);
|
||||
|
||||
// Titel
|
||||
doc.fontSize(18).font('Helvetica-Bold').text('Datenschutzerklärung', { align: 'center' });
|
||||
doc.moveDown(1);
|
||||
|
||||
// Datum
|
||||
doc.fontSize(10).font('Helvetica')
|
||||
.text(`Erstellt am: ${new Date().toLocaleDateString('de-DE')}`, { align: 'right' });
|
||||
doc.moveDown(1);
|
||||
|
||||
// Inhalt
|
||||
doc.fontSize(11).font('Helvetica').text(text, {
|
||||
align: 'left',
|
||||
lineGap: 4,
|
||||
});
|
||||
|
||||
doc.end();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
import { ConsentType, ConsentStatus } from '@prisma/client';
|
||||
import prisma from '../lib/prisma.js';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
export interface UpdateConsentData {
|
||||
status: ConsentStatus;
|
||||
source?: string;
|
||||
documentPath?: string;
|
||||
version?: string;
|
||||
ipAddress?: string;
|
||||
createdBy: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Holt alle Einwilligungen eines Kunden
|
||||
*/
|
||||
export async function getCustomerConsents(customerId: number) {
|
||||
const consents = await prisma.customerConsent.findMany({
|
||||
where: { customerId },
|
||||
orderBy: { consentType: 'asc' },
|
||||
});
|
||||
|
||||
// Alle verfügbaren Consent-Typen mit Status
|
||||
const allTypes = Object.values(ConsentType);
|
||||
const consentMap = new Map(consents.map((c) => [c.consentType, c]));
|
||||
|
||||
return allTypes.map((type) => {
|
||||
const existing = consentMap.get(type);
|
||||
return existing || {
|
||||
id: null,
|
||||
customerId,
|
||||
consentType: type,
|
||||
status: 'PENDING' as ConsentStatus,
|
||||
grantedAt: null,
|
||||
withdrawnAt: null,
|
||||
source: null,
|
||||
documentPath: null,
|
||||
version: null,
|
||||
ipAddress: null,
|
||||
createdBy: null,
|
||||
createdAt: null,
|
||||
updatedAt: null,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert oder erstellt eine Einwilligung
|
||||
*/
|
||||
export async function updateConsent(
|
||||
customerId: number,
|
||||
consentType: ConsentType,
|
||||
data: UpdateConsentData
|
||||
) {
|
||||
// Prüfen ob Kunde existiert
|
||||
const customer = await prisma.customer.findUnique({
|
||||
where: { id: customerId },
|
||||
});
|
||||
|
||||
if (!customer) {
|
||||
throw new Error('Kunde nicht gefunden');
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const updateData = {
|
||||
status: data.status,
|
||||
source: data.source,
|
||||
documentPath: data.documentPath,
|
||||
version: data.version,
|
||||
ipAddress: data.ipAddress,
|
||||
grantedAt: data.status === 'GRANTED' ? now : undefined,
|
||||
withdrawnAt: data.status === 'WITHDRAWN' ? now : undefined,
|
||||
};
|
||||
|
||||
const result = await prisma.customerConsent.upsert({
|
||||
where: {
|
||||
customerId_consentType: { customerId, consentType },
|
||||
},
|
||||
update: updateData,
|
||||
create: {
|
||||
customerId,
|
||||
consentType,
|
||||
...updateData,
|
||||
createdBy: data.createdBy,
|
||||
},
|
||||
});
|
||||
|
||||
// Bei Widerruf: Datenschutz-PDF löschen wenn keine Einwilligung mehr besteht
|
||||
if (data.status === 'WITHDRAWN') {
|
||||
await deletePrivacyPdfOnWithdraw(customerId);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Holt die Historie einer Einwilligung (aus Audit-Logs)
|
||||
*/
|
||||
export async function getConsentHistory(customerId: number, consentType: ConsentType) {
|
||||
// Aus Audit-Logs die Änderungen dieser Einwilligung abrufen
|
||||
const logs = await prisma.auditLog.findMany({
|
||||
where: {
|
||||
resourceType: 'CustomerConsent',
|
||||
dataSubjectId: customerId,
|
||||
changesAfter: { contains: consentType },
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 50,
|
||||
});
|
||||
|
||||
return logs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob eine bestimmte Einwilligung erteilt wurde
|
||||
*/
|
||||
export async function hasConsent(customerId: number, consentType: ConsentType): Promise<boolean> {
|
||||
const consent = await prisma.customerConsent.findUnique({
|
||||
where: {
|
||||
customerId_consentType: { customerId, consentType },
|
||||
},
|
||||
});
|
||||
|
||||
return consent?.status === 'GRANTED';
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob ein Kunde die DSGVO-Einwilligung erfüllt hat.
|
||||
* Erfüllt = entweder privacyPolicyPath vorhanden ODER alle Online-Consents GRANTED.
|
||||
*/
|
||||
export async function hasFullConsent(customerId: number): Promise<{
|
||||
hasConsent: boolean;
|
||||
hasPaperConsent: boolean;
|
||||
hasOnlineConsent: boolean;
|
||||
consentDetails: { type: string; status: string }[];
|
||||
consentHash: string | null;
|
||||
}> {
|
||||
// Prüfe ob Papier-Datenschutzerklärung vorhanden
|
||||
const customer = await prisma.customer.findUnique({
|
||||
where: { id: customerId },
|
||||
select: { privacyPolicyPath: true, consentHash: true },
|
||||
});
|
||||
|
||||
const hasPaperConsent = !!customer?.privacyPolicyPath;
|
||||
|
||||
// Online-Consents prüfen
|
||||
const allTypes = Object.values(ConsentType);
|
||||
const consents = await prisma.customerConsent.findMany({
|
||||
where: { customerId },
|
||||
});
|
||||
|
||||
const consentMap = new Map(consents.map((c) => [c.consentType, c.status]));
|
||||
const consentDetails = allTypes.map((type) => ({
|
||||
type,
|
||||
status: (consentMap.get(type) || 'PENDING') as string,
|
||||
}));
|
||||
|
||||
const hasOnlineConsent = allTypes.every(
|
||||
(type) => consentMap.get(type) === 'GRANTED'
|
||||
);
|
||||
|
||||
return {
|
||||
hasConsent: hasPaperConsent || hasOnlineConsent,
|
||||
hasPaperConsent,
|
||||
hasOnlineConsent,
|
||||
consentDetails,
|
||||
consentHash: customer?.consentHash || null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Widerruft alle Einwilligungen eines Kunden
|
||||
*/
|
||||
export async function withdrawAllConsents(customerId: number, withdrawnBy: string) {
|
||||
const result = await prisma.customerConsent.updateMany({
|
||||
where: {
|
||||
customerId,
|
||||
status: 'GRANTED',
|
||||
},
|
||||
data: {
|
||||
status: 'WITHDRAWN',
|
||||
withdrawnAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
// Datenschutz-PDF löschen
|
||||
await deletePrivacyPdfOnWithdraw(customerId);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Löscht die Datenschutz-PDF bei Widerruf.
|
||||
* Sobald auch nur eine Einwilligung widerrufen wird, ist die Gesamteinwilligung ungültig.
|
||||
*/
|
||||
async function deletePrivacyPdfOnWithdraw(customerId: number) {
|
||||
const customer = await prisma.customer.findUnique({
|
||||
where: { id: customerId },
|
||||
select: { privacyPolicyPath: true },
|
||||
});
|
||||
|
||||
if (customer?.privacyPolicyPath) {
|
||||
try {
|
||||
const filePath = path.join(process.cwd(), customer.privacyPolicyPath);
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Löschen der Datenschutz-PDF:', err);
|
||||
}
|
||||
|
||||
await prisma.customer.update({
|
||||
where: { id: customerId },
|
||||
data: { privacyPolicyPath: null },
|
||||
});
|
||||
|
||||
console.log(`Datenschutz-PDF für Kunde ${customerId} gelöscht (Einwilligung widerrufen)`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Consent-Übersicht für DSGVO-Dashboard
|
||||
*/
|
||||
export async function getConsentOverview() {
|
||||
const allConsents = await prisma.customerConsent.groupBy({
|
||||
by: ['consentType', 'status'],
|
||||
_count: { id: true },
|
||||
});
|
||||
|
||||
// Gruppieren nach Typ
|
||||
const overview: Record<string, { granted: number; withdrawn: number; pending: number }> = {};
|
||||
|
||||
for (const type of Object.values(ConsentType)) {
|
||||
overview[type] = { granted: 0, withdrawn: 0, pending: 0 };
|
||||
}
|
||||
|
||||
for (const row of allConsents) {
|
||||
const type = row.consentType;
|
||||
const status = row.status.toLowerCase() as 'granted' | 'withdrawn' | 'pending';
|
||||
overview[type][status] = row._count.id;
|
||||
}
|
||||
|
||||
return overview;
|
||||
}
|
||||
|
||||
/**
|
||||
* Consent-Typ Labels für UI
|
||||
*/
|
||||
export const CONSENT_TYPE_LABELS: Record<ConsentType, { label: string; description: string }> = {
|
||||
DATA_PROCESSING: {
|
||||
label: 'Datenverarbeitung',
|
||||
description: 'Grundlegende Verarbeitung personenbezogener Daten zur Vertragserfüllung',
|
||||
},
|
||||
MARKETING_EMAIL: {
|
||||
label: 'E-Mail-Marketing',
|
||||
description: 'Zusendung von Werbung und Angeboten per E-Mail',
|
||||
},
|
||||
MARKETING_PHONE: {
|
||||
label: 'Telefonmarketing',
|
||||
description: 'Kontaktaufnahme zu Werbezwecken per Telefon',
|
||||
},
|
||||
DATA_SHARING_PARTNER: {
|
||||
label: 'Datenweitergabe',
|
||||
description: 'Weitergabe von Daten an Partnerunternehmen',
|
||||
},
|
||||
};
|
||||
@@ -53,11 +53,54 @@ export interface CockpitSummary {
|
||||
openTasks: number;
|
||||
pendingContracts: number;
|
||||
reviewDue: number; // Erneute Prüfung fällig (Snooze abgelaufen)
|
||||
missingConsents: number; // Fehlende oder widerrufene Einwilligungen
|
||||
};
|
||||
}
|
||||
|
||||
export interface DocumentAlert {
|
||||
id: number;
|
||||
type: string; // ID_CARD, PASSPORT, DRIVERS_LICENSE, OTHER
|
||||
documentNumber: string;
|
||||
expiryDate: string;
|
||||
daysUntilExpiry: number;
|
||||
urgency: UrgencyLevel;
|
||||
customer: {
|
||||
id: number;
|
||||
customerNumber: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ReportedMeterReading {
|
||||
id: number;
|
||||
readingDate: string;
|
||||
value: number;
|
||||
unit: string;
|
||||
notes?: string;
|
||||
reportedBy?: string;
|
||||
createdAt: string;
|
||||
meter: {
|
||||
id: number;
|
||||
meterNumber: string;
|
||||
type: string;
|
||||
};
|
||||
customer: {
|
||||
id: number;
|
||||
customerNumber: string;
|
||||
name: string;
|
||||
};
|
||||
// Anbieter-Info für Quick-Login
|
||||
providerPortal?: {
|
||||
providerName: string;
|
||||
portalUrl: string;
|
||||
portalUsername?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CockpitResult {
|
||||
contracts: CockpitContract[];
|
||||
documentAlerts: DocumentAlert[];
|
||||
reportedReadings: ReportedMeterReading[];
|
||||
summary: CockpitSummary;
|
||||
thresholds: {
|
||||
criticalDays: number;
|
||||
@@ -143,6 +186,8 @@ export async function getCockpitData(): Promise<CockpitResult> {
|
||||
const criticalDays = parseInt(settings.deadlineCriticalDays) || 14;
|
||||
const warningDays = parseInt(settings.deadlineWarningDays) || 42;
|
||||
const okDays = parseInt(settings.deadlineOkDays) || 90;
|
||||
const docExpiryCriticalDays = parseInt(settings.documentExpiryCriticalDays) || 30;
|
||||
const docExpiryWarningDays = parseInt(settings.documentExpiryWarningDays) || 90;
|
||||
|
||||
// Lade alle relevanten Verträge (inkl. CANCELLED/DEACTIVATED für Schlussrechnung-Check)
|
||||
const contracts = await prisma.contract.findMany({
|
||||
@@ -231,9 +276,41 @@ export async function getCockpitData(): Promise<CockpitResult> {
|
||||
openTasks: 0,
|
||||
pendingContracts: 0,
|
||||
reviewDue: 0,
|
||||
missingConsents: 0,
|
||||
},
|
||||
};
|
||||
|
||||
// Consent-Daten batch-laden für alle Kunden
|
||||
const allConsents = await prisma.customerConsent.findMany({
|
||||
where: { status: 'GRANTED' },
|
||||
select: { customerId: true, consentType: true },
|
||||
});
|
||||
|
||||
// Map: customerId → Set<consentType>
|
||||
const grantedConsentsMap = new Map<number, Set<string>>();
|
||||
for (const c of allConsents) {
|
||||
if (!grantedConsentsMap.has(c.customerId)) {
|
||||
grantedConsentsMap.set(c.customerId, new Set());
|
||||
}
|
||||
grantedConsentsMap.get(c.customerId)!.add(c.consentType);
|
||||
}
|
||||
|
||||
// Widerrufene Consents laden
|
||||
const withdrawnConsents = await prisma.customerConsent.findMany({
|
||||
where: { status: 'WITHDRAWN' },
|
||||
select: { customerId: true, consentType: true },
|
||||
});
|
||||
const withdrawnConsentsMap = new Map<number, Set<string>>();
|
||||
for (const c of withdrawnConsents) {
|
||||
if (!withdrawnConsentsMap.has(c.customerId)) {
|
||||
withdrawnConsentsMap.set(c.customerId, new Set());
|
||||
}
|
||||
withdrawnConsentsMap.get(c.customerId)!.add(c.consentType);
|
||||
}
|
||||
|
||||
// Track welche Kunden bereits eine Consent-Warnung bekommen haben (nur einmal pro Kunde)
|
||||
const customerConsentWarned = new Set<number>();
|
||||
|
||||
for (const contract of contracts) {
|
||||
const issues: CockpitIssue[] = [];
|
||||
|
||||
@@ -407,17 +484,43 @@ export async function getCockpitData(): Promise<CockpitResult> {
|
||||
summary.byCategory.missingData++;
|
||||
}
|
||||
|
||||
// 7b. KEIN AUSWEIS (für DSL, FIBER, CABLE, MOBILE ist dies ein kritisches Problem)
|
||||
if (!contract.identityDocumentId) {
|
||||
// 7b. KEIN AUSWEIS (nur für Telekommunikationsprodukte relevant)
|
||||
const requiresIdentityDocument = ['DSL', 'FIBER', 'CABLE', 'MOBILE'].includes(contract.type);
|
||||
if (requiresIdentityDocument && !contract.identityDocumentId) {
|
||||
issues.push({
|
||||
type: 'missing_identity_document',
|
||||
label: 'Ausweis fehlt',
|
||||
urgency: requiresBankAndId ? 'critical' : 'warning',
|
||||
urgency: 'critical',
|
||||
details: 'Kein Ausweisdokument verknüpft',
|
||||
});
|
||||
summary.byCategory.missingData++;
|
||||
}
|
||||
|
||||
// 7c. AUSWEIS LÄUFT AB (nur aktive Ausweise prüfen)
|
||||
if (contract.identityDocument && contract.identityDocument.isActive && contract.identityDocument.expiryDate) {
|
||||
const expiryDate = new Date(contract.identityDocument.expiryDate);
|
||||
const today = new Date();
|
||||
const daysUntilExpiry = Math.ceil((expiryDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (daysUntilExpiry < 0) {
|
||||
issues.push({
|
||||
type: 'identity_document_expired',
|
||||
label: 'Ausweis abgelaufen',
|
||||
urgency: 'critical',
|
||||
details: `Ausweis seit ${Math.abs(daysUntilExpiry)} Tagen abgelaufen (${expiryDate.toLocaleDateString('de-DE')})`,
|
||||
});
|
||||
summary.byCategory.missingData++;
|
||||
} else if (daysUntilExpiry <= docExpiryWarningDays) {
|
||||
issues.push({
|
||||
type: 'identity_document_expiring',
|
||||
label: 'Ausweis läuft ab',
|
||||
urgency: daysUntilExpiry <= docExpiryCriticalDays ? 'critical' : 'warning',
|
||||
details: `Ausweis läuft in ${daysUntilExpiry} Tagen ab (${expiryDate.toLocaleDateString('de-DE')})`,
|
||||
});
|
||||
summary.byCategory.cancellationDeadlines++;
|
||||
}
|
||||
}
|
||||
|
||||
// 8. ENERGIE-SPEZIFISCH: KEIN ZÄHLER
|
||||
if (['ELECTRICITY', 'GAS'].includes(contract.type) && contract.energyDetails) {
|
||||
if (!contract.energyDetails.meterId) {
|
||||
@@ -546,6 +649,36 @@ export async function getCockpitData(): Promise<CockpitResult> {
|
||||
}
|
||||
}
|
||||
|
||||
// #14 - Consent-Prüfung (nur für aktive Verträge, einmal pro Kunde)
|
||||
if (['ACTIVE', 'PENDING', 'DRAFT'].includes(contract.status) && !customerConsentWarned.has(contract.customer.id)) {
|
||||
const granted = grantedConsentsMap.get(contract.customer.id);
|
||||
const withdrawn = withdrawnConsentsMap.get(contract.customer.id);
|
||||
const requiredTypes = ['DATA_PROCESSING', 'MARKETING_EMAIL', 'MARKETING_PHONE', 'DATA_SHARING_PARTNER'];
|
||||
|
||||
if (withdrawn && withdrawn.size > 0) {
|
||||
// Mindestens eine Einwilligung widerrufen
|
||||
issues.push({
|
||||
type: 'consent_withdrawn',
|
||||
label: 'Einwilligung widerrufen',
|
||||
urgency: 'critical',
|
||||
details: `${withdrawn.size} Einwilligung(en) widerrufen`,
|
||||
});
|
||||
summary.byCategory.missingConsents++;
|
||||
customerConsentWarned.add(contract.customer.id);
|
||||
} else if (!granted || granted.size < requiredTypes.length) {
|
||||
// Nicht alle 4 Einwilligungen erteilt
|
||||
const missing = requiredTypes.length - (granted?.size || 0);
|
||||
issues.push({
|
||||
type: 'missing_consents',
|
||||
label: 'Fehlende Einwilligungen',
|
||||
urgency: 'critical',
|
||||
details: `${missing} von ${requiredTypes.length} Einwilligungen fehlen`,
|
||||
});
|
||||
summary.byCategory.missingConsents++;
|
||||
customerConsentWarned.add(contract.customer.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Nur Verträge mit Issues hinzufügen
|
||||
if (issues.length > 0) {
|
||||
const highestUrgency = getHighestUrgency(issues);
|
||||
@@ -596,8 +729,16 @@ export async function getCockpitData(): Promise<CockpitResult> {
|
||||
return urgencyOrder[a.highestUrgency] - urgencyOrder[b.highestUrgency];
|
||||
});
|
||||
|
||||
// Vertragsunabhängige Ausweis-Warnungen
|
||||
const documentAlerts = await getDocumentExpiryAlerts(docExpiryCriticalDays, docExpiryWarningDays);
|
||||
|
||||
// Gemeldete Zählerstände (REPORTED Status)
|
||||
const reportedReadings = await getReportedMeterReadings();
|
||||
|
||||
return {
|
||||
contracts: cockpitContracts,
|
||||
documentAlerts,
|
||||
reportedReadings,
|
||||
summary,
|
||||
thresholds: {
|
||||
criticalDays,
|
||||
@@ -606,3 +747,111 @@ export async function getCockpitData(): Promise<CockpitResult> {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Alle aktiven Ausweise die ablaufen oder abgelaufen sind (vertragsunabhängig)
|
||||
*/
|
||||
async function getDocumentExpiryAlerts(criticalDays: number, warningDays: number): Promise<DocumentAlert[]> {
|
||||
const now = new Date();
|
||||
const inWarningDays = new Date(now.getTime() + warningDays * 24 * 60 * 60 * 1000);
|
||||
|
||||
const documents = await prisma.identityDocument.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
expiryDate: { lte: inWarningDays },
|
||||
},
|
||||
include: {
|
||||
customer: {
|
||||
select: { id: true, customerNumber: true, firstName: true, lastName: true },
|
||||
},
|
||||
},
|
||||
orderBy: { expiryDate: 'asc' },
|
||||
});
|
||||
|
||||
return documents.map((doc) => {
|
||||
const expiryDate = new Date(doc.expiryDate!);
|
||||
const daysUntilExpiry = Math.ceil((expiryDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
|
||||
|
||||
let urgency: UrgencyLevel = 'warning';
|
||||
if (daysUntilExpiry < 0) urgency = 'critical';
|
||||
else if (daysUntilExpiry <= criticalDays) urgency = 'critical';
|
||||
|
||||
return {
|
||||
id: doc.id,
|
||||
type: doc.type,
|
||||
documentNumber: doc.documentNumber,
|
||||
expiryDate: expiryDate.toISOString(),
|
||||
daysUntilExpiry,
|
||||
urgency,
|
||||
customer: {
|
||||
id: doc.customer.id,
|
||||
customerNumber: doc.customer.customerNumber,
|
||||
name: `${doc.customer.firstName} ${doc.customer.lastName}`,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Vom Kunden gemeldete Zählerstände die noch nicht übertragen wurden
|
||||
*/
|
||||
async function getReportedMeterReadings(): Promise<ReportedMeterReading[]> {
|
||||
const readings = await prisma.meterReading.findMany({
|
||||
where: { status: 'REPORTED' },
|
||||
include: {
|
||||
meter: {
|
||||
include: {
|
||||
customer: {
|
||||
select: { id: true, customerNumber: true, firstName: true, lastName: true },
|
||||
},
|
||||
// Energie-Verträge für diesen Zähler (um Provider-Portal-Daten zu bekommen)
|
||||
energyDetails: {
|
||||
include: {
|
||||
contract: {
|
||||
select: {
|
||||
id: true,
|
||||
portalUsername: true,
|
||||
provider: {
|
||||
select: { id: true, name: true, portalUrl: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: 'asc' },
|
||||
});
|
||||
|
||||
return readings.map((r) => {
|
||||
const contract = r.meter.energyDetails?.[0]?.contract;
|
||||
const provider = contract?.provider;
|
||||
|
||||
return {
|
||||
id: r.id,
|
||||
readingDate: r.readingDate.toISOString(),
|
||||
value: r.value,
|
||||
unit: r.unit,
|
||||
notes: r.notes ?? undefined,
|
||||
reportedBy: r.reportedBy ?? undefined,
|
||||
createdAt: r.createdAt.toISOString(),
|
||||
meter: {
|
||||
id: r.meter.id,
|
||||
meterNumber: r.meter.meterNumber,
|
||||
type: r.meter.type,
|
||||
},
|
||||
customer: {
|
||||
id: r.meter.customer.id,
|
||||
customerNumber: r.meter.customer.customerNumber,
|
||||
name: `${r.meter.customer.firstName} ${r.meter.customer.lastName}`,
|
||||
},
|
||||
providerPortal: provider?.portalUrl ? {
|
||||
providerName: provider.name,
|
||||
portalUrl: provider.portalUrl,
|
||||
portalUsername: contract?.portalUsername ?? undefined,
|
||||
} : undefined,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import prisma from '../lib/prisma.js';
|
||||
|
||||
export interface CreateEmailLogData {
|
||||
fromAddress: string;
|
||||
toAddress: string;
|
||||
subject: string;
|
||||
context: string;
|
||||
customerId?: number;
|
||||
triggeredBy?: string;
|
||||
smtpServer: string;
|
||||
smtpPort: number;
|
||||
smtpEncryption: string;
|
||||
smtpUser: string;
|
||||
success: boolean;
|
||||
messageId?: string;
|
||||
errorMessage?: string;
|
||||
smtpResponse?: string;
|
||||
}
|
||||
|
||||
export async function createEmailLog(data: CreateEmailLogData) {
|
||||
return prisma.emailLog.create({ data });
|
||||
}
|
||||
|
||||
export async function getEmailLogs(options?: {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
success?: boolean;
|
||||
search?: string;
|
||||
context?: string;
|
||||
}) {
|
||||
const page = options?.page || 1;
|
||||
const limit = options?.limit || 50;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const where: Record<string, unknown> = {};
|
||||
|
||||
if (options?.success !== undefined) {
|
||||
where.success = options.success;
|
||||
}
|
||||
|
||||
if (options?.context) {
|
||||
where.context = options.context;
|
||||
}
|
||||
|
||||
if (options?.search) {
|
||||
where.OR = [
|
||||
{ fromAddress: { contains: options.search } },
|
||||
{ toAddress: { contains: options.search } },
|
||||
{ subject: { contains: options.search } },
|
||||
{ errorMessage: { contains: options.search } },
|
||||
];
|
||||
}
|
||||
|
||||
const [logs, total] = await Promise.all([
|
||||
prisma.emailLog.findMany({
|
||||
where,
|
||||
orderBy: { sentAt: 'desc' },
|
||||
skip,
|
||||
take: limit,
|
||||
}),
|
||||
prisma.emailLog.count({ where }),
|
||||
]);
|
||||
|
||||
return {
|
||||
data: logs,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function getEmailLogById(id: number) {
|
||||
return prisma.emailLog.findUnique({ where: { id } });
|
||||
}
|
||||
|
||||
export async function getEmailLogStats() {
|
||||
const [total, success, failed, last24h] = await Promise.all([
|
||||
prisma.emailLog.count(),
|
||||
prisma.emailLog.count({ where: { success: true } }),
|
||||
prisma.emailLog.count({ where: { success: false } }),
|
||||
prisma.emailLog.count({
|
||||
where: { sentAt: { gte: new Date(Date.now() - 24 * 60 * 60 * 1000) } },
|
||||
}),
|
||||
]);
|
||||
|
||||
return { total, success, failed, last24h };
|
||||
}
|
||||
@@ -73,6 +73,9 @@ export interface CreateProviderConfigData {
|
||||
imapEncryption?: MailEncryption;
|
||||
smtpEncryption?: MailEncryption;
|
||||
allowSelfSignedCerts?: boolean;
|
||||
// System-E-Mail
|
||||
systemEmailAddress?: string;
|
||||
systemEmailPassword?: string;
|
||||
isActive?: boolean;
|
||||
isDefault?: boolean;
|
||||
}
|
||||
@@ -86,9 +89,10 @@ export async function createProviderConfig(data: CreateProviderConfigData) {
|
||||
});
|
||||
}
|
||||
|
||||
// Passwort verschlüsseln falls vorhanden
|
||||
// Passwörter verschlüsseln falls vorhanden
|
||||
const { encrypt } = await import('../../utils/encryption.js');
|
||||
const passwordEncrypted = data.password ? encrypt(data.password) : null;
|
||||
const systemEmailPasswordEncrypted = data.systemEmailPassword ? encrypt(data.systemEmailPassword) : null;
|
||||
|
||||
return prisma.emailProviderConfig.create({
|
||||
data: {
|
||||
@@ -103,6 +107,8 @@ export async function createProviderConfig(data: CreateProviderConfigData) {
|
||||
imapEncryption: data.imapEncryption ?? 'SSL',
|
||||
smtpEncryption: data.smtpEncryption ?? 'SSL',
|
||||
allowSelfSignedCerts: data.allowSelfSignedCerts ?? false,
|
||||
systemEmailAddress: data.systemEmailAddress || null,
|
||||
systemEmailPasswordEncrypted,
|
||||
isActive: data.isActive ?? true,
|
||||
isDefault: data.isDefault ?? false,
|
||||
},
|
||||
@@ -134,20 +140,30 @@ export async function updateProviderConfig(
|
||||
if (data.imapEncryption !== undefined) updateData.imapEncryption = data.imapEncryption;
|
||||
if (data.smtpEncryption !== undefined) updateData.smtpEncryption = data.smtpEncryption;
|
||||
if (data.allowSelfSignedCerts !== undefined) updateData.allowSelfSignedCerts = data.allowSelfSignedCerts;
|
||||
if (data.systemEmailAddress !== undefined) updateData.systemEmailAddress = data.systemEmailAddress || null;
|
||||
if (data.isActive !== undefined) updateData.isActive = data.isActive;
|
||||
if (data.isDefault !== undefined) updateData.isDefault = data.isDefault;
|
||||
|
||||
const { encrypt } = await import('../../utils/encryption.js');
|
||||
|
||||
// Passwort-Logik:
|
||||
// - Wenn neues Passwort übergeben → verschlüsseln und speichern
|
||||
// - Wenn Benutzername gelöscht wird → Passwort auch löschen (gehören zusammen)
|
||||
if (data.password) {
|
||||
const { encrypt } = await import('../../utils/encryption.js');
|
||||
updateData.passwordEncrypted = encrypt(data.password);
|
||||
} else if (data.username !== undefined && !data.username) {
|
||||
// Benutzername wird gelöscht → Passwort auch löschen
|
||||
updateData.passwordEncrypted = null;
|
||||
}
|
||||
|
||||
// System-E-Mail-Passwort
|
||||
if (data.systemEmailPassword) {
|
||||
updateData.systemEmailPasswordEncrypted = encrypt(data.systemEmailPassword);
|
||||
} else if (data.systemEmailAddress !== undefined && !data.systemEmailAddress) {
|
||||
// System-E-Mail wird gelöscht → Passwort auch löschen
|
||||
updateData.systemEmailPasswordEncrypted = null;
|
||||
}
|
||||
|
||||
return prisma.emailProviderConfig.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
@@ -564,3 +580,45 @@ export async function testProviderConnection(options?: {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== SYSTEM EMAIL ====================
|
||||
|
||||
export interface SystemEmailCredentials {
|
||||
emailAddress: string;
|
||||
password: string;
|
||||
smtpServer: string;
|
||||
smtpPort: number;
|
||||
smtpEncryption: MailEncryption;
|
||||
allowSelfSignedCerts: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* System-E-Mail-Credentials vom aktiven Provider holen.
|
||||
* Wird für automatisierten Versand (DSGVO, Benachrichtigungen etc.) verwendet.
|
||||
*/
|
||||
export async function getSystemEmailCredentials(): Promise<SystemEmailCredentials | null> {
|
||||
const config = await getActiveProviderConfig();
|
||||
if (!config?.systemEmailAddress || !config?.systemEmailPasswordEncrypted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let password: string;
|
||||
try {
|
||||
password = decrypt(config.systemEmailPasswordEncrypted);
|
||||
} catch {
|
||||
console.error('System-E-Mail-Passwort konnte nicht entschlüsselt werden');
|
||||
return null;
|
||||
}
|
||||
|
||||
const settings = await getImapSmtpSettings();
|
||||
if (!settings) return null;
|
||||
|
||||
return {
|
||||
emailAddress: config.systemEmailAddress,
|
||||
password,
|
||||
smtpServer: settings.smtpServer,
|
||||
smtpPort: settings.smtpPort,
|
||||
smtpEncryption: settings.smtpEncryption,
|
||||
allowSelfSignedCerts: settings.allowSelfSignedCerts,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,565 @@
|
||||
import { DeletionRequestStatus } from '@prisma/client';
|
||||
import prisma from '../lib/prisma.js';
|
||||
import { getAuditLogsByDataSubject } from './audit.service.js';
|
||||
import { getCustomerConsents, withdrawAllConsents } from './consent.service.js';
|
||||
import PDFDocument from 'pdfkit';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
export interface CreateDeletionRequestData {
|
||||
customerId: number;
|
||||
requestSource: string;
|
||||
requestedBy: string;
|
||||
}
|
||||
|
||||
export interface ProcessDeletionRequestData {
|
||||
processedBy: string;
|
||||
action: 'complete' | 'partial' | 'reject';
|
||||
retentionReason?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exportiert alle Daten eines Kunden (DSGVO Art. 15)
|
||||
*/
|
||||
export async function exportCustomerData(customerId: number) {
|
||||
const customer = await prisma.customer.findUnique({
|
||||
where: { id: customerId },
|
||||
include: {
|
||||
addresses: true,
|
||||
bankCards: {
|
||||
select: {
|
||||
id: true,
|
||||
accountHolder: true,
|
||||
iban: true,
|
||||
bic: true,
|
||||
bankName: true,
|
||||
isActive: true,
|
||||
createdAt: true,
|
||||
},
|
||||
},
|
||||
identityDocuments: {
|
||||
select: {
|
||||
id: true,
|
||||
type: true,
|
||||
documentNumber: true,
|
||||
issuingAuthority: true,
|
||||
issueDate: true,
|
||||
expiryDate: true,
|
||||
isActive: true,
|
||||
createdAt: true,
|
||||
},
|
||||
},
|
||||
meters: {
|
||||
include: {
|
||||
readings: true,
|
||||
},
|
||||
},
|
||||
contracts: {
|
||||
include: {
|
||||
address: true,
|
||||
billingAddress: true,
|
||||
provider: true,
|
||||
tariff: true,
|
||||
energyDetails: {
|
||||
include: { invoices: true },
|
||||
},
|
||||
internetDetails: {
|
||||
include: { phoneNumbers: true },
|
||||
},
|
||||
mobileDetails: {
|
||||
include: { simCards: true },
|
||||
},
|
||||
tvDetails: true,
|
||||
carInsuranceDetails: true,
|
||||
historyEntries: true,
|
||||
tasks: {
|
||||
include: { subtasks: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
stressfreiEmails: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
platform: true,
|
||||
isActive: true,
|
||||
createdAt: true,
|
||||
},
|
||||
},
|
||||
consents: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!customer) {
|
||||
throw new Error('Kunde nicht gefunden');
|
||||
}
|
||||
|
||||
// Audit-Logs für diesen Kunden
|
||||
const accessLogs = await getAuditLogsByDataSubject(customerId);
|
||||
|
||||
// Sensible Felder entfernen
|
||||
const exportData = {
|
||||
exportDate: new Date().toISOString(),
|
||||
dataSubject: {
|
||||
id: customer.id,
|
||||
customerNumber: customer.customerNumber,
|
||||
name: `${customer.firstName} ${customer.lastName}`,
|
||||
},
|
||||
personalData: {
|
||||
salutation: customer.salutation,
|
||||
firstName: customer.firstName,
|
||||
lastName: customer.lastName,
|
||||
companyName: customer.companyName,
|
||||
type: customer.type,
|
||||
birthDate: customer.birthDate,
|
||||
birthPlace: customer.birthPlace,
|
||||
email: customer.email,
|
||||
phone: customer.phone,
|
||||
mobile: customer.mobile,
|
||||
taxNumber: customer.taxNumber,
|
||||
portalEnabled: customer.portalEnabled,
|
||||
portalEmail: customer.portalEmail,
|
||||
portalLastLogin: customer.portalLastLogin,
|
||||
createdAt: customer.createdAt,
|
||||
updatedAt: customer.updatedAt,
|
||||
},
|
||||
addresses: customer.addresses,
|
||||
bankCards: customer.bankCards,
|
||||
identityDocuments: customer.identityDocuments,
|
||||
meters: customer.meters,
|
||||
contracts: customer.contracts.map((c) => ({
|
||||
...c,
|
||||
// Sensible Daten entfernen
|
||||
portalPasswordEncrypted: undefined,
|
||||
})),
|
||||
emails: customer.stressfreiEmails,
|
||||
consents: customer.consents,
|
||||
accessHistory: accessLogs.map((log) => ({
|
||||
timestamp: log.createdAt,
|
||||
action: log.action,
|
||||
user: log.userEmail,
|
||||
resource: log.resourceType,
|
||||
ipAddress: log.ipAddress,
|
||||
})),
|
||||
};
|
||||
|
||||
return exportData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt eine Löschanfrage
|
||||
*/
|
||||
export async function createDeletionRequest(data: CreateDeletionRequestData) {
|
||||
// Prüfen ob Kunde existiert
|
||||
const customer = await prisma.customer.findUnique({
|
||||
where: { id: data.customerId },
|
||||
});
|
||||
|
||||
if (!customer) {
|
||||
throw new Error('Kunde nicht gefunden');
|
||||
}
|
||||
|
||||
// Prüfen ob bereits eine offene Anfrage existiert
|
||||
const existingRequest = await prisma.dataDeletionRequest.findFirst({
|
||||
where: {
|
||||
customerId: data.customerId,
|
||||
status: { in: ['PENDING', 'IN_PROGRESS'] },
|
||||
},
|
||||
});
|
||||
|
||||
if (existingRequest) {
|
||||
throw new Error('Es existiert bereits eine offene Löschanfrage für diesen Kunden');
|
||||
}
|
||||
|
||||
return prisma.dataDeletionRequest.create({
|
||||
data: {
|
||||
customerId: data.customerId,
|
||||
requestSource: data.requestSource,
|
||||
requestedBy: data.requestedBy,
|
||||
status: 'PENDING',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Holt alle Löschanfragen mit Paginierung
|
||||
*/
|
||||
export async function getDeletionRequests(params: {
|
||||
status?: DeletionRequestStatus;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}) {
|
||||
const { status, page = 1, limit = 20 } = params;
|
||||
|
||||
const where = status ? { status } : {};
|
||||
|
||||
const [requests, total] = await Promise.all([
|
||||
prisma.dataDeletionRequest.findMany({
|
||||
where,
|
||||
orderBy: { requestedAt: 'desc' },
|
||||
skip: (page - 1) * limit,
|
||||
take: limit,
|
||||
}),
|
||||
prisma.dataDeletionRequest.count({ where }),
|
||||
]);
|
||||
|
||||
// Kundendaten hinzufügen
|
||||
const customerIds = requests.map((r) => r.customerId);
|
||||
const customers = await prisma.customer.findMany({
|
||||
where: { id: { in: customerIds } },
|
||||
select: {
|
||||
id: true,
|
||||
customerNumber: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
},
|
||||
});
|
||||
|
||||
const customerMap = new Map(customers.map((c) => [c.id, c]));
|
||||
|
||||
const requestsWithCustomer = requests.map((r) => ({
|
||||
...r,
|
||||
customer: customerMap.get(r.customerId) || null,
|
||||
}));
|
||||
|
||||
return {
|
||||
data: requestsWithCustomer,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Holt eine einzelne Löschanfrage
|
||||
*/
|
||||
export async function getDeletionRequest(id: number) {
|
||||
const request = await prisma.dataDeletionRequest.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!request) return null;
|
||||
|
||||
const customer = await prisma.customer.findUnique({
|
||||
where: { id: request.customerId },
|
||||
select: {
|
||||
id: true,
|
||||
customerNumber: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
return { ...request, customer };
|
||||
}
|
||||
|
||||
/**
|
||||
* Bearbeitet eine Löschanfrage
|
||||
*/
|
||||
export async function processDeletionRequest(
|
||||
requestId: number,
|
||||
data: ProcessDeletionRequestData
|
||||
) {
|
||||
const request = await prisma.dataDeletionRequest.findUnique({
|
||||
where: { id: requestId },
|
||||
});
|
||||
|
||||
if (!request) {
|
||||
throw new Error('Löschanfrage nicht gefunden');
|
||||
}
|
||||
|
||||
if (request.status !== 'PENDING' && request.status !== 'IN_PROGRESS') {
|
||||
throw new Error('Diese Anfrage wurde bereits bearbeitet');
|
||||
}
|
||||
|
||||
// Status auf IN_PROGRESS setzen
|
||||
await prisma.dataDeletionRequest.update({
|
||||
where: { id: requestId },
|
||||
data: { status: 'IN_PROGRESS' },
|
||||
});
|
||||
|
||||
const customerId = request.customerId;
|
||||
const deletedData: Record<string, number> = {};
|
||||
const retainedData: Record<string, { count: number; reason: string }> = {};
|
||||
|
||||
try {
|
||||
if (data.action === 'reject') {
|
||||
// Anfrage ablehnen
|
||||
return prisma.dataDeletionRequest.update({
|
||||
where: { id: requestId },
|
||||
data: {
|
||||
status: 'REJECTED',
|
||||
processedAt: new Date(),
|
||||
processedBy: data.processedBy,
|
||||
retentionReason: data.retentionReason,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Einwilligungen widerrufen
|
||||
await withdrawAllConsents(customerId, data.processedBy);
|
||||
deletedData['consents'] = 1;
|
||||
|
||||
// Verträge prüfen - aktive Verträge müssen behalten werden
|
||||
const contracts = await prisma.contract.findMany({
|
||||
where: { customerId },
|
||||
});
|
||||
|
||||
const activeContracts = contracts.filter(
|
||||
(c) => c.status === 'ACTIVE' || c.status === 'PENDING'
|
||||
);
|
||||
|
||||
if (activeContracts.length > 0) {
|
||||
retainedData['contracts'] = {
|
||||
count: activeContracts.length,
|
||||
reason: 'Aktive Verträge müssen für die Vertragserfüllung aufbewahrt werden',
|
||||
};
|
||||
}
|
||||
|
||||
// Löschbare Daten anonymisieren (statt hart löschen)
|
||||
if (data.action === 'complete' && activeContracts.length === 0) {
|
||||
// Kunde vollständig anonymisieren
|
||||
await anonymizeCustomer(customerId);
|
||||
deletedData['customer'] = 1;
|
||||
deletedData['addresses'] = 1;
|
||||
deletedData['bankCards'] = 1;
|
||||
deletedData['identityDocuments'] = 1;
|
||||
} else {
|
||||
// Teilweise Löschung - nur optionale Daten
|
||||
const deletedAddresses = await prisma.address.deleteMany({
|
||||
where: { customerId, isDefault: false },
|
||||
});
|
||||
deletedData['addresses'] = deletedAddresses.count;
|
||||
|
||||
// Inaktive Bankkarten löschen
|
||||
const deletedBankCards = await prisma.bankCard.deleteMany({
|
||||
where: { customerId, isActive: false },
|
||||
});
|
||||
deletedData['bankCards'] = deletedBankCards.count;
|
||||
|
||||
// Inaktive Dokumente löschen
|
||||
const deletedDocs = await prisma.identityDocument.deleteMany({
|
||||
where: { customerId, isActive: false },
|
||||
});
|
||||
deletedData['identityDocuments'] = deletedDocs.count;
|
||||
}
|
||||
|
||||
// Löschnachweis generieren
|
||||
const proofPath = await generateDeletionProof(requestId, customerId, deletedData, retainedData);
|
||||
|
||||
// Anfrage abschließen
|
||||
const status = Object.keys(retainedData).length > 0 ? 'PARTIALLY_COMPLETED' : 'COMPLETED';
|
||||
|
||||
return prisma.dataDeletionRequest.update({
|
||||
where: { id: requestId },
|
||||
data: {
|
||||
status,
|
||||
processedAt: new Date(),
|
||||
processedBy: data.processedBy,
|
||||
deletedData: JSON.stringify(deletedData),
|
||||
retainedData: JSON.stringify(retainedData),
|
||||
retentionReason: data.retentionReason,
|
||||
proofDocument: proofPath,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
// Bei Fehler Status zurücksetzen
|
||||
await prisma.dataDeletionRequest.update({
|
||||
where: { id: requestId },
|
||||
data: { status: 'PENDING' },
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Anonymisiert Kundendaten (DSGVO-konform)
|
||||
*/
|
||||
async function anonymizeCustomer(customerId: number) {
|
||||
const anonymized = `[GELÖSCHT-${Date.now()}]`;
|
||||
|
||||
await prisma.customer.update({
|
||||
where: { id: customerId },
|
||||
data: {
|
||||
firstName: anonymized,
|
||||
lastName: anonymized,
|
||||
salutation: null,
|
||||
companyName: null,
|
||||
birthDate: null,
|
||||
birthPlace: null,
|
||||
email: null,
|
||||
phone: null,
|
||||
mobile: null,
|
||||
taxNumber: null,
|
||||
notes: null,
|
||||
portalEnabled: false,
|
||||
portalEmail: null,
|
||||
portalPasswordHash: null,
|
||||
portalPasswordEncrypted: null,
|
||||
},
|
||||
});
|
||||
|
||||
// Adressen anonymisieren
|
||||
await prisma.address.updateMany({
|
||||
where: { customerId },
|
||||
data: {
|
||||
street: anonymized,
|
||||
houseNumber: '',
|
||||
postalCode: '00000',
|
||||
city: anonymized,
|
||||
},
|
||||
});
|
||||
|
||||
// Bankkarten anonymisieren
|
||||
await prisma.bankCard.updateMany({
|
||||
where: { customerId },
|
||||
data: {
|
||||
accountHolder: anonymized,
|
||||
iban: 'XX00000000000000000000',
|
||||
bic: null,
|
||||
bankName: null,
|
||||
isActive: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Ausweisdokumente anonymisieren
|
||||
await prisma.identityDocument.updateMany({
|
||||
where: { customerId },
|
||||
data: {
|
||||
documentNumber: anonymized,
|
||||
issuingAuthority: null,
|
||||
isActive: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert ein Löschnachweis-PDF
|
||||
*/
|
||||
async function generateDeletionProof(
|
||||
requestId: number,
|
||||
customerId: number,
|
||||
deletedData: Record<string, number>,
|
||||
retainedData: Record<string, { count: number; reason: string }>
|
||||
): Promise<string> {
|
||||
const uploadsDir = path.join(process.cwd(), 'uploads', 'gdpr');
|
||||
|
||||
// Verzeichnis erstellen falls nicht vorhanden
|
||||
if (!fs.existsSync(uploadsDir)) {
|
||||
fs.mkdirSync(uploadsDir, { recursive: true });
|
||||
}
|
||||
|
||||
const filename = `loeschnachweis_${requestId}_${Date.now()}.pdf`;
|
||||
const filepath = path.join(uploadsDir, filename);
|
||||
|
||||
const doc = new PDFDocument({ size: 'A4', margin: 50 });
|
||||
const writeStream = fs.createWriteStream(filepath);
|
||||
doc.pipe(writeStream);
|
||||
|
||||
// Titel
|
||||
doc.fontSize(18).text('Datenlöschungsnachweis', { align: 'center' });
|
||||
doc.moveDown();
|
||||
|
||||
// Metadaten
|
||||
doc.fontSize(12);
|
||||
doc.text(`Anfrage-ID: ${requestId}`);
|
||||
doc.text(`Kunden-ID: ${customerId}`);
|
||||
doc.text(`Datum: ${new Date().toLocaleDateString('de-DE')}`);
|
||||
doc.text(`Uhrzeit: ${new Date().toLocaleTimeString('de-DE')}`);
|
||||
doc.moveDown();
|
||||
|
||||
// Gelöschte Daten
|
||||
doc.fontSize(14).text('Gelöschte Daten:', { underline: true });
|
||||
doc.fontSize(12);
|
||||
for (const [category, count] of Object.entries(deletedData)) {
|
||||
doc.text(`• ${category}: ${count} Einträge`);
|
||||
}
|
||||
doc.moveDown();
|
||||
|
||||
// Aufbewahrte Daten
|
||||
if (Object.keys(retainedData).length > 0) {
|
||||
doc.fontSize(14).text('Aufbewahrte Daten:', { underline: true });
|
||||
doc.fontSize(12);
|
||||
for (const [category, info] of Object.entries(retainedData)) {
|
||||
doc.text(`• ${category}: ${info.count} Einträge`);
|
||||
doc.fontSize(10).text(` Grund: ${info.reason}`, { indent: 20 });
|
||||
doc.fontSize(12);
|
||||
}
|
||||
doc.moveDown();
|
||||
}
|
||||
|
||||
// Rechtlicher Hinweis
|
||||
doc.moveDown();
|
||||
doc.fontSize(10).text(
|
||||
'Dieses Dokument bestätigt die Durchführung der Datenlöschung gemäß Art. 17 DSGVO. ' +
|
||||
'Daten, die aus gesetzlichen Gründen aufbewahrt werden müssen, wurden nicht gelöscht.',
|
||||
{ align: 'justify' }
|
||||
);
|
||||
|
||||
doc.end();
|
||||
|
||||
// Warten bis Datei geschrieben wurde
|
||||
await new Promise<void>((resolve) => writeStream.on('finish', resolve));
|
||||
|
||||
return `gdpr/${filename}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dashboard-Statistiken für DSGVO
|
||||
*/
|
||||
export async function getGDPRDashboardStats() {
|
||||
const [
|
||||
pendingDeletions,
|
||||
completedDeletions,
|
||||
recentExports,
|
||||
consentStats,
|
||||
] = await Promise.all([
|
||||
// Offene Löschanfragen
|
||||
prisma.dataDeletionRequest.count({
|
||||
where: { status: { in: ['PENDING', 'IN_PROGRESS'] } },
|
||||
}),
|
||||
// Abgeschlossene Löschungen (letzte 30 Tage)
|
||||
prisma.dataDeletionRequest.count({
|
||||
where: {
|
||||
status: { in: ['COMPLETED', 'PARTIALLY_COMPLETED'] },
|
||||
processedAt: { gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) },
|
||||
},
|
||||
}),
|
||||
// Letzte Datenexporte (aus Audit-Log)
|
||||
prisma.auditLog.count({
|
||||
where: {
|
||||
action: 'EXPORT',
|
||||
resourceType: 'GDPR',
|
||||
createdAt: { gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) },
|
||||
},
|
||||
}),
|
||||
// Consent-Statistik
|
||||
prisma.customerConsent.groupBy({
|
||||
by: ['status'],
|
||||
_count: { id: true },
|
||||
}),
|
||||
]);
|
||||
|
||||
const consentByStatus = consentStats.reduce(
|
||||
(acc, s) => {
|
||||
acc[s.status.toLowerCase()] = s._count.id;
|
||||
return acc;
|
||||
},
|
||||
{ granted: 0, withdrawn: 0, pending: 0 } as Record<string, number>
|
||||
);
|
||||
|
||||
return {
|
||||
deletionRequests: {
|
||||
pending: pendingDeletions,
|
||||
completedLast30Days: completedDeletions,
|
||||
},
|
||||
dataExports: {
|
||||
last30Days: recentExports,
|
||||
},
|
||||
consents: consentByStatus,
|
||||
};
|
||||
}
|
||||
@@ -42,11 +42,19 @@ export interface SendEmailResult {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// Optionaler Logging-Kontext
|
||||
export interface EmailLogContext {
|
||||
context: string; // z.B. "consent-link", "authorization-request", "customer-email"
|
||||
customerId?: number;
|
||||
triggeredBy?: string; // User-Email
|
||||
}
|
||||
|
||||
// E-Mail senden
|
||||
export async function sendEmail(
|
||||
credentials: SmtpCredentials,
|
||||
fromAddress: string,
|
||||
params: SendEmailParams
|
||||
params: SendEmailParams,
|
||||
logContext?: EmailLogContext
|
||||
): Promise<SendEmailResult> {
|
||||
// Verschlüsselungs-Einstellungen basierend auf Modus
|
||||
const encryption = credentials.encryption ?? 'SSL';
|
||||
@@ -155,6 +163,27 @@ export async function sendEmail(
|
||||
// Nicht kritisch - E-Mail wurde trotzdem gesendet
|
||||
}
|
||||
|
||||
// E-Mail-Log erstellen (async, nicht blockierend)
|
||||
if (logContext) {
|
||||
import('./emailLog.service.js').then(({ createEmailLog }) => {
|
||||
createEmailLog({
|
||||
fromAddress,
|
||||
toAddress: Array.isArray(params.to) ? params.to.join(', ') : params.to,
|
||||
subject: params.subject,
|
||||
context: logContext.context,
|
||||
customerId: logContext.customerId,
|
||||
triggeredBy: logContext.triggeredBy,
|
||||
smtpServer: credentials.host,
|
||||
smtpPort: credentials.port,
|
||||
smtpEncryption: credentials.encryption ?? 'SSL',
|
||||
smtpUser: credentials.user,
|
||||
success: true,
|
||||
messageId: result.messageId,
|
||||
smtpResponse: result.response,
|
||||
}).catch((err) => console.error('EmailLog write error:', err));
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
messageId: result.messageId,
|
||||
@@ -203,6 +232,26 @@ export async function sendEmail(
|
||||
}
|
||||
}
|
||||
|
||||
// E-Mail-Log erstellen (Fehler)
|
||||
if (logContext) {
|
||||
import('./emailLog.service.js').then(({ createEmailLog }) => {
|
||||
createEmailLog({
|
||||
fromAddress,
|
||||
toAddress: Array.isArray(params.to) ? params.to.join(', ') : params.to,
|
||||
subject: params.subject,
|
||||
context: logContext.context,
|
||||
customerId: logContext.customerId,
|
||||
triggeredBy: logContext.triggeredBy,
|
||||
smtpServer: credentials.host,
|
||||
smtpPort: credentials.port,
|
||||
smtpEncryption: credentials.encryption ?? 'SSL',
|
||||
smtpUser: credentials.user,
|
||||
success: false,
|
||||
errorMessage,
|
||||
}).catch((err) => console.error('EmailLog write error:', err));
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
|
||||
@@ -47,6 +47,9 @@ export async function getAllUsers(filters: UserFilters) {
|
||||
lastName: true,
|
||||
isActive: true,
|
||||
customerId: true,
|
||||
whatsappNumber: true,
|
||||
telegramUsername: true,
|
||||
signalNumber: true,
|
||||
createdAt: true,
|
||||
roles: {
|
||||
include: {
|
||||
@@ -62,21 +65,25 @@ export async function getAllUsers(filters: UserFilters) {
|
||||
prisma.user.count({ where }),
|
||||
]);
|
||||
|
||||
// Get Developer role ID
|
||||
const developerRole = await prisma.role.findFirst({
|
||||
where: { name: 'Developer' },
|
||||
});
|
||||
// Get hidden role IDs
|
||||
const [developerRole, gdprRole] = await Promise.all([
|
||||
prisma.role.findFirst({ where: { name: 'Developer' } }),
|
||||
prisma.role.findFirst({ where: { name: 'DSGVO' } }),
|
||||
]);
|
||||
|
||||
return {
|
||||
users: users.map((u) => {
|
||||
// Check if user has developer role assigned
|
||||
const hasDeveloperAccess = developerRole
|
||||
? u.roles.some((ur) => ur.roleId === developerRole.id)
|
||||
: false;
|
||||
const hasGdprAccess = gdprRole
|
||||
? u.roles.some((ur) => ur.roleId === gdprRole.id)
|
||||
: false;
|
||||
return {
|
||||
...u,
|
||||
roles: u.roles.map((r) => r.role),
|
||||
hasDeveloperAccess,
|
||||
hasGdprAccess,
|
||||
};
|
||||
}),
|
||||
pagination: buildPaginationResponse(page, limit, total),
|
||||
@@ -93,6 +100,9 @@ export async function getUserById(id: number) {
|
||||
lastName: true,
|
||||
isActive: true,
|
||||
customerId: true,
|
||||
whatsappNumber: true,
|
||||
telegramUsername: true,
|
||||
signalNumber: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
roles: {
|
||||
@@ -135,6 +145,10 @@ export async function createUser(data: {
|
||||
roleIds: number[];
|
||||
customerId?: number;
|
||||
hasDeveloperAccess?: boolean;
|
||||
hasGdprAccess?: boolean;
|
||||
whatsappNumber?: string;
|
||||
telegramUsername?: string;
|
||||
signalNumber?: string;
|
||||
}) {
|
||||
const hashedPassword = await bcrypt.hash(data.password, 10);
|
||||
|
||||
@@ -145,6 +159,9 @@ export async function createUser(data: {
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
customerId: data.customerId,
|
||||
whatsappNumber: data.whatsappNumber || null,
|
||||
telegramUsername: data.telegramUsername || null,
|
||||
signalNumber: data.signalNumber || null,
|
||||
roles: {
|
||||
create: data.roleIds.map((roleId) => ({ roleId })),
|
||||
},
|
||||
@@ -167,6 +184,11 @@ export async function createUser(data: {
|
||||
await setUserDeveloperAccess(user.id, true);
|
||||
}
|
||||
|
||||
// DSGVO-Zugriff setzen falls aktiviert
|
||||
if (data.hasGdprAccess) {
|
||||
await setUserGdprAccess(user.id, true);
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
@@ -181,9 +203,13 @@ export async function updateUser(
|
||||
roleIds?: number[];
|
||||
customerId?: number;
|
||||
hasDeveloperAccess?: boolean;
|
||||
hasGdprAccess?: boolean;
|
||||
whatsappNumber?: string;
|
||||
telegramUsername?: string;
|
||||
signalNumber?: string;
|
||||
}
|
||||
) {
|
||||
const { roleIds, password, hasDeveloperAccess, ...userData } = data;
|
||||
const { roleIds, password, hasDeveloperAccess, hasGdprAccess, ...userData } = data;
|
||||
|
||||
// Check if this would remove the last admin
|
||||
const isBeingDeactivated = userData.isActive === false;
|
||||
@@ -311,18 +337,20 @@ export async function updateUser(
|
||||
}
|
||||
|
||||
// Handle developer access
|
||||
console.log('updateUser - hasDeveloperAccess:', hasDeveloperAccess);
|
||||
if (hasDeveloperAccess !== undefined) {
|
||||
await setUserDeveloperAccess(id, hasDeveloperAccess);
|
||||
}
|
||||
|
||||
// Handle GDPR access
|
||||
if (hasGdprAccess !== undefined) {
|
||||
await setUserGdprAccess(id, hasGdprAccess);
|
||||
}
|
||||
|
||||
return getUserById(id);
|
||||
}
|
||||
|
||||
// Helper to set developer access for a user
|
||||
async function setUserDeveloperAccess(userId: number, enabled: boolean) {
|
||||
console.log('setUserDeveloperAccess called - userId:', userId, 'enabled:', enabled);
|
||||
|
||||
// Get or create developer:access permission
|
||||
let developerPerm = await prisma.permission.findFirst({
|
||||
where: { resource: 'developer', action: 'access' },
|
||||
@@ -356,11 +384,7 @@ async function setUserDeveloperAccess(userId: number, enabled: boolean) {
|
||||
where: { userId, roleId: developerRole.id },
|
||||
});
|
||||
|
||||
console.log('setUserDeveloperAccess - developerRole.id:', developerRole.id, 'hasRole:', hasRole);
|
||||
|
||||
if (enabled && !hasRole) {
|
||||
// Add Developer role
|
||||
console.log('Adding Developer role');
|
||||
await prisma.userRole.create({
|
||||
data: { userId, roleId: developerRole.id },
|
||||
});
|
||||
@@ -370,8 +394,6 @@ async function setUserDeveloperAccess(userId: number, enabled: boolean) {
|
||||
data: { tokenInvalidatedAt: new Date() },
|
||||
});
|
||||
} else if (!enabled && hasRole) {
|
||||
// Remove Developer role
|
||||
console.log('Removing Developer role');
|
||||
await prisma.userRole.delete({
|
||||
where: { userId_roleId: { userId, roleId: developerRole.id } },
|
||||
});
|
||||
@@ -380,8 +402,56 @@ async function setUserDeveloperAccess(userId: number, enabled: boolean) {
|
||||
where: { id: userId },
|
||||
data: { tokenInvalidatedAt: new Date() },
|
||||
});
|
||||
} else {
|
||||
console.log('No action needed - enabled:', enabled, 'hasRole:', !!hasRole);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to set GDPR access for a user
|
||||
async function setUserGdprAccess(userId: number, enabled: boolean) {
|
||||
// Get or create DSGVO role
|
||||
let gdprRole = await prisma.role.findFirst({
|
||||
where: { name: 'DSGVO' },
|
||||
});
|
||||
|
||||
if (!gdprRole) {
|
||||
// Create DSGVO role with all audit:* and gdpr:* permissions
|
||||
const gdprPermissions = await prisma.permission.findMany({
|
||||
where: {
|
||||
OR: [{ resource: 'audit' }, { resource: 'gdpr' }],
|
||||
},
|
||||
});
|
||||
|
||||
gdprRole = await prisma.role.create({
|
||||
data: {
|
||||
name: 'DSGVO',
|
||||
description: 'DSGVO-Zugriff: Audit-Logs und Datenschutz-Verwaltung',
|
||||
permissions: {
|
||||
create: gdprPermissions.map((p) => ({ permissionId: p.id })),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Check if user already has DSGVO role
|
||||
const hasRole = await prisma.userRole.findFirst({
|
||||
where: { userId, roleId: gdprRole.id },
|
||||
});
|
||||
|
||||
if (enabled && !hasRole) {
|
||||
await prisma.userRole.create({
|
||||
data: { userId, roleId: gdprRole.id },
|
||||
});
|
||||
await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: { tokenInvalidatedAt: new Date() },
|
||||
});
|
||||
} else if (!enabled && hasRole) {
|
||||
await prisma.userRole.delete({
|
||||
where: { userId_roleId: { userId, roleId: gdprRole.id } },
|
||||
});
|
||||
await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: { tokenInvalidatedAt: new Date() },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user