-- Migration: Banner Consent Tables -- Für @breakpilot/consent-sdk -- DSGVO/TTDSG-konforme Speicherung von Cookie-Einwilligungen -- ======================================== -- Banner Consents (anonyme Einwilligungen) -- ======================================== CREATE TABLE IF NOT EXISTS banner_consents ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), site_id VARCHAR(50) NOT NULL, device_fingerprint VARCHAR(64) NOT NULL, user_id VARCHAR(100), -- Optional: Für eingeloggte Nutzer -- Consent-Daten categories JSONB NOT NULL DEFAULT '{}', -- { "analytics": true, "marketing": false } vendors JSONB DEFAULT '{}', -- { "google-analytics": true } tcf_string TEXT, -- IAB TCF String -- Metadaten (anonymisiert) ip_hash VARCHAR(64), -- Anonymisierte IP user_agent TEXT, language VARCHAR(10), platform VARCHAR(20), -- web, ios, android app_version VARCHAR(20), -- Versionierung version VARCHAR(20) DEFAULT '1.0.0', -- Zeitstempel created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), expires_at TIMESTAMPTZ, revoked_at TIMESTAMPTZ, -- Constraints CONSTRAINT unique_site_device UNIQUE (site_id, device_fingerprint) ); -- Indizes für schnelle Abfragen CREATE INDEX IF NOT EXISTS idx_banner_consents_site ON banner_consents(site_id); CREATE INDEX IF NOT EXISTS idx_banner_consents_user ON banner_consents(user_id) WHERE user_id IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_banner_consents_device ON banner_consents(device_fingerprint); CREATE INDEX IF NOT EXISTS idx_banner_consents_created ON banner_consents(created_at); CREATE INDEX IF NOT EXISTS idx_banner_consents_expires ON banner_consents(expires_at) WHERE expires_at IS NOT NULL; -- ======================================== -- Audit Log (unveränderbar) -- ======================================== CREATE TABLE IF NOT EXISTS banner_consent_audit_log ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), consent_id UUID NOT NULL, action VARCHAR(20) NOT NULL, -- created, updated, revoked details JSONB, ip_hash VARCHAR(64), created_at TIMESTAMPTZ DEFAULT NOW() ); -- Kein UPDATE/DELETE auf Audit-Log -- REVOKE UPDATE, DELETE ON banner_consent_audit_log FROM PUBLIC; CREATE INDEX IF NOT EXISTS idx_banner_audit_consent ON banner_consent_audit_log(consent_id); CREATE INDEX IF NOT EXISTS idx_banner_audit_created ON banner_consent_audit_log(created_at); -- ======================================== -- Site-Konfigurationen -- ======================================== CREATE TABLE IF NOT EXISTS banner_site_configs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), site_id VARCHAR(50) UNIQUE NOT NULL, site_name VARCHAR(100) NOT NULL, -- UI-Konfiguration ui_theme VARCHAR(20) DEFAULT 'auto', ui_position VARCHAR(20) DEFAULT 'bottom', ui_layout VARCHAR(20) DEFAULT 'modal', custom_css TEXT, -- Rechtliche Links privacy_policy_url VARCHAR(255), imprint_url VARCHAR(255), dpo_name VARCHAR(100), dpo_email VARCHAR(100), -- TCF 2.2 tcf_enabled BOOLEAN DEFAULT FALSE, tcf_cmp_id INTEGER, tcf_cmp_version INTEGER, -- Zeitstempel created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); -- ======================================== -- Kategorie-Konfigurationen pro Site -- ======================================== CREATE TABLE IF NOT EXISTS banner_category_configs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), site_id VARCHAR(50) NOT NULL, category_id VARCHAR(50) NOT NULL, -- Namen (mehrsprachig) name_de VARCHAR(100) NOT NULL, name_en VARCHAR(100), -- Beschreibungen (mehrsprachig) description_de TEXT, description_en TEXT, -- Einstellungen is_required BOOLEAN DEFAULT FALSE, sort_order INTEGER DEFAULT 0, is_active BOOLEAN DEFAULT TRUE, -- Zeitstempel created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), CONSTRAINT unique_site_category UNIQUE (site_id, category_id) ); -- ======================================== -- Vendor-Konfigurationen -- ======================================== CREATE TABLE IF NOT EXISTS banner_vendor_configs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), site_id VARCHAR(50) NOT NULL, category_id VARCHAR(50) NOT NULL, vendor_id VARCHAR(100) NOT NULL, -- Vendor-Informationen name VARCHAR(100) NOT NULL, privacy_policy_url VARCHAR(255), data_retention VARCHAR(50), data_transfer VARCHAR(100), -- TCF tcf_vendor_id INTEGER, tcf_purposes JSONB, tcf_legitimate_interests JSONB, -- Cookies cookies JSONB DEFAULT '[]', -- Zeitstempel created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), CONSTRAINT unique_site_vendor UNIQUE (site_id, vendor_id) ); -- ======================================== -- Helper-Funktionen -- ======================================== -- Abgelaufene Consents bereinigen CREATE OR REPLACE FUNCTION cleanup_expired_banner_consents() RETURNS INTEGER AS $$ DECLARE deleted_count INTEGER; BEGIN -- Soft-Delete nach 30 Tagen nach Ablauf WITH deleted AS ( DELETE FROM banner_consents WHERE expires_at < NOW() - INTERVAL '30 days' AND revoked_at IS NULL RETURNING id ) SELECT COUNT(*) INTO deleted_count FROM deleted; RETURN deleted_count; END; $$ LANGUAGE plpgsql; -- Statistik-View CREATE OR REPLACE VIEW banner_consent_stats AS SELECT site_id, DATE(created_at) as consent_date, COUNT(*) as total_consents, COUNT(*) FILTER (WHERE revoked_at IS NOT NULL) as revoked_consents, COUNT(*) FILTER (WHERE (categories->>'analytics')::boolean = true) as analytics_accepted, COUNT(*) FILTER (WHERE (categories->>'marketing')::boolean = true) as marketing_accepted, COUNT(*) FILTER (WHERE (categories->>'functional')::boolean = true) as functional_accepted, COUNT(*) FILTER (WHERE (categories->>'social')::boolean = true) as social_accepted FROM banner_consents GROUP BY site_id, DATE(created_at); -- ======================================== -- Standard-Kategorien einfügen -- ======================================== INSERT INTO banner_category_configs (site_id, category_id, name_de, name_en, description_de, description_en, is_required, sort_order) VALUES ('default', 'essential', 'Essentiell', 'Essential', 'Notwendig für die Grundfunktionen der Website.', 'Required for basic website functionality.', TRUE, 1), ('default', 'functional', 'Funktional', 'Functional', 'Ermöglicht Personalisierung und Komfortfunktionen.', 'Enables personalization and comfort features.', FALSE, 2), ('default', 'analytics', 'Statistik', 'Analytics', 'Hilft uns, die Website zu verbessern.', 'Helps us improve the website.', FALSE, 3), ('default', 'marketing', 'Marketing', 'Marketing', 'Ermöglicht personalisierte Werbung.', 'Enables personalized advertising.', FALSE, 4), ('default', 'social', 'Soziale Medien', 'Social Media', 'Ermöglicht Inhalte von sozialen Netzwerken.', 'Enables content from social networks.', FALSE, 5) ON CONFLICT (site_id, category_id) DO NOTHING; -- Fertig SELECT 'Banner Consent Tables created successfully' as status;