Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 30s
CI / test-python-backend-compliance (push) Successful in 30s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 17s
- Ruff: 144 auto-fixes (unused imports, == None → is None), F821/F811/F841 manuell - CVEs: python-multipart>=0.0.22, weasyprint>=68.0, pillow>=12.1.1, npm audit fix (0 vulns) - TS: 5 tote Drafting-Engine-Dateien entfernt, allowed-facts/sanitizer/StepHeader/context fixes - Tests: +104 (ISMS 58, Evidence 18, VVT 14, Generation 14) → 1449 passed - Refactoring: collect_ci_evidence (F→A), row_to_response (E→A), extract_requirements (E→A) - Dead Code: pca-platform, 7 Go-Handler, dsr_api.py, duplicate Schemas entfernt Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
654 lines
20 KiB
Python
654 lines
20 KiB
Python
"""
|
|
Banner Consent Routes — Device-basierte Cookie-Consents fuer Kunden-Websites.
|
|
|
|
Public SDK-Endpoints (fuer Einbettung) + Admin-Endpoints (Konfiguration & Stats).
|
|
"""
|
|
|
|
import uuid
|
|
import hashlib
|
|
from datetime import datetime, timedelta
|
|
from typing import Optional, List
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, Header
|
|
from pydantic import BaseModel
|
|
from sqlalchemy.orm import Session
|
|
|
|
from classroom_engine.database import get_db
|
|
from ..db.banner_models import (
|
|
BannerConsentDB, BannerConsentAuditLogDB,
|
|
BannerSiteConfigDB, BannerCategoryConfigDB, BannerVendorConfigDB,
|
|
)
|
|
|
|
router = APIRouter(prefix="/banner", tags=["compliance-banner"])
|
|
|
|
DEFAULT_TENANT = "9282a473-5c95-4b3a-bf78-0ecc0ec71d3e"
|
|
|
|
|
|
# =============================================================================
|
|
# Schemas
|
|
# =============================================================================
|
|
|
|
class ConsentCreate(BaseModel):
|
|
site_id: str
|
|
device_fingerprint: str
|
|
categories: List[str] = []
|
|
vendors: List[str] = []
|
|
ip_address: Optional[str] = None
|
|
user_agent: Optional[str] = None
|
|
consent_string: Optional[str] = None
|
|
|
|
|
|
class SiteConfigCreate(BaseModel):
|
|
site_id: str
|
|
site_name: Optional[str] = None
|
|
site_url: Optional[str] = None
|
|
banner_title: Optional[str] = None
|
|
banner_description: Optional[str] = None
|
|
privacy_url: Optional[str] = None
|
|
imprint_url: Optional[str] = None
|
|
dsb_name: Optional[str] = None
|
|
dsb_email: Optional[str] = None
|
|
theme: Optional[dict] = None
|
|
tcf_enabled: bool = False
|
|
|
|
|
|
class SiteConfigUpdate(BaseModel):
|
|
site_name: Optional[str] = None
|
|
site_url: Optional[str] = None
|
|
banner_title: Optional[str] = None
|
|
banner_description: Optional[str] = None
|
|
privacy_url: Optional[str] = None
|
|
imprint_url: Optional[str] = None
|
|
dsb_name: Optional[str] = None
|
|
dsb_email: Optional[str] = None
|
|
theme: Optional[dict] = None
|
|
tcf_enabled: Optional[bool] = None
|
|
is_active: Optional[bool] = None
|
|
|
|
|
|
class CategoryConfigCreate(BaseModel):
|
|
category_key: str
|
|
name_de: str
|
|
name_en: Optional[str] = None
|
|
description_de: Optional[str] = None
|
|
description_en: Optional[str] = None
|
|
is_required: bool = False
|
|
sort_order: int = 0
|
|
|
|
|
|
class VendorConfigCreate(BaseModel):
|
|
vendor_name: str
|
|
vendor_url: Optional[str] = None
|
|
category_key: str
|
|
description_de: Optional[str] = None
|
|
description_en: Optional[str] = None
|
|
cookie_names: List[str] = []
|
|
retention_days: int = 365
|
|
|
|
|
|
# =============================================================================
|
|
# Helpers
|
|
# =============================================================================
|
|
|
|
def _get_tenant(x_tenant_id: Optional[str] = Header(None, alias='X-Tenant-ID')) -> str:
|
|
return x_tenant_id or DEFAULT_TENANT
|
|
|
|
|
|
def _hash_ip(ip: Optional[str]) -> Optional[str]:
|
|
if not ip:
|
|
return None
|
|
return hashlib.sha256(ip.encode()).hexdigest()[:16]
|
|
|
|
|
|
def _consent_to_dict(c: BannerConsentDB) -> dict:
|
|
return {
|
|
"id": str(c.id),
|
|
"site_id": c.site_id,
|
|
"device_fingerprint": c.device_fingerprint,
|
|
"categories": c.categories or [],
|
|
"vendors": c.vendors or [],
|
|
"ip_hash": c.ip_hash,
|
|
"consent_string": c.consent_string,
|
|
"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,
|
|
}
|
|
|
|
|
|
def _site_config_to_dict(s: BannerSiteConfigDB) -> dict:
|
|
return {
|
|
"id": str(s.id),
|
|
"site_id": s.site_id,
|
|
"site_name": s.site_name,
|
|
"site_url": s.site_url,
|
|
"banner_title": s.banner_title,
|
|
"banner_description": s.banner_description,
|
|
"privacy_url": s.privacy_url,
|
|
"imprint_url": s.imprint_url,
|
|
"dsb_name": s.dsb_name,
|
|
"dsb_email": s.dsb_email,
|
|
"theme": s.theme or {},
|
|
"tcf_enabled": s.tcf_enabled,
|
|
"is_active": s.is_active,
|
|
"created_at": s.created_at.isoformat() if s.created_at else None,
|
|
"updated_at": s.updated_at.isoformat() if s.updated_at else None,
|
|
}
|
|
|
|
|
|
def _category_to_dict(c: BannerCategoryConfigDB) -> dict:
|
|
return {
|
|
"id": str(c.id),
|
|
"site_config_id": str(c.site_config_id),
|
|
"category_key": c.category_key,
|
|
"name_de": c.name_de,
|
|
"name_en": c.name_en,
|
|
"description_de": c.description_de,
|
|
"description_en": c.description_en,
|
|
"is_required": c.is_required,
|
|
"sort_order": c.sort_order,
|
|
"is_active": c.is_active,
|
|
}
|
|
|
|
|
|
def _vendor_to_dict(v: BannerVendorConfigDB) -> dict:
|
|
return {
|
|
"id": str(v.id),
|
|
"site_config_id": str(v.site_config_id),
|
|
"vendor_name": v.vendor_name,
|
|
"vendor_url": v.vendor_url,
|
|
"category_key": v.category_key,
|
|
"description_de": v.description_de,
|
|
"description_en": v.description_en,
|
|
"cookie_names": v.cookie_names or [],
|
|
"retention_days": v.retention_days,
|
|
"is_active": v.is_active,
|
|
}
|
|
|
|
|
|
def _log_banner_audit(db, tenant_id, consent_id, action, site_id, device_fingerprint=None, categories=None, ip_hash=None):
|
|
entry = BannerConsentAuditLogDB(
|
|
tenant_id=tenant_id,
|
|
consent_id=consent_id,
|
|
action=action,
|
|
site_id=site_id,
|
|
device_fingerprint=device_fingerprint,
|
|
categories=categories or [],
|
|
ip_hash=ip_hash,
|
|
)
|
|
db.add(entry)
|
|
return entry
|
|
|
|
|
|
# =============================================================================
|
|
# Public SDK Endpoints (fuer Einbettung in Kunden-Websites)
|
|
# =============================================================================
|
|
|
|
@router.post("/consent")
|
|
async def record_consent(
|
|
body: ConsentCreate,
|
|
tenant_id: str = Depends(_get_tenant),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""Record device consent (upsert by site_id + device_fingerprint)."""
|
|
tid = uuid.UUID(tenant_id)
|
|
ip_hash = _hash_ip(body.ip_address)
|
|
|
|
# Upsert: check existing
|
|
existing = db.query(BannerConsentDB).filter(
|
|
BannerConsentDB.tenant_id == tid,
|
|
BannerConsentDB.site_id == body.site_id,
|
|
BannerConsentDB.device_fingerprint == body.device_fingerprint,
|
|
).first()
|
|
|
|
if existing:
|
|
existing.categories = body.categories
|
|
existing.vendors = body.vendors
|
|
existing.ip_hash = ip_hash
|
|
existing.user_agent = body.user_agent
|
|
existing.consent_string = body.consent_string
|
|
existing.expires_at = datetime.utcnow() + timedelta(days=365)
|
|
existing.updated_at = datetime.utcnow()
|
|
db.flush()
|
|
|
|
_log_banner_audit(
|
|
db, tid, existing.id, "consent_updated",
|
|
body.site_id, body.device_fingerprint, body.categories, ip_hash,
|
|
)
|
|
db.commit()
|
|
db.refresh(existing)
|
|
return _consent_to_dict(existing)
|
|
|
|
consent = BannerConsentDB(
|
|
tenant_id=tid,
|
|
site_id=body.site_id,
|
|
device_fingerprint=body.device_fingerprint,
|
|
categories=body.categories,
|
|
vendors=body.vendors,
|
|
ip_hash=ip_hash,
|
|
user_agent=body.user_agent,
|
|
consent_string=body.consent_string,
|
|
expires_at=datetime.utcnow() + timedelta(days=365),
|
|
)
|
|
db.add(consent)
|
|
db.flush()
|
|
|
|
_log_banner_audit(
|
|
db, tid, consent.id, "consent_given",
|
|
body.site_id, body.device_fingerprint, body.categories, ip_hash,
|
|
)
|
|
db.commit()
|
|
db.refresh(consent)
|
|
return _consent_to_dict(consent)
|
|
|
|
|
|
@router.get("/consent")
|
|
async def get_consent(
|
|
site_id: str = Query(...),
|
|
device_fingerprint: str = Query(...),
|
|
tenant_id: str = Depends(_get_tenant),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""Retrieve consent for a device."""
|
|
tid = uuid.UUID(tenant_id)
|
|
consent = db.query(BannerConsentDB).filter(
|
|
BannerConsentDB.tenant_id == tid,
|
|
BannerConsentDB.site_id == site_id,
|
|
BannerConsentDB.device_fingerprint == device_fingerprint,
|
|
).first()
|
|
|
|
if not consent:
|
|
return {"has_consent": False, "consent": None}
|
|
|
|
return {"has_consent": True, "consent": _consent_to_dict(consent)}
|
|
|
|
|
|
@router.delete("/consent/{consent_id}")
|
|
async def withdraw_consent(
|
|
consent_id: str,
|
|
tenant_id: str = Depends(_get_tenant),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""Withdraw a banner consent."""
|
|
tid = uuid.UUID(tenant_id)
|
|
try:
|
|
cid = uuid.UUID(consent_id)
|
|
except ValueError:
|
|
raise HTTPException(status_code=400, detail="Invalid consent ID")
|
|
|
|
consent = db.query(BannerConsentDB).filter(
|
|
BannerConsentDB.id == cid,
|
|
BannerConsentDB.tenant_id == tid,
|
|
).first()
|
|
if not consent:
|
|
raise HTTPException(status_code=404, detail="Consent not found")
|
|
|
|
_log_banner_audit(
|
|
db, tid, cid, "consent_withdrawn",
|
|
consent.site_id, consent.device_fingerprint,
|
|
)
|
|
|
|
db.delete(consent)
|
|
db.commit()
|
|
return {"success": True, "message": "Consent withdrawn"}
|
|
|
|
|
|
@router.get("/config/{site_id}")
|
|
async def get_site_config(
|
|
site_id: str,
|
|
tenant_id: str = Depends(_get_tenant),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""Load site configuration for banner display."""
|
|
tid = uuid.UUID(tenant_id)
|
|
config = db.query(BannerSiteConfigDB).filter(
|
|
BannerSiteConfigDB.tenant_id == tid,
|
|
BannerSiteConfigDB.site_id == site_id,
|
|
).first()
|
|
|
|
if not config:
|
|
return {
|
|
"site_id": site_id,
|
|
"banner_title": "Cookie-Einstellungen",
|
|
"banner_description": "Wir verwenden Cookies, um Ihnen die bestmoegliche Erfahrung zu bieten.",
|
|
"categories": [],
|
|
"vendors": [],
|
|
}
|
|
|
|
categories = db.query(BannerCategoryConfigDB).filter(
|
|
BannerCategoryConfigDB.site_config_id == config.id,
|
|
BannerCategoryConfigDB.is_active,
|
|
).order_by(BannerCategoryConfigDB.sort_order).all()
|
|
|
|
vendors = db.query(BannerVendorConfigDB).filter(
|
|
BannerVendorConfigDB.site_config_id == config.id,
|
|
BannerVendorConfigDB.is_active,
|
|
).all()
|
|
|
|
result = _site_config_to_dict(config)
|
|
result["categories"] = [_category_to_dict(c) for c in categories]
|
|
result["vendors"] = [_vendor_to_dict(v) for v in vendors]
|
|
return result
|
|
|
|
|
|
@router.get("/consent/export")
|
|
async def export_consent(
|
|
site_id: str = Query(...),
|
|
device_fingerprint: str = Query(...),
|
|
tenant_id: str = Depends(_get_tenant),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""DSGVO export of all consent data for a device."""
|
|
tid = uuid.UUID(tenant_id)
|
|
|
|
consents = db.query(BannerConsentDB).filter(
|
|
BannerConsentDB.tenant_id == tid,
|
|
BannerConsentDB.site_id == site_id,
|
|
BannerConsentDB.device_fingerprint == device_fingerprint,
|
|
).all()
|
|
|
|
audit = db.query(BannerConsentAuditLogDB).filter(
|
|
BannerConsentAuditLogDB.tenant_id == tid,
|
|
BannerConsentAuditLogDB.site_id == site_id,
|
|
BannerConsentAuditLogDB.device_fingerprint == device_fingerprint,
|
|
).order_by(BannerConsentAuditLogDB.created_at.desc()).all()
|
|
|
|
return {
|
|
"device_fingerprint": device_fingerprint,
|
|
"site_id": site_id,
|
|
"consents": [_consent_to_dict(c) for c in consents],
|
|
"audit_trail": [
|
|
{
|
|
"id": str(a.id),
|
|
"action": a.action,
|
|
"categories": a.categories or [],
|
|
"created_at": a.created_at.isoformat() if a.created_at else None,
|
|
}
|
|
for a in audit
|
|
],
|
|
}
|
|
|
|
|
|
# =============================================================================
|
|
# Admin Endpoints
|
|
# =============================================================================
|
|
|
|
@router.get("/admin/stats/{site_id}")
|
|
async def get_site_stats(
|
|
site_id: str,
|
|
tenant_id: str = Depends(_get_tenant),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""Consent statistics per site."""
|
|
tid = uuid.UUID(tenant_id)
|
|
base = db.query(BannerConsentDB).filter(
|
|
BannerConsentDB.tenant_id == tid,
|
|
BannerConsentDB.site_id == site_id,
|
|
)
|
|
|
|
total = base.count()
|
|
|
|
# Count category acceptance rates
|
|
category_stats = {}
|
|
all_consents = base.all()
|
|
for c in all_consents:
|
|
for cat in (c.categories or []):
|
|
category_stats[cat] = category_stats.get(cat, 0) + 1
|
|
|
|
return {
|
|
"site_id": site_id,
|
|
"total_consents": total,
|
|
"category_acceptance": {
|
|
cat: {"count": count, "rate": round(count / total * 100, 1) if total > 0 else 0}
|
|
for cat, count in category_stats.items()
|
|
},
|
|
}
|
|
|
|
|
|
@router.get("/admin/sites")
|
|
async def list_site_configs(
|
|
tenant_id: str = Depends(_get_tenant),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""List all site configurations."""
|
|
tid = uuid.UUID(tenant_id)
|
|
configs = db.query(BannerSiteConfigDB).filter(
|
|
BannerSiteConfigDB.tenant_id == tid,
|
|
).order_by(BannerSiteConfigDB.created_at.desc()).all()
|
|
return [_site_config_to_dict(c) for c in configs]
|
|
|
|
|
|
@router.post("/admin/sites")
|
|
async def create_site_config(
|
|
body: SiteConfigCreate,
|
|
tenant_id: str = Depends(_get_tenant),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""Create a site configuration."""
|
|
tid = uuid.UUID(tenant_id)
|
|
|
|
existing = db.query(BannerSiteConfigDB).filter(
|
|
BannerSiteConfigDB.tenant_id == tid,
|
|
BannerSiteConfigDB.site_id == body.site_id,
|
|
).first()
|
|
if existing:
|
|
raise HTTPException(status_code=409, detail=f"Site config for '{body.site_id}' already exists")
|
|
|
|
config = BannerSiteConfigDB(
|
|
tenant_id=tid,
|
|
site_id=body.site_id,
|
|
site_name=body.site_name,
|
|
site_url=body.site_url,
|
|
banner_title=body.banner_title or "Cookie-Einstellungen",
|
|
banner_description=body.banner_description,
|
|
privacy_url=body.privacy_url,
|
|
imprint_url=body.imprint_url,
|
|
dsb_name=body.dsb_name,
|
|
dsb_email=body.dsb_email,
|
|
theme=body.theme or {},
|
|
tcf_enabled=body.tcf_enabled,
|
|
)
|
|
db.add(config)
|
|
db.commit()
|
|
db.refresh(config)
|
|
return _site_config_to_dict(config)
|
|
|
|
|
|
@router.put("/admin/sites/{site_id}")
|
|
async def update_site_config(
|
|
site_id: str,
|
|
body: SiteConfigUpdate,
|
|
tenant_id: str = Depends(_get_tenant),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""Update a site configuration."""
|
|
tid = uuid.UUID(tenant_id)
|
|
config = db.query(BannerSiteConfigDB).filter(
|
|
BannerSiteConfigDB.tenant_id == tid,
|
|
BannerSiteConfigDB.site_id == site_id,
|
|
).first()
|
|
if not config:
|
|
raise HTTPException(status_code=404, detail="Site config not found")
|
|
|
|
for field in ["site_name", "site_url", "banner_title", "banner_description",
|
|
"privacy_url", "imprint_url", "dsb_name", "dsb_email",
|
|
"theme", "tcf_enabled", "is_active"]:
|
|
val = getattr(body, field, None)
|
|
if val is not None:
|
|
setattr(config, field, val)
|
|
|
|
config.updated_at = datetime.utcnow()
|
|
db.commit()
|
|
db.refresh(config)
|
|
return _site_config_to_dict(config)
|
|
|
|
|
|
@router.delete("/admin/sites/{site_id}", status_code=204)
|
|
async def delete_site_config(
|
|
site_id: str,
|
|
tenant_id: str = Depends(_get_tenant),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""Delete a site configuration."""
|
|
tid = uuid.UUID(tenant_id)
|
|
config = db.query(BannerSiteConfigDB).filter(
|
|
BannerSiteConfigDB.tenant_id == tid,
|
|
BannerSiteConfigDB.site_id == site_id,
|
|
).first()
|
|
if not config:
|
|
raise HTTPException(status_code=404, detail="Site config not found")
|
|
|
|
db.delete(config)
|
|
db.commit()
|
|
|
|
|
|
# =============================================================================
|
|
# Admin Category Endpoints
|
|
# =============================================================================
|
|
|
|
@router.get("/admin/sites/{site_id}/categories")
|
|
async def list_categories(
|
|
site_id: str,
|
|
tenant_id: str = Depends(_get_tenant),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""List categories for a site."""
|
|
tid = uuid.UUID(tenant_id)
|
|
config = db.query(BannerSiteConfigDB).filter(
|
|
BannerSiteConfigDB.tenant_id == tid,
|
|
BannerSiteConfigDB.site_id == site_id,
|
|
).first()
|
|
if not config:
|
|
raise HTTPException(status_code=404, detail="Site config not found")
|
|
|
|
cats = db.query(BannerCategoryConfigDB).filter(
|
|
BannerCategoryConfigDB.site_config_id == config.id,
|
|
).order_by(BannerCategoryConfigDB.sort_order).all()
|
|
return [_category_to_dict(c) for c in cats]
|
|
|
|
|
|
@router.post("/admin/sites/{site_id}/categories")
|
|
async def create_category(
|
|
site_id: str,
|
|
body: CategoryConfigCreate,
|
|
tenant_id: str = Depends(_get_tenant),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""Create a category for a site."""
|
|
tid = uuid.UUID(tenant_id)
|
|
config = db.query(BannerSiteConfigDB).filter(
|
|
BannerSiteConfigDB.tenant_id == tid,
|
|
BannerSiteConfigDB.site_id == site_id,
|
|
).first()
|
|
if not config:
|
|
raise HTTPException(status_code=404, detail="Site config not found")
|
|
|
|
cat = BannerCategoryConfigDB(
|
|
site_config_id=config.id,
|
|
category_key=body.category_key,
|
|
name_de=body.name_de,
|
|
name_en=body.name_en,
|
|
description_de=body.description_de,
|
|
description_en=body.description_en,
|
|
is_required=body.is_required,
|
|
sort_order=body.sort_order,
|
|
)
|
|
db.add(cat)
|
|
db.commit()
|
|
db.refresh(cat)
|
|
return _category_to_dict(cat)
|
|
|
|
|
|
@router.delete("/admin/categories/{category_id}", status_code=204)
|
|
async def delete_category(
|
|
category_id: str,
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""Delete a category."""
|
|
try:
|
|
cid = uuid.UUID(category_id)
|
|
except ValueError:
|
|
raise HTTPException(status_code=400, detail="Invalid category ID")
|
|
|
|
cat = db.query(BannerCategoryConfigDB).filter(BannerCategoryConfigDB.id == cid).first()
|
|
if not cat:
|
|
raise HTTPException(status_code=404, detail="Category not found")
|
|
|
|
db.delete(cat)
|
|
db.commit()
|
|
|
|
|
|
# =============================================================================
|
|
# Admin Vendor Endpoints
|
|
# =============================================================================
|
|
|
|
@router.get("/admin/sites/{site_id}/vendors")
|
|
async def list_vendors(
|
|
site_id: str,
|
|
tenant_id: str = Depends(_get_tenant),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""List vendors for a site."""
|
|
tid = uuid.UUID(tenant_id)
|
|
config = db.query(BannerSiteConfigDB).filter(
|
|
BannerSiteConfigDB.tenant_id == tid,
|
|
BannerSiteConfigDB.site_id == site_id,
|
|
).first()
|
|
if not config:
|
|
raise HTTPException(status_code=404, detail="Site config not found")
|
|
|
|
vendors = db.query(BannerVendorConfigDB).filter(
|
|
BannerVendorConfigDB.site_config_id == config.id,
|
|
).all()
|
|
return [_vendor_to_dict(v) for v in vendors]
|
|
|
|
|
|
@router.post("/admin/sites/{site_id}/vendors")
|
|
async def create_vendor(
|
|
site_id: str,
|
|
body: VendorConfigCreate,
|
|
tenant_id: str = Depends(_get_tenant),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""Create a vendor for a site."""
|
|
tid = uuid.UUID(tenant_id)
|
|
config = db.query(BannerSiteConfigDB).filter(
|
|
BannerSiteConfigDB.tenant_id == tid,
|
|
BannerSiteConfigDB.site_id == site_id,
|
|
).first()
|
|
if not config:
|
|
raise HTTPException(status_code=404, detail="Site config not found")
|
|
|
|
vendor = BannerVendorConfigDB(
|
|
site_config_id=config.id,
|
|
vendor_name=body.vendor_name,
|
|
vendor_url=body.vendor_url,
|
|
category_key=body.category_key,
|
|
description_de=body.description_de,
|
|
description_en=body.description_en,
|
|
cookie_names=body.cookie_names,
|
|
retention_days=body.retention_days,
|
|
)
|
|
db.add(vendor)
|
|
db.commit()
|
|
db.refresh(vendor)
|
|
return _vendor_to_dict(vendor)
|
|
|
|
|
|
@router.delete("/admin/vendors/{vendor_id}", status_code=204)
|
|
async def delete_vendor(
|
|
vendor_id: str,
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""Delete a vendor."""
|
|
try:
|
|
vid = uuid.UUID(vendor_id)
|
|
except ValueError:
|
|
raise HTTPException(status_code=400, detail="Invalid vendor ID")
|
|
|
|
vendor = db.query(BannerVendorConfigDB).filter(BannerVendorConfigDB.id == vid).first()
|
|
if not vendor:
|
|
raise HTTPException(status_code=404, detail="Vendor not found")
|
|
|
|
db.delete(vendor)
|
|
db.commit()
|