Files
breakpilot-compliance/backend-compliance/compliance/api/tcf_routes.py
T
Benjamin Admin d3c8811fdb 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>
2026-05-04 07:01:37 +02:00

96 lines
2.9 KiB
Python

"""
FastAPI routes for IAB TCF 2.2 (Transparency & Consent Framework).
Endpoints:
GET /tcf/purposes — list 12 IAB purposes with translations
GET /tcf/special-features — list 2 IAB special features
GET /tcf/category-mapping — banner category → IAB purpose mapping
POST /tcf/encode — generate TC String from consent decisions
POST /tcf/encode-categories — generate TC String from banner categories
"""
import logging
from typing import Optional, List, Dict
from fastapi import APIRouter, Depends
from pydantic import BaseModel
from sqlalchemy.orm import Session
from classroom_engine.database import get_db
from .tenant_utils import get_tenant_id as _get_tenant_id
from compliance.services.tcf_encoder_service import TCFEncoderService
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/tcf", tags=["tcf"])
class TCFEncodeRequest(BaseModel):
purpose_consents: Dict[int, bool] = {}
vendor_consents: Dict[int, bool] = {}
purpose_li: Optional[Dict[int, bool]] = None
special_features: Optional[Dict[int, bool]] = None
cmp_id: int = 1
cmp_version: int = 1
consent_language: str = "DE"
class TCFCategoryEncodeRequest(BaseModel):
categories: List[str] = []
vendor_consents: Optional[Dict[int, bool]] = None
cmp_id: int = 1
consent_language: str = "DE"
@router.get("/purposes")
def list_purposes():
return TCFEncoderService.get_purposes()
@router.get("/special-features")
def list_special_features():
return TCFEncoderService.get_special_features()
@router.get("/category-mapping")
def get_category_mapping():
return TCFEncoderService.get_category_purpose_map()
@router.post("/encode")
def encode_tc_string(body: TCFEncodeRequest):
encoder = TCFEncoderService(
cmp_id=body.cmp_id,
cmp_version=body.cmp_version,
consent_language=body.consent_language,
)
tc_string = encoder.encode(
purpose_consents=body.purpose_consents,
vendor_consents=body.vendor_consents,
purpose_li=body.purpose_li,
special_features=body.special_features,
)
return {"tc_string": tc_string, "version": 2}
@router.post("/encode-categories")
def encode_from_categories(body: TCFCategoryEncodeRequest):
encoder = TCFEncoderService(
cmp_id=body.cmp_id,
consent_language=body.consent_language,
)
tc_string = encoder.encode_from_categories(
categories=body.categories,
vendor_consents=body.vendor_consents,
)
# Also return which purposes were set
from compliance.services.tcf_encoder_service import CATEGORY_PURPOSE_MAP
purpose_ids = set()
for cat in body.categories:
purpose_ids.update(CATEGORY_PURPOSE_MAP.get(cat, []))
return {
"tc_string": tc_string,
"version": 2,
"purposes_consented": sorted(purpose_ids),
"categories": body.categories,
}