feat: Cookie-Banner ↔ Backend Integration (DSR, Retention, Consent Proof)

Phase 1: Vendor sync from service registry (82+ services → banner vendors)
Phase 2: Category-based retention (marketing=90d, statistics=790d, not hardcoded 365d)
Phase 3: DSR ↔ Banner email linking (link-email, by-email, Art.17 erasure, Art.15/20 export)
Phase 4: Consent sync (Banner → Einwilligungen bridge)
Phase 6: Consent proof (SHA256 config hash + config_version in audit log, Art. 7(1) DSGVO)

New files:
- banner_dsr_service.py — email linking + DSR integration
- vendor_banner_sync.py — service registry → vendor configs
- migration 106 — linked_email, banner_config_hash, consent_version columns

Tests: 20+ new backend tests + 2 Playwright E2E test suites (API + UI)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-05-02 19:41:22 +02:00
parent c3f8e19e92
commit 44acd68c96
12 changed files with 1522 additions and 5 deletions
@@ -20,12 +20,16 @@ from compliance.api._http_errors import translate_domain_errors
from compliance.schemas.banner import (
CategoryConfigCreate,
ConsentCreate,
ConsentSyncRequest,
LinkEmailRequest,
SiteConfigCreate,
SiteConfigUpdate,
VendorConfigCreate,
)
from compliance.services.banner_admin_service import BannerAdminService
from compliance.services.banner_consent_service import BannerConsentService
from compliance.services.banner_dsr_service import BannerDSRService
from compliance.services.vendor_banner_sync import get_banner_vendors_from_registry
router = APIRouter(prefix="/banner", tags=["compliance-banner"])
@@ -48,6 +52,10 @@ def get_admin_service(db: Session = Depends(get_db)) -> BannerAdminService:
return BannerAdminService(db)
def get_dsr_service(db: Session = Depends(get_db)) -> BannerDSRService:
return BannerDSRService(db)
# =============================================================================
# Public SDK Endpoints (fuer Einbettung in Kunden-Websites)
# =============================================================================
@@ -118,6 +126,69 @@ async def export_consent(
return service.export_consent(tenant_id, site_id, device_fingerprint)
# =============================================================================
# DSR Integration — Email Linking + Consent Sync (Phase 3 + 4)
# =============================================================================
@router.post("/consent/link-email")
async def link_email(
body: LinkEmailRequest,
tenant_id: str = Depends(_get_tenant),
service: BannerDSRService = Depends(get_dsr_service),
) -> dict[str, Any]:
"""Link an email to a device fingerprint (e.g. after signup/login)."""
with translate_domain_errors():
return service.link_email(
tenant_id, body.site_id, body.device_fingerprint, body.email,
)
@router.get("/consent/by-email/{email}")
async def get_consents_by_email(
email: str,
tenant_id: str = Depends(_get_tenant),
service: BannerDSRService = Depends(get_dsr_service),
) -> list[dict[str, Any]]:
"""Find all banner consents linked to an email (Art. 15 DSGVO)."""
with translate_domain_errors():
return service.get_consents_by_email(tenant_id, email)
@router.delete("/consent/by-email/{email}")
async def delete_consents_by_email(
email: str,
tenant_id: str = Depends(_get_tenant),
service: BannerDSRService = Depends(get_dsr_service),
) -> dict[str, Any]:
"""Delete all banner consents for an email (Art. 17 DSGVO erasure)."""
with translate_domain_errors():
return service.delete_consents_by_email(tenant_id, email)
@router.get("/consent/dsr-export/{email}")
async def export_for_dsr(
email: str,
tenant_id: str = Depends(_get_tenant),
service: BannerDSRService = Depends(get_dsr_service),
) -> dict[str, Any]:
"""Export all banner consent data for DSR (Art. 15/20 DSGVO)."""
with translate_domain_errors():
return service.export_for_dsr(tenant_id, email)
@router.post("/consent/sync")
async def sync_consent(
body: ConsentSyncRequest,
tenant_id: str = Depends(_get_tenant),
service: BannerDSRService = Depends(get_dsr_service),
) -> dict[str, Any]:
"""Sync banner consent to Einwilligungen (Phase 4 — user-based bridge)."""
with translate_domain_errors():
return service.sync_consent_to_einwilligungen(
tenant_id, body.device_fingerprint, body.email, body.site_id,
)
# =============================================================================
# Admin — Stats
# =============================================================================
@@ -253,3 +324,43 @@ async def delete_vendor(
"""Delete a vendor."""
with translate_domain_errors():
service.delete_vendor(vendor_id)
# =============================================================================
# Admin — Vendor Sync from Service Registry (Phase 1)
# =============================================================================
@router.post("/admin/sites/{site_id}/sync-vendors")
async def sync_vendors_from_registry(
site_id: str,
tenant_id: str = Depends(_get_tenant),
service: BannerAdminService = Depends(get_admin_service),
) -> dict[str, Any]:
"""Sync 82+ services from service registry to banner vendor configs."""
with translate_domain_errors():
vendors = get_banner_vendors_from_registry()
created = 0
updated = 0
for v in vendors:
try:
existing = service.list_vendors(tenant_id, site_id)
match = next(
(e for e in existing if e["vendor_name"] == v["vendor_name"]),
None,
)
if match:
updated += 1
else:
from compliance.schemas.banner import VendorConfigCreate
service.create_vendor(tenant_id, site_id, VendorConfigCreate(
vendor_name=v["vendor_name"],
category_key=v["category_key"],
description_de=v["description_de"],
description_en=v["description_en"],
cookie_names=v["cookie_names"],
retention_days=v["retention_days"],
))
created += 1
except Exception:
continue
return {"created": created, "updated": updated, "total": len(vendors)}