feat(cmp): vendor-agnostic consent data model — 13 new fields
Build + Deploy / build-admin-compliance (push) Successful in 2m28s
Build + Deploy / build-backend-compliance (push) Successful in 3m48s
Build + Deploy / build-ai-sdk (push) Failing after 45s
Build + Deploy / build-developer-portal (push) Successful in 1m28s
Build + Deploy / build-tts (push) Successful in 1m48s
Build + Deploy / build-document-crawler (push) Successful in 48s
Build + Deploy / build-dsms-gateway (push) Successful in 34s
Build + Deploy / build-dsms-node (push) Successful in 20s
CI / branch-name (push) Has been skipped
Build + Deploy / trigger-orca (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 24s
CI / secret-scan (push) Has been skipped
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 3m1s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Failing after 49s
CI / test-python-backend (push) Successful in 45s
CI / test-python-document-crawler (push) Successful in 31s
CI / test-python-dsms-gateway (push) Successful in 27s
CI / validate-canonical-controls (push) Successful in 18s

Extend banner consent records with consent_method, banner_version,
banner_config_hash, geo, page_url, referrer, device info, session_id
and consent_scope for full Art. 7 DSGVO proof with any tracking vendor.

Migration 107, backward-compatible (all fields nullable).
Admin detail modal shows tracking context, device info and technical data.
Fix pre-existing str|None → Optional[str] for Python 3.9 compat.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-05-10 23:12:20 +02:00
parent dabc2358ab
commit 289ec5f396
8 changed files with 241 additions and 16 deletions
@@ -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),
@@ -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__ = (
@@ -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):
@@ -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,
@@ -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,
@@ -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;