diff --git a/admin-compliance/app/sdk/einwilligungen/_components/BannerConsentsTab.tsx b/admin-compliance/app/sdk/einwilligungen/_components/BannerConsentsTab.tsx index a5ccf8f..bb0b5d3 100644 --- a/admin-compliance/app/sdk/einwilligungen/_components/BannerConsentsTab.tsx +++ b/admin-compliance/app/sdk/einwilligungen/_components/BannerConsentsTab.tsx @@ -27,6 +27,18 @@ const categoryColors: Record = { marketing: 'bg-pink-100 text-pink-700', } +const methodLabels: Record = { + accept_all: 'Alle akzeptiert', + reject_all: 'Nur notwendige', + custom_selection: 'Individuelle Auswahl', +} + +const methodColors: Record = { + accept_all: 'bg-green-100 text-green-700', + reject_all: 'bg-red-100 text-red-700', + custom_selection: 'bg-yellow-100 text-yellow-700', +} + export default function BannerConsentsTab() { const { records, sites, selectedSite, changeSite, @@ -76,7 +88,7 @@ export default function BannerConsentsTab() { Device Kategorien - Verknüpft mit + Methode Erteilt am Ablauf Browser @@ -102,10 +114,11 @@ export default function BannerConsentsTab() { - {record.linked_email - ? {record.linked_email} - : — (anonym) - } + {record.consent_method ? ( + + {methodLabels[record.consent_method] || record.consent_method} + + ) : } {formatDate(record.created_at)} {formatDate(record.expires_at)} @@ -171,6 +184,14 @@ export default function BannerConsentsTab() { ))} +
+ Methode + {detail.consent_method ? ( + + {methodLabels[detail.consent_method] || detail.consent_method} + + ) : '—'} +
Verknüpft mit {detail.linked_email || '— (anonym)'} @@ -178,16 +199,40 @@ export default function BannerConsentsTab() {
Erteilt{formatDate(detail.created_at)}
Ablauf{formatDate(detail.expires_at)}
Aktualisiert{formatDate(detail.updated_at)}
-
- User-Agent -

{detail.user_agent || '—'}

-
- {detail.ip_hash && ( -
- IP-Hash -

{detail.ip_hash}

-
+
Geltungsbereich{detail.consent_scope || '—'}
+ {detail.banner_version && ( +
Banner-Version{detail.banner_version}
)} + + {/* Tracking-Kontext */} +
+

Tracking-Kontext

+ {detail.page_url &&
Seite{detail.page_url}
} + {detail.referrer &&
Referrer{detail.referrer}
} + {detail.geo_country &&
Land{detail.geo_country}{detail.geo_region ? ` / ${detail.geo_region}` : ''}
} +
+ + {/* Device-Informationen */} +
+

Device

+
+ Typ{detail.device_type || '—'} + Browser{detail.browser || shortenUA(detail.user_agent)} + OS{detail.os || '—'} + Auflösung{detail.screen_resolution || '—'} +
+
+ + {/* Technische Details */} +
+

Technisch

+
+
User-Agent

{detail.user_agent || '—'}

+ {detail.ip_hash &&
IP-Hash

{detail.ip_hash}

} + {detail.session_id &&
Session

{detail.session_id}

} + {detail.banner_config_hash &&
Config-Hash

{detail.banner_config_hash}

} +
+
diff --git a/admin-compliance/app/sdk/einwilligungen/_types.ts b/admin-compliance/app/sdk/einwilligungen/_types.ts index d7d4476..756a4ca 100644 --- a/admin-compliance/app/sdk/einwilligungen/_types.ts +++ b/admin-compliance/app/sdk/einwilligungen/_types.ts @@ -112,6 +112,20 @@ export interface BannerConsentRecord { user_agent: string | null linked_email: string | null consent_string: string | null + // Vendor-agnostische Felder (Migration 107) + consent_method: string | null + banner_version: number | null + banner_config_hash: string | null + geo_country: string | null + geo_region: string | null + consent_scope: string | null + page_url: string | null + referrer: string | null + device_type: string | null + browser: string | null + os: string | null + screen_resolution: string | null + session_id: string | null expires_at: string | null created_at: string | null updated_at: string | null diff --git a/backend-compliance/compliance/api/banner_routes.py b/backend-compliance/compliance/api/banner_routes.py index b67365a..dad42e1 100644 --- a/backend-compliance/compliance/api/banner_routes.py +++ b/backend-compliance/compliance/api/banner_routes.py @@ -77,6 +77,15 @@ async def record_consent( ip_address=body.ip_address, user_agent=body.user_agent, consent_string=body.consent_string, + consent_method=body.consent_method, + page_url=body.page_url, + referrer=body.referrer, + device_type=body.device_type, + browser=body.browser, + os=body.os, + screen_resolution=body.screen_resolution, + session_id=body.session_id, + consent_scope=body.consent_scope, ) @@ -206,7 +215,7 @@ async def get_site_stats( @router.get("/admin/consents") async def list_banner_consents( - site_id: str | None = None, + site_id: Optional[str] = None, limit: int = 50, offset: int = 0, tenant_id: str = Depends(_get_tenant), diff --git a/backend-compliance/compliance/db/banner_models.py b/backend-compliance/compliance/db/banner_models.py index 9185214..58bd6c6 100644 --- a/backend-compliance/compliance/db/banner_models.py +++ b/backend-compliance/compliance/db/banner_models.py @@ -35,6 +35,20 @@ class BannerConsentDB(Base): user_agent = Column(Text) consent_string = Column(Text) linked_email = Column(Text) + # Vendor-agnostische Felder (Migration 107) + consent_method = Column(Text) # accept_all / reject_all / custom_selection + banner_version = Column(Integer) + banner_config_hash = Column(Text) + geo_country = Column(Text) + geo_region = Column(Text) + consent_scope = Column(Text, default='domain') + page_url = Column(Text) + referrer = Column(Text) + device_type = Column(Text) # mobile / desktop / tablet + browser = Column(Text) + os = Column(Text) + screen_resolution = Column(Text) + session_id = Column(Text) expires_at = Column(DateTime) created_at = Column(DateTime, nullable=False, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) @@ -63,6 +77,8 @@ class BannerConsentAuditLogDB(Base): ip_hash = Column(Text) banner_config_hash = Column(Text) consent_version = Column(Integer) + consent_method = Column(Text) + page_url = Column(Text) created_at = Column(DateTime, nullable=False, default=datetime.utcnow) __table_args__ = ( diff --git a/backend-compliance/compliance/schemas/banner.py b/backend-compliance/compliance/schemas/banner.py index 17d0063..bfb38ac 100644 --- a/backend-compliance/compliance/schemas/banner.py +++ b/backend-compliance/compliance/schemas/banner.py @@ -19,6 +19,16 @@ class ConsentCreate(BaseModel): ip_address: Optional[str] = None user_agent: Optional[str] = None consent_string: Optional[str] = None + # Vendor-agnostische Felder (Migration 107) + consent_method: Optional[str] = None + page_url: Optional[str] = None + referrer: Optional[str] = None + device_type: Optional[str] = None + browser: Optional[str] = None + os: Optional[str] = None + screen_resolution: Optional[str] = None + session_id: Optional[str] = None + consent_scope: Optional[str] = None class SiteConfigCreate(BaseModel): diff --git a/backend-compliance/compliance/services/_banner_serializers.py b/backend-compliance/compliance/services/_banner_serializers.py index c0f8aa7..8e118aa 100644 --- a/backend-compliance/compliance/services/_banner_serializers.py +++ b/backend-compliance/compliance/services/_banner_serializers.py @@ -24,8 +24,22 @@ def consent_to_dict(c: BannerConsentDB) -> dict[str, Any]: "categories": c.categories or [], "vendors": c.vendors or [], "ip_hash": c.ip_hash, + "user_agent": c.user_agent, "consent_string": c.consent_string, "linked_email": c.linked_email, + "consent_method": c.consent_method, + "banner_version": c.banner_version, + "banner_config_hash": c.banner_config_hash, + "geo_country": c.geo_country, + "geo_region": c.geo_region, + "consent_scope": c.consent_scope, + "page_url": c.page_url, + "referrer": c.referrer, + "device_type": c.device_type, + "browser": c.browser, + "os": c.os, + "screen_resolution": c.screen_resolution, + "session_id": c.session_id, "expires_at": c.expires_at.isoformat() if c.expires_at else None, "created_at": c.created_at.isoformat() if c.created_at else None, "updated_at": c.updated_at.isoformat() if c.updated_at else None, diff --git a/backend-compliance/compliance/services/banner_consent_service.py b/backend-compliance/compliance/services/banner_consent_service.py index f6f3fc3..cc19551 100644 --- a/backend-compliance/compliance/services/banner_consent_service.py +++ b/backend-compliance/compliance/services/banner_consent_service.py @@ -73,6 +73,9 @@ class BannerConsentService: ip_hash: Optional[str] = None, banner_config_hash: Optional[str] = None, consent_version: Optional[int] = None, + *, + consent_method: Optional[str] = None, + page_url: Optional[str] = None, ) -> None: entry = BannerConsentAuditLogDB( tenant_id=tenant_id, @@ -84,6 +87,8 @@ class BannerConsentService: ip_hash=ip_hash, banner_config_hash=banner_config_hash, consent_version=consent_version, + consent_method=consent_method, + page_url=page_url, ) self.db.add(entry) @@ -143,6 +148,16 @@ class BannerConsentService: ip_address: Optional[str], user_agent: Optional[str], consent_string: Optional[str], + *, + consent_method: Optional[str] = None, + page_url: Optional[str] = None, + referrer: Optional[str] = None, + device_type: Optional[str] = None, + browser: Optional[str] = None, + os: Optional[str] = None, + screen_resolution: Optional[str] = None, + session_id: Optional[str] = None, + consent_scope: Optional[str] = None, ) -> dict[str, Any]: """Upsert a device consent row for (tenant, site, device_fingerprint). @@ -158,6 +173,21 @@ class BannerConsentService: expires_at = now + timedelta(days=retention) config_hash, config_ver = self._compute_config_hash(tid, site_id) + # Vendor-agnostische Zusatzfelder + extra = { + "consent_method": consent_method, + "banner_version": config_ver, + "banner_config_hash": config_hash, + "page_url": page_url, + "referrer": referrer, + "device_type": device_type, + "browser": browser, + "os": os, + "screen_resolution": screen_resolution, + "session_id": session_id, + "consent_scope": consent_scope or "domain", + } + existing = ( self.db.query(BannerConsentDB) .filter( @@ -176,10 +206,13 @@ class BannerConsentService: existing.consent_string = consent_string existing.expires_at = expires_at existing.updated_at = now + for key, val in extra.items(): + setattr(existing, key, val) self.db.flush() self._log( tid, existing.id, "consent_updated", site_id, device_fingerprint, categories, ip_hash, config_hash, config_ver, + consent_method=consent_method, page_url=page_url, ) self.db.commit() self.db.refresh(existing) @@ -195,12 +228,14 @@ class BannerConsentService: user_agent=user_agent, consent_string=consent_string, expires_at=expires_at, + **extra, ) self.db.add(consent) self.db.flush() self._log( tid, consent.id, "consent_given", site_id, device_fingerprint, categories, ip_hash, config_hash, config_ver, + consent_method=consent_method, page_url=page_url, ) self.db.commit() self.db.refresh(consent) @@ -371,7 +406,7 @@ class BannerConsentService: } def list_consents( - self, tenant_id: str, site_id: str | None = None, + self, tenant_id: str, site_id: Optional[str] = None, limit: int = 50, offset: int = 0, ) -> dict[str, Any]: """List paginated banner consents with parsed categories.""" @@ -406,6 +441,19 @@ class BannerConsentService: "user_agent": c.user_agent, "linked_email": c.linked_email, "consent_string": c.consent_string, + "consent_method": c.consent_method, + "banner_version": c.banner_version, + "banner_config_hash": c.banner_config_hash, + "geo_country": c.geo_country, + "geo_region": c.geo_region, + "consent_scope": c.consent_scope, + "page_url": c.page_url, + "referrer": c.referrer, + "device_type": c.device_type, + "browser": c.browser, + "os": c.os, + "screen_resolution": c.screen_resolution, + "session_id": c.session_id, "expires_at": c.expires_at.isoformat() if c.expires_at else None, "created_at": c.created_at.isoformat() if c.created_at else None, "updated_at": c.updated_at.isoformat() if c.updated_at else None, diff --git a/backend-compliance/migrations/107_banner_vendor_agnostic.sql b/backend-compliance/migrations/107_banner_vendor_agnostic.sql new file mode 100644 index 0000000..8d5ee73 --- /dev/null +++ b/backend-compliance/migrations/107_banner_vendor_agnostic.sql @@ -0,0 +1,69 @@ +-- Migration 107: Vendor-agnostische Felder fuer Banner-Consents +-- Erweitert compliance_banner_consents um Felder die fuer beliebige +-- Tracking-Anbieter (GA4, Matomo, HubSpot etc.) benoetigt werden. +-- Alle Felder nullable → backward-compatible. + +-- Consent-Methode: wie hat der User entschieden +ALTER TABLE compliance_banner_consents + ADD COLUMN IF NOT EXISTS consent_method TEXT; + -- "accept_all" / "reject_all" / "custom_selection" + +-- Banner-Version + Config-Hash zum Zeitpunkt des Consents (Art. 7(1) DSGVO) +ALTER TABLE compliance_banner_consents + ADD COLUMN IF NOT EXISTS banner_version INTEGER; + +ALTER TABLE compliance_banner_consents + ADD COLUMN IF NOT EXISTS banner_config_hash TEXT; + +-- Geo-Daten fuer EWR-Only-Logik +ALTER TABLE compliance_banner_consents + ADD COLUMN IF NOT EXISTS geo_country TEXT; + +ALTER TABLE compliance_banner_consents + ADD COLUMN IF NOT EXISTS geo_region TEXT; + +-- Geltungsbereich: "page" / "domain" / "cross-domain" +ALTER TABLE compliance_banner_consents + ADD COLUMN IF NOT EXISTS consent_scope TEXT DEFAULT 'domain'; + +-- Tracking-Kontext +ALTER TABLE compliance_banner_consents + ADD COLUMN IF NOT EXISTS page_url TEXT; + +ALTER TABLE compliance_banner_consents + ADD COLUMN IF NOT EXISTS referrer TEXT; + +-- Device-Informationen (parsed aus User-Agent im Frontend) +ALTER TABLE compliance_banner_consents + ADD COLUMN IF NOT EXISTS device_type TEXT; + -- "mobile" / "desktop" / "tablet" + +ALTER TABLE compliance_banner_consents + ADD COLUMN IF NOT EXISTS browser TEXT; + +ALTER TABLE compliance_banner_consents + ADD COLUMN IF NOT EXISTS os TEXT; + +ALTER TABLE compliance_banner_consents + ADD COLUMN IF NOT EXISTS screen_resolution TEXT; + +-- Session-basierte Zuordnung +ALTER TABLE compliance_banner_consents + ADD COLUMN IF NOT EXISTS session_id TEXT; + +-- Audit-Log ebenfalls erweitern (consent_method fuer Nachweis) +ALTER TABLE compliance_banner_consent_audit_log + ADD COLUMN IF NOT EXISTS consent_method TEXT; + +ALTER TABLE compliance_banner_consent_audit_log + ADD COLUMN IF NOT EXISTS page_url TEXT; + +-- Index auf consent_method fuer Reporting +CREATE INDEX IF NOT EXISTS idx_banner_consent_method + ON compliance_banner_consents (consent_method) + WHERE consent_method IS NOT NULL; + +-- Index auf session_id fuer Session-Lookup +CREATE INDEX IF NOT EXISTS idx_banner_consent_session + ON compliance_banner_consents (session_id) + WHERE session_id IS NOT NULL;