mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-17 19:44:12 +00:00
Tests: - Update frontend.e2e.spec.ts with locale testing - Add search.e2e.spec.ts for search functionality - Add i18n.int.spec.ts for localization tests - Add search.int.spec.ts for search integration - Update playwright.config.ts Documentation: - Add CLAUDE.md with project instructions - Add docs/ directory with detailed documentation - Add scripts/ for utility scripts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
853 lines
No EOL
23 KiB
Markdown
853 lines
No EOL
23 KiB
Markdown
# 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<void> => {
|
|
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) | |