""" Banner Consent Routes — Device-basierte Cookie-Consents fuer Kunden-Websites. Public SDK-Endpoints (fuer Einbettung) + Admin-Endpoints (Konfiguration & Stats). Phase 1 Step 4 refactor: handlers are thin and delegate to ``BannerConsentService`` (SDK surface) or ``BannerAdminService`` (admin CRUD). Domain errors raised by the services are translated to HTTPException via ``translate_domain_errors``. Pydantic request schemas live in ``compliance.schemas.banner``. """ from typing import Any, Optional from fastapi import APIRouter, Depends, Header, Query from sqlalchemy.orm import Session from classroom_engine.database import get_db from compliance.api._http_errors import translate_domain_errors from compliance.schemas.banner import ( CategoryConfigCreate, ConsentCreate, SiteConfigCreate, SiteConfigUpdate, VendorConfigCreate, ) from compliance.services.banner_admin_service import BannerAdminService from compliance.services.banner_consent_service import BannerConsentService router = APIRouter(prefix="/banner", tags=["compliance-banner"]) DEFAULT_TENANT = "9282a473-5c95-4b3a-bf78-0ecc0ec71d3e" # ---------------------------------------------------------------------- # Dependencies # ---------------------------------------------------------------------- def _get_tenant(x_tenant_id: Optional[str] = Header(None, alias="X-Tenant-ID")) -> str: return x_tenant_id or DEFAULT_TENANT def get_consent_service(db: Session = Depends(get_db)) -> BannerConsentService: return BannerConsentService(db) def get_admin_service(db: Session = Depends(get_db)) -> BannerAdminService: return BannerAdminService(db) # ============================================================================= # Public SDK Endpoints (fuer Einbettung in Kunden-Websites) # ============================================================================= @router.post("/consent") async def record_consent( body: ConsentCreate, tenant_id: str = Depends(_get_tenant), service: BannerConsentService = Depends(get_consent_service), ) -> dict[str, Any]: """Record device consent (upsert by site_id + device_fingerprint).""" with translate_domain_errors(): return service.record_consent( tenant_id=tenant_id, site_id=body.site_id, device_fingerprint=body.device_fingerprint, categories=body.categories, vendors=body.vendors, ip_address=body.ip_address, user_agent=body.user_agent, consent_string=body.consent_string, ) @router.get("/consent") async def get_consent( site_id: str = Query(...), device_fingerprint: str = Query(...), tenant_id: str = Depends(_get_tenant), service: BannerConsentService = Depends(get_consent_service), ) -> dict[str, Any]: """Retrieve consent for a device.""" with translate_domain_errors(): return service.get_consent(tenant_id, site_id, device_fingerprint) @router.delete("/consent/{consent_id}") async def withdraw_consent( consent_id: str, tenant_id: str = Depends(_get_tenant), service: BannerConsentService = Depends(get_consent_service), ) -> dict[str, Any]: """Withdraw a banner consent.""" with translate_domain_errors(): return service.withdraw_consent(tenant_id, consent_id) @router.get("/config/{site_id}") async def get_site_config( site_id: str, tenant_id: str = Depends(_get_tenant), service: BannerConsentService = Depends(get_consent_service), ) -> dict[str, Any]: """Load site configuration for banner display.""" with translate_domain_errors(): return service.get_site_config(tenant_id, site_id) @router.get("/consent/export") async def export_consent( site_id: str = Query(...), device_fingerprint: str = Query(...), tenant_id: str = Depends(_get_tenant), service: BannerConsentService = Depends(get_consent_service), ) -> dict[str, Any]: """DSGVO export of all consent data for a device.""" with translate_domain_errors(): return service.export_consent(tenant_id, site_id, device_fingerprint) # ============================================================================= # Admin — Stats # ============================================================================= @router.get("/admin/stats/{site_id}") async def get_site_stats( site_id: str, tenant_id: str = Depends(_get_tenant), service: BannerConsentService = Depends(get_consent_service), ) -> dict[str, Any]: """Consent statistics per site.""" with translate_domain_errors(): return service.get_site_stats(tenant_id, site_id) # ============================================================================= # Admin — Sites # ============================================================================= @router.get("/admin/sites") async def list_site_configs( tenant_id: str = Depends(_get_tenant), service: BannerAdminService = Depends(get_admin_service), ) -> list[dict[str, Any]]: """List all site configurations.""" with translate_domain_errors(): return service.list_sites(tenant_id) @router.post("/admin/sites") async def create_site_config( body: SiteConfigCreate, tenant_id: str = Depends(_get_tenant), service: BannerAdminService = Depends(get_admin_service), ) -> dict[str, Any]: """Create a site configuration.""" with translate_domain_errors(): return service.create_site(tenant_id, body) @router.put("/admin/sites/{site_id}") async def update_site_config( site_id: str, body: SiteConfigUpdate, tenant_id: str = Depends(_get_tenant), service: BannerAdminService = Depends(get_admin_service), ) -> dict[str, Any]: """Update a site configuration.""" with translate_domain_errors(): return service.update_site(tenant_id, site_id, body) @router.delete("/admin/sites/{site_id}", status_code=204) async def delete_site_config( site_id: str, tenant_id: str = Depends(_get_tenant), service: BannerAdminService = Depends(get_admin_service), ) -> None: """Delete a site configuration.""" with translate_domain_errors(): service.delete_site(tenant_id, site_id) # ============================================================================= # Admin — Categories # ============================================================================= @router.get("/admin/sites/{site_id}/categories") async def list_categories( site_id: str, tenant_id: str = Depends(_get_tenant), service: BannerAdminService = Depends(get_admin_service), ) -> list[dict[str, Any]]: """List categories for a site.""" with translate_domain_errors(): return service.list_categories(tenant_id, site_id) @router.post("/admin/sites/{site_id}/categories") async def create_category( site_id: str, body: CategoryConfigCreate, tenant_id: str = Depends(_get_tenant), service: BannerAdminService = Depends(get_admin_service), ) -> dict[str, Any]: """Create a category for a site.""" with translate_domain_errors(): return service.create_category(tenant_id, site_id, body) @router.delete("/admin/categories/{category_id}", status_code=204) async def delete_category( category_id: str, service: BannerAdminService = Depends(get_admin_service), ) -> None: """Delete a category.""" with translate_domain_errors(): service.delete_category(category_id) # ============================================================================= # Admin — Vendors # ============================================================================= @router.get("/admin/sites/{site_id}/vendors") async def list_vendors( site_id: str, tenant_id: str = Depends(_get_tenant), service: BannerAdminService = Depends(get_admin_service), ) -> list[dict[str, Any]]: """List vendors for a site.""" with translate_domain_errors(): return service.list_vendors(tenant_id, site_id) @router.post("/admin/sites/{site_id}/vendors") async def create_vendor( site_id: str, body: VendorConfigCreate, tenant_id: str = Depends(_get_tenant), service: BannerAdminService = Depends(get_admin_service), ) -> dict[str, Any]: """Create a vendor for a site.""" with translate_domain_errors(): return service.create_vendor(tenant_id, site_id, body) @router.delete("/admin/vendors/{vendor_id}", status_code=204) async def delete_vendor( vendor_id: str, service: BannerAdminService = Depends(get_admin_service), ) -> None: """Delete a vendor.""" with translate_domain_errors(): service.delete_vendor(vendor_id)