feat: IAB TCF 2.2 — TC String encoder + purpose mapping + UI

- TCFEncoderService: generates base64url-encoded TC Strings per IAB spec
  with 12 purposes, vendor consent bitfield, CMP metadata
- Category-to-purpose mapping (necessary→none, statistics→1,7,8,9,10,
  marketing→1,2,3,4,5,6,7,12, functional→1,11)
- tcf_routes: 5 endpoints (purposes, features, mapping, encode, encode-categories)
- banner_consent_service: auto-generates TC String when tcf_enabled=true
- TCFSettings.tsx: enable/disable toggle, purpose grid with category mapping,
  TC String test generator, CMP registration info
- New "TCF/IAB" tab in cookie-banner page (7 tabs total)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-05-04 07:01:37 +02:00
parent c89a68e59e
commit d3c8811fdb
6 changed files with 505 additions and 1 deletions
@@ -133,6 +133,24 @@ class BannerConsentService:
return max(v.retention_days for v in vendors if v.retention_days)
return max((CATEGORY_RETENTION_DAYS.get(c, 365) for c in categories), default=365)
def _maybe_generate_tc_string(
self, tenant_id: uuid.UUID, site_id: str, categories: list[str],
) -> Optional[str]:
"""Generate TC String if TCF is enabled for this site."""
config = (
self.db.query(BannerSiteConfigDB)
.filter(BannerSiteConfigDB.tenant_id == tenant_id, BannerSiteConfigDB.site_id == site_id)
.first()
)
if not config or not config.tcf_enabled:
return None
try:
from compliance.services.tcf_encoder_service import TCFEncoderService
encoder = TCFEncoderService()
return encoder.encode_from_categories(categories)
except Exception:
return None
# ------------------------------------------------------------------
# Consent CRUD (public SDK)
# ------------------------------------------------------------------
@@ -163,6 +181,10 @@ class BannerConsentService:
expires_at = now + timedelta(days=retention)
config_hash, config_ver = self._compute_config_hash(tid, site_id)
# Auto-generate TC String if TCF is enabled for this site
if not consent_string:
consent_string = self._maybe_generate_tc_string(tid, site_id, categories)
existing = (
self.db.query(BannerConsentDB)
.filter(