feat: DSR User Data Export — Art. 15 PDF + Art. 20 JSON/CSV

- DSRExportService: aggregates all CMP data about a user from
  Banner Consents, Einwilligungen, Audit Trail, DSR History
- GET /dsr/{id}/export-user-data?format=json|csv|pdf endpoint
- PDF: A4 reportlab with 4 sections (Consents, Einwilligungen,
  Audit-Trail, DSR-Anfragen) + cover page
- CSV: BOM-encoded for Excel with flattened data rows
- JSON: structured export with all data categories
- ActionButtons.tsx: PDF/JSON/CSV export buttons now functional

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-05-03 22:42:03 +02:00
parent 630fffc0cc
commit 02468c94c0
3 changed files with 316 additions and 1 deletions
@@ -367,3 +367,42 @@ async def update_exception_check(
):
with translate_domain_errors():
return svc.update_exception_check(dsr_id, check_id, body, tenant_id)
# =============================================================================
# User Data Export (Art. 15 / Art. 20)
# =============================================================================
@router.get("/{dsr_id}/export-user-data")
async def export_user_data(
dsr_id: str,
format: str = Query("json"),
tenant_id: str = Depends(_get_tenant),
svc: DSRService = Depends(_dsr_svc),
db: Session = Depends(get_db),
):
"""Export all CMP data about the data subject as JSON, CSV, or PDF."""
import io
from compliance.services.dsr_export_service import DSRExportService
with translate_domain_errors():
dsr = svc.get(dsr_id, tenant_id)
email = dsr.get("requester_email")
if not email:
from fastapi import HTTPException
raise HTTPException(400, "DSR has no requester email")
export_svc = DSRExportService(db)
if format == "pdf":
content, filename = export_svc.export_pdf(tenant_id, email)
return StreamingResponse(io.BytesIO(content), media_type="application/pdf",
headers={"Content-Disposition": f'attachment; filename="{filename}"'})
elif format == "csv":
content, filename = export_svc.export_csv(tenant_id, email)
return StreamingResponse(io.BytesIO(content), media_type="text/csv",
headers={"Content-Disposition": f'attachment; filename="{filename}"'})
else:
content, filename = export_svc.export_json(tenant_id, email)
return StreamingResponse(io.BytesIO(content), media_type="application/json",
headers={"Content-Disposition": f'attachment; filename="{filename}"'})