Datenschutzerklärung als unterschreibbare PDF-Vorlage

Neuer Endpoint GET /api/gdpr/customer/:customerId/privacy-pdf
generiert eine PDF mit:
- Titel
- Personalisiertem Kopf (Name / Firma + Kundennummer + Datum)
- Voller Datenschutzerklärung (HTML → Text)
- Einwilligungsklausel
- Unterschriftenblock (Ort/Datum links, Unterschrift rechts,
  zweite Linie "Name in Druckbuchstaben" mit vorausgefuelltem
  Kundennamen)

Auth: customers:read + canAccessCustomer. Filename:
"datenschutzerklaerung-<kundennummer>.pdf".

Im Tab "Einwilligungen / Datenschutz" beim Kunden gibt es jetzt
direkt neben dem Upload-Feld den Link "Vorlage zum Unterschreiben"
– Ausdrucken, unterschreiben lassen, scannen, wieder hochladen.

Verifiziert auf dev: Magic-Bytes %PDF-1.3, %%EOF-Marker am Ende,
2 KB Output, pdftotext zeigt korrekten Aufbau inkl. Unterschrift-
Linien.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-24 15:30:11 +02:00
parent 69a52ffe03
commit 897abc7b21
5 changed files with 162 additions and 5 deletions
@@ -1114,3 +1114,35 @@ export async function getMyAuthorizationStatus(req: AuthRequest, res: Response)
res.status(500).json({ success: false, error: 'Fehler beim Laden' });
}
}
/**
* Unterschreibbare Datenschutzerklärung (Papierform) als PDF generieren.
* Verwendung: Mitarbeiter klickt im Tab "Einwilligungen / Datenschutz"
* auf "Vorlage zum Unterschreiben", PDF kommt mit personalisiertem
* Kopf + Unterschriftsfeld zum Ausdrucken zurück.
*
* GET /api/gdpr/customer/:customerId/privacy-pdf
*/
export async function getSignablePrivacyPdf(req: AuthRequest, res: Response) {
try {
const customerId = parseInt(req.params.customerId, 10);
if (!Number.isFinite(customerId) || customerId < 1) {
return res.status(400).json({ success: false, error: 'Ungültige Kunden-ID' });
}
if (!(await canAccessCustomer(req, res, customerId))) return;
const pdf = await consentPublicService.generateSignablePrivacyPdf(customerId);
const customer = await prisma.customer.findUnique({
where: { id: customerId },
select: { customerNumber: true },
});
const filename = `datenschutzerklaerung-${customer?.customerNumber || customerId}.pdf`;
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
res.send(pdf);
} catch (error) {
console.error('Fehler bei Datenschutz-PDF:', error);
res.status(500).json({ success: false, error: 'Fehler beim Generieren der PDF' });
}
}