# PROMPT: Consent Management System - Payload Backend ## Kontext Du arbeitest auf dem Server **sv-payload** (10.10.181.100) im Verzeichnis `/home/payload/payload-cms`. Dieses Projekt implementiert ein DSGVO-konformes Consent Management System gemäß Spezifikation SAS v2.6. Das System ist Multi-Tenant-fähig und nutzt die bestehende Tenants-Collection. ## Referenz-Dokument Basis: **Systemarchitektur-Spezifikation v2.6 (Implementation Master)** ## Aufgabe Erstelle drei neue Collections und die zugehörigen Hooks für das Consent Management: 1. **CookieConfigurations** - Mandantenspezifische Banner-Konfiguration 2. **CookieInventory** - Cookie-Dokumentation für Datenschutzerklärung 3. **ConsentLogs** - WORM Audit-Trail für Einwilligungen --- ## Schritt 1: Collection - CookieConfigurations Erstelle `src/collections/CookieConfigurations.ts`: ```typescript import type { CollectionConfig } from 'payload' export const CookieConfigurations: CollectionConfig = { slug: 'cookie-configurations', admin: { useAsTitle: 'title', group: 'Consent Management', description: 'Cookie-Banner Konfiguration pro Tenant', }, access: { // Öffentlich lesbar für Frontend-Initialisierung read: () => true, // Nur authentifizierte User können bearbeiten create: ({ req }) => !!req.user, update: ({ req }) => !!req.user, delete: ({ req }) => !!req.user, }, fields: [ { name: 'tenant', type: 'relationship', relationTo: 'tenants', required: true, unique: true, admin: { description: 'Jeder Tenant kann nur eine Konfiguration haben', }, }, { name: 'title', type: 'text', required: true, defaultValue: 'Cookie-Einstellungen', admin: { description: 'Interner Titel zur Identifikation', }, }, { name: 'revision', type: 'number', required: true, defaultValue: 1, admin: { description: 'Bei inhaltlichen Änderungen erhöhen → erzwingt erneuten Consent bei allen Nutzern', }, }, { name: 'enabledCategories', type: 'select', hasMany: true, required: true, defaultValue: ['necessary', 'analytics'], options: [ { label: 'Notwendig', value: 'necessary' }, { label: 'Funktional', value: 'functional' }, { label: 'Statistik', value: 'analytics' }, { label: 'Marketing', value: 'marketing' }, ], admin: { description: 'Welche Kategorien sollen im Banner angezeigt werden?', }, }, { name: 'translations', type: 'group', fields: [ { name: 'de', type: 'group', label: 'Deutsch', fields: [ { name: 'bannerTitle', type: 'text', defaultValue: 'Wir respektieren Ihre Privatsphäre', }, { name: 'bannerDescription', type: 'textarea', defaultValue: 'Diese Website verwendet Cookies, um Ihnen die bestmögliche Erfahrung zu bieten. Sie können Ihre Einstellungen jederzeit anpassen.', }, { name: 'acceptAllButton', type: 'text', defaultValue: 'Alle akzeptieren', }, { name: 'acceptNecessaryButton', type: 'text', defaultValue: 'Nur notwendige', }, { name: 'settingsButton', type: 'text', defaultValue: 'Einstellungen', }, { name: 'saveButton', type: 'text', defaultValue: 'Auswahl speichern', }, { name: 'privacyPolicyUrl', type: 'text', defaultValue: '/datenschutz', admin: { description: 'Link zur Datenschutzerklärung', }, }, { name: 'categoryLabels', type: 'group', fields: [ { name: 'necessary', type: 'group', fields: [ { name: 'title', type: 'text', defaultValue: 'Notwendig' }, { name: 'description', type: 'textarea', defaultValue: 'Diese Cookies sind für die Grundfunktionen der Website erforderlich.' }, ], }, { name: 'functional', type: 'group', fields: [ { name: 'title', type: 'text', defaultValue: 'Funktional' }, { name: 'description', type: 'textarea', defaultValue: 'Diese Cookies ermöglichen erweiterte Funktionen und Personalisierung.' }, ], }, { name: 'analytics', type: 'group', fields: [ { name: 'title', type: 'text', defaultValue: 'Statistik' }, { name: 'description', type: 'textarea', defaultValue: 'Diese Cookies helfen uns zu verstehen, wie Besucher mit der Website interagieren.' }, ], }, { name: 'marketing', type: 'group', fields: [ { name: 'title', type: 'text', defaultValue: 'Marketing' }, { name: 'description', type: 'textarea', defaultValue: 'Diese Cookies werden verwendet, um Werbung relevanter für Sie zu gestalten.' }, ], }, ], }, ], }, ], }, { name: 'styling', type: 'group', admin: { description: 'Optionale Anpassung des Banner-Designs', }, fields: [ { name: 'position', type: 'select', defaultValue: 'bottom', options: [ { label: 'Unten', value: 'bottom' }, { label: 'Oben', value: 'top' }, { label: 'Mitte (Modal)', value: 'middle' }, ], }, { name: 'theme', type: 'select', defaultValue: 'dark', options: [ { label: 'Dunkel', value: 'dark' }, { label: 'Hell', value: 'light' }, { label: 'Auto (System)', value: 'auto' }, ], }, ], }, ], } ``` --- ## Schritt 2: Collection - CookieInventory Erstelle `src/collections/CookieInventory.ts`: ```typescript import type { CollectionConfig } from 'payload' export const CookieInventory: CollectionConfig = { slug: 'cookie-inventory', admin: { useAsTitle: 'name', group: 'Consent Management', description: 'Dokumentation aller verwendeten Cookies für die Datenschutzerklärung', defaultColumns: ['name', 'provider', 'category', 'duration', 'tenant'], }, access: { // Öffentlich lesbar für Datenschutzerklärung read: () => true, create: ({ req }) => !!req.user, update: ({ req }) => !!req.user, delete: ({ req }) => !!req.user, }, fields: [ { name: 'tenant', type: 'relationship', relationTo: 'tenants', required: true, admin: { description: 'Zuordnung zum Mandanten', }, }, { name: 'name', type: 'text', required: true, admin: { description: 'Technischer Name des Cookies (z.B. "_ga", "cc_cookie")', }, }, { name: 'provider', type: 'text', required: true, admin: { description: 'Anbieter/Setzer des Cookies (z.B. "Google LLC", "Eigene Website")', }, }, { name: 'category', type: 'select', required: true, options: [ { label: 'Notwendig', value: 'necessary' }, { label: 'Funktional', value: 'functional' }, { label: 'Statistik', value: 'analytics' }, { label: 'Marketing', value: 'marketing' }, ], }, { name: 'duration', type: 'text', required: true, admin: { description: 'Speicherdauer (z.B. "Session", "1 Jahr", "2 Jahre")', }, }, { name: 'description', type: 'textarea', required: true, admin: { description: 'Verständliche Erklärung des Zwecks für Endnutzer', }, }, { name: 'isActive', type: 'checkbox', defaultValue: true, admin: { description: 'Wird dieser Cookie aktuell verwendet?', }, }, ], } ``` --- ## Schritt 3: Collection - ConsentLogs (WORM) Erstelle `src/collections/ConsentLogs.ts`: ```typescript import type { CollectionConfig } from 'payload' import crypto from 'crypto' // Helper: Täglicher Salt für IP-Anonymisierung const getDailySalt = (tenantId: string): string => { const date = new Date().toISOString().split('T')[0] // YYYY-MM-DD const pepper = process.env.IP_ANONYMIZATION_PEPPER || 'default-pepper-change-me' return crypto.createHash('sha256').update(`${pepper}-${tenantId}-${date}`).digest('hex') } // Helper: IP anonymisieren const anonymizeIp = (ip: string, tenantId: string): string => { const salt = getDailySalt(tenantId) return crypto.createHmac('sha256', salt).update(ip).digest('hex').substring(0, 32) } // Helper: IP aus Request extrahieren const extractIp = (req: any): string => { const forwarded = req.headers?.['x-forwarded-for'] if (typeof forwarded === 'string') { return forwarded.split(',')[0].trim() } if (Array.isArray(forwarded)) { return forwarded[0] } return req.socket?.remoteAddress || req.ip || 'unknown' } export const ConsentLogs: CollectionConfig = { slug: 'consent-logs', admin: { useAsTitle: 'consentId', group: 'Consent Management', description: 'WORM Audit-Trail für Cookie-Einwilligungen (unveränderbar)', defaultColumns: ['consentId', 'tenant', 'categories', 'revision', 'createdAt'], }, // Keine Versionierung/Drafts für Performance bei hohem Schreibvolumen versions: false, access: { // Erstellen nur mit API-Key (wird in Hook geprüft) create: ({ req }) => { const apiKey = req.headers?.['x-api-key'] const validKey = process.env.CONSENT_LOGGING_API_KEY return apiKey === validKey }, // Lesen nur für authentifizierte Admin-User read: ({ req }) => !!req.user, // WORM: Keine Updates erlaubt update: () => false, // WORM: Keine Deletes über API (nur via Retention Job) delete: () => false, }, hooks: { beforeChange: [ ({ data, req, operation }) => { if (operation !== 'create') return data // 1. Server-generierte Consent-ID (Trust Boundary) data.consentId = crypto.randomUUID() // 2. IP anonymisieren const rawIp = data.ip || extractIp(req) const tenantId = typeof data.tenant === 'object' ? data.tenant.id : data.tenant data.anonymizedIp = anonymizeIp(rawIp, String(tenantId)) // Rohe IP entfernen (nie speichern!) delete data.ip // 3. Ablaufdatum setzen (3 Jahre Retention) const threeYearsFromNow = new Date() threeYearsFromNow.setFullYear(threeYearsFromNow.getFullYear() + 3) data.expiresAt = threeYearsFromNow.toISOString() // 4. User Agent kürzen (Datensparsamkeit) if (data.userAgent && data.userAgent.length > 500) { data.userAgent = data.userAgent.substring(0, 500) } return data }, ], }, fields: [ { name: 'consentId', type: 'text', required: true, unique: true, admin: { readOnly: true, description: 'Server-generierte eindeutige ID', }, }, { name: 'clientRef', type: 'text', admin: { readOnly: true, description: 'Client-seitige Referenz (Cookie-UUID) für Traceability', }, }, { name: 'tenant', type: 'relationship', relationTo: 'tenants', required: true, admin: { readOnly: true, }, }, { name: 'categories', type: 'json', required: true, admin: { readOnly: true, description: 'Akzeptierte Kategorien zum Zeitpunkt der Einwilligung', }, }, { name: 'revision', type: 'number', required: true, admin: { readOnly: true, description: 'Version der Konfiguration zum Zeitpunkt der Zustimmung', }, }, { name: 'userAgent', type: 'text', admin: { readOnly: true, description: 'Browser/Device (für Forensik und Bot-Erkennung)', }, }, { name: 'anonymizedIp', type: 'text', admin: { readOnly: true, description: 'HMAC-Hash der IP (täglich rotierender, tenant-spezifischer Salt)', }, }, { name: 'expiresAt', type: 'date', required: true, admin: { readOnly: true, description: 'Automatische Löschung nach 3 Jahren', date: { pickerAppearance: 'dayOnly', }, }, }, ], } ``` --- ## Schritt 4: Retention Job (Automatische Löschung) Erstelle `src/jobs/consentRetentionJob.ts`: ```typescript import type { Payload } from 'payload' /** * Consent Retention Job * * Löscht abgelaufene ConsentLogs gemäß DSGVO Art. 5 Abs. 1e (Speicherbegrenzung). * Sollte täglich via Cron ausgeführt werden. */ export const runConsentRetentionJob = async (payload: Payload): Promise => { const now = new Date().toISOString() try { // Finde abgelaufene Einträge const expired = await payload.find({ collection: 'consent-logs', where: { expiresAt: { less_than: now, }, }, limit: 1000, // Batch-Größe }) if (expired.docs.length === 0) { console.log('[ConsentRetention] Keine abgelaufenen Einträge gefunden.') return } console.log(`[ConsentRetention] ${expired.docs.length} abgelaufene Einträge gefunden. Lösche...`) // Lösche jeden Eintrag einzeln (WORM-Bypass via direktem DB-Zugriff) // Da delete: () => false gesetzt ist, müssen wir den DB-Adapter direkt nutzen for (const doc of expired.docs) { await payload.db.deleteOne({ collection: 'consent-logs', where: { id: { equals: doc.id } }, }) } console.log(`[ConsentRetention] ${expired.docs.length} Einträge gelöscht.`) // Falls mehr als 1000 Einträge: Rekursiv weitermachen if (expired.docs.length === 1000) { console.log('[ConsentRetention] Weitere Einträge vorhanden, führe nächsten Batch aus...') await runConsentRetentionJob(payload) } } catch (error) { console.error('[ConsentRetention] Fehler:', error) throw error } } ``` --- ## Schritt 5: Job-Scheduler einrichten Erstelle `src/jobs/scheduler.ts`: ```typescript import cron from 'node-cron' import type { Payload } from 'payload' import { runConsentRetentionJob } from './consentRetentionJob' /** * Initialisiert alle Scheduled Jobs */ export const initScheduledJobs = (payload: Payload): void => { // Consent Retention: Täglich um 03:00 Uhr cron.schedule('0 3 * * *', async () => { console.log('[Scheduler] Starte Consent Retention Job...') try { await runConsentRetentionJob(payload) } catch (error) { console.error('[Scheduler] Consent Retention Job fehlgeschlagen:', error) } }, { timezone: 'Europe/Berlin' }) console.log('[Scheduler] Scheduled Jobs initialisiert.') } ``` --- ## Schritt 6: Collections registrieren Aktualisiere `src/payload.config.ts`: ```typescript import { buildConfig } from 'payload' import { postgresAdapter } from '@payloadcms/db-postgres' import { lexicalEditor } from '@payloadcms/richtext-lexical' import { multiTenantPlugin } from '@payloadcms/plugin-multi-tenant' import path from 'path' import { fileURLToPath } from 'url' // Existing Collections import { Users } from './collections/Users' import { Media } from './collections/Media' import { Tenants } from './collections/Tenants' import { Pages } from './collections/Pages' import { Posts } from './collections/Posts' import { Categories } from './collections/Categories' import { SocialLinks } from './collections/SocialLinks' // NEW: Consent Management Collections import { CookieConfigurations } from './collections/CookieConfigurations' import { CookieInventory } from './collections/CookieInventory' import { ConsentLogs } from './collections/ConsentLogs' // Existing Globals import { SiteSettings } from './globals/SiteSettings' import { Navigation } from './globals/Navigation' const filename = fileURLToPath(import.meta.url) const dirname = path.dirname(filename) export default buildConfig({ admin: { user: Users.slug, }, serverURL: process.env.PAYLOAD_PUBLIC_SERVER_URL || 'https://pl.c2sgmbh.de', cors: [ 'http://localhost:3000', 'http://localhost:3001', 'http://10.10.181.102:3000', 'http://10.10.181.102:3001', 'https://dev.zh3.de', 'https://porwoll.de', 'https://www.porwoll.de', 'https://complexcaresolutions.de', 'https://www.complexcaresolutions.de', 'https://gunshin.de', 'https://www.gunshin.de', ], csrf: [ 'http://localhost:3000', 'http://localhost:3001', 'http://10.10.181.102:3000', 'http://10.10.181.102:3001', 'https://dev.zh3.de', 'https://porwoll.de', 'https://www.porwoll.de', 'https://complexcaresolutions.de', 'https://www.complexcaresolutions.de', 'https://gunshin.de', 'https://www.gunshin.de', ], collections: [ Users, Media, Tenants, Pages, Posts, Categories, SocialLinks, // NEW: Consent Management CookieConfigurations, CookieInventory, ConsentLogs, ], globals: [SiteSettings, Navigation], editor: lexicalEditor(), secret: process.env.PAYLOAD_SECRET || '', typescript: { outputFile: path.resolve(dirname, 'payload-types.ts'), }, db: postgresAdapter({ pool: { connectionString: process.env.DATABASE_URI || '', }, }), plugins: [ multiTenantPlugin({ tenantsSlug: 'tenants', collections: { media: {}, pages: {}, posts: {}, categories: {}, 'social-links': {}, // NEW: Consent Collections mit Tenant-Scoping 'cookie-configurations': {}, 'cookie-inventory': {}, 'consent-logs': {}, }, }), ], }) ``` --- ## Schritt 7: Scheduler in Server einbinden Aktualisiere `src/server.ts` (oder erstelle, falls nicht vorhanden): ```typescript import express from 'express' import payload from 'payload' import { initScheduledJobs } from './jobs/scheduler' const app = express() const start = async () => { await payload.init({ secret: process.env.PAYLOAD_SECRET || '', express: app, onInit: async () => { payload.logger.info(`Payload Admin URL: ${payload.getAdminURL()}`) // Scheduled Jobs starten initScheduledJobs(payload) }, }) app.listen(3000) } start() ``` --- ## Schritt 8: Dependencies installieren ```bash cd /home/payload/payload-cms pnpm add node-cron pnpm add -D @types/node-cron ``` --- ## Schritt 9: Environment Variables Füge zu `.env` hinzu: ```env # Consent Management CONSENT_LOGGING_API_KEY=GENERIERE_EINEN_SICHEREN_KEY_HIER IP_ANONYMIZATION_PEPPER=GENERIERE_EINEN_ANDEREN_SICHEREN_KEY_HIER ``` Generiere sichere Keys: ```bash # Auf sv-payload openssl rand -hex 32 # Für CONSENT_LOGGING_API_KEY openssl rand -hex 32 # Für IP_ANONYMIZATION_PEPPER ``` --- ## Schritt 10: Migrationen und Build ```bash cd /home/payload/payload-cms # TypeScript Types generieren pnpm payload generate:types # Migrationen erstellen und ausführen pnpm payload migrate:create pnpm payload migrate # Build pnpm build # PM2 neustarten pm2 restart payload ``` --- ## Schritt 11: Verifizierung ### API-Endpoints testen ```bash # CookieConfigurations (öffentlich) curl -s http://localhost:3000/api/cookie-configurations | jq # CookieInventory (öffentlich) curl -s http://localhost:3000/api/cookie-inventory | jq # ConsentLogs erstellen (mit API-Key) curl -X POST http://localhost:3000/api/consent-logs \ -H "Content-Type: application/json" \ -H "x-api-key: DEIN_CONSENT_LOGGING_API_KEY" \ -d '{ "clientRef": "test-client-123", "tenant": 1, "categories": ["necessary", "analytics"], "revision": 1, "userAgent": "Mozilla/5.0 Test", "ip": "192.168.1.100" }' | jq # ConsentLogs lesen (nur mit Admin-Auth) # → Über Admin Panel: https://pl.c2sgmbh.de/admin/collections/consent-logs ``` ### Prüfpunkte - [ ] CookieConfigurations Collection erscheint im Admin unter "Consent Management" - [ ] CookieInventory Collection erscheint im Admin - [ ] ConsentLogs Collection erscheint im Admin (nur lesbar) - [ ] ConsentLogs: Update-Button ist deaktiviert - [ ] ConsentLogs: Delete-Button ist deaktiviert - [ ] API-Erstellung von ConsentLogs funktioniert nur mit korrektem API-Key - [ ] `consentId` wird serverseitig generiert (nicht vom Client überschreibbar) - [ ] `anonymizedIp` ist ein Hash, keine echte IP - [ ] `expiresAt` wird automatisch auf +3 Jahre gesetzt --- ## Schritt 12: Initiale Daten anlegen (Optional) ### Cookie-Konfiguration für porwoll.de Im Admin Panel unter **Consent Management → Cookie Configurations → Create**: | Feld | Wert | |------|------| | Tenant | porwoll.de | | Title | Cookie-Einstellungen porwoll.de | | Revision | 1 | | Enabled Categories | Notwendig, Statistik | ### Cookie-Inventory für porwoll.de Im Admin Panel unter **Consent Management → Cookie Inventory → Create**: | Name | Provider | Category | Duration | Description | |------|----------|----------|----------|-------------| | cc_cookie | Eigene Website | Notwendig | 1 Jahr | Speichert Ihre Cookie-Einstellungen | | _ga | Google LLC | Statistik | 2 Jahre | Google Analytics - Unterscheidung von Nutzern | | _ga_* | Google LLC | Statistik | 2 Jahre | Google Analytics - Session-Daten | --- ## Zusammenfassung der erstellten Dateien | Datei | Beschreibung | |-------|--------------| | `src/collections/CookieConfigurations.ts` | Banner-Konfiguration pro Tenant | | `src/collections/CookieInventory.ts` | Cookie-Dokumentation | | `src/collections/ConsentLogs.ts` | WORM Audit-Trail mit Hooks | | `src/jobs/consentRetentionJob.ts` | Automatische Löschung nach 3 Jahren | | `src/jobs/scheduler.ts` | Cron-Scheduler für Jobs | ## Neue Environment Variables | Variable | Beschreibung | |----------|--------------| | `CONSENT_LOGGING_API_KEY` | API-Key für Frontend-zu-Backend Logging | | `IP_ANONYMIZATION_PEPPER` | Geheimer Pepper für IP-Hashing | ## API-Endpoints | Endpoint | Method | Auth | Beschreibung | |----------|--------|------|--------------| | `/api/cookie-configurations` | GET | Public | Banner-Config abrufen | | `/api/cookie-inventory` | GET | Public | Cookie-Liste für Datenschutz | | `/api/consent-logs` | POST | x-api-key | Consent loggen | | `/api/consent-logs` | GET | Admin | Logs einsehen (nur Admin) |