opencrm/backend/src/services/consent-public.service.ts

188 lines
4.8 KiB
TypeScript

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(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/&nbsp;/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();
});
}