Files
opencrm/backend/src/services/consent.service.ts
T
2026-03-21 18:23:54 +01:00

268 lines
7.0 KiB
TypeScript

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: 'Elektronisches Marketing',
description: 'Zusendung von Werbung und Angeboten über elektronische Kommunikationswege (E-Mail, Messenger etc.)',
},
MARKETING_PHONE: {
label: 'Telefonmarketing',
description: 'Kontaktaufnahme zu Werbezwecken per Telefon',
},
DATA_SHARING_PARTNER: {
label: 'Datenweitergabe',
description: 'Weitergabe von Daten an Partnerunternehmen',
},
};