268 lines
7.0 KiB
TypeScript
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',
|
|
},
|
|
};
|