Files
Benjamin Boenisch 4435e7ea0a Initial commit: breakpilot-compliance - Compliance SDK Platform
Services: Admin-Compliance, Backend-Compliance,
AI-Compliance-SDK, Consent-SDK, Developer-Portal,
PCA-Platform, DSMS

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:28 +01:00

364 lines
11 KiB
Python

"""
GDPR API Endpoints für BreakPilot
Stellt DSGVO-konforme Funktionen für Datenauskunft und -löschung bereit
Endpoints:
- POST /privacy/export-pdf - PDF-Datenauskunft herunterladen
- GET /privacy/export-html - HTML-Preview der Datenauskunft
- GET /privacy/data-categories - Datenkategorien mit Löschfristen
- POST /privacy/request-deletion - Löschanfrage einreichen
"""
from fastapi import APIRouter, HTTPException, Header, Query
from fastapi.responses import Response, HTMLResponse
from typing import Optional
from pydantic import BaseModel
import os
from gdpr_export_service import (
GDPRExportService,
get_data_retention_policies,
get_essential_data_categories,
get_optional_data_categories,
WEASYPRINT_AVAILABLE
)
from consent_client import generate_jwt_token
# Consent Service URL
CONSENT_SERVICE_URL = os.getenv("CONSENT_SERVICE_URL", "http://localhost:8081")
# Admin User UUID
ADMIN_USER_UUID = "a0000000-0000-0000-0000-000000000001"
router = APIRouter(prefix="/consent/privacy", tags=["gdpr-privacy"])
# Service-Instanz
gdpr_service = GDPRExportService()
# Request Models
class DeletionRequest(BaseModel):
reason: Optional[str] = None
confirm: bool = False
# Helper für Token
def get_user_token(authorization: Optional[str]) -> str:
"""
Extrahiert Token aus Authorization Header oder generiert einen für Dev.
"""
if authorization:
parts = authorization.split(" ")
if len(parts) == 2 and parts[0] == "Bearer":
return parts[1]
# Für Entwicklung: Generiere einen Demo-Token
return generate_jwt_token(
user_id="demo-user-12345",
email="demo@breakpilot.app",
role="user"
)
# ==========================================
# DSGVO Art. 15: Auskunftsrecht
# ==========================================
@router.post("/export-pdf", response_class=Response)
async def export_user_data_pdf(authorization: Optional[str] = Header(None)):
"""
Generiert eine PDF-Datenauskunft gemäß DSGVO Art. 15.
Returns:
PDF-Dokument mit allen gespeicherten Nutzerdaten
"""
if not WEASYPRINT_AVAILABLE:
raise HTTPException(
status_code=501,
detail={
"error": "PDF-Export nicht verfügbar",
"message": "WeasyPrint ist nicht installiert. Bitte nutzen Sie den HTML-Export.",
"alternative": "/consent/privacy/export-html"
}
)
token = get_user_token(authorization)
try:
pdf_bytes = await gdpr_service.generate_user_data_pdf(token)
return Response(
content=pdf_bytes,
media_type="application/pdf",
headers={
"Content-Disposition": "attachment; filename=breakpilot_datenauskunft.pdf",
"Cache-Control": "no-store, no-cache, must-revalidate",
"Pragma": "no-cache"
}
)
except Exception as e:
raise HTTPException(
status_code=500,
detail={
"error": "PDF-Generierung fehlgeschlagen",
"message": str(e)
}
)
@router.get("/export-html", response_class=HTMLResponse)
async def export_user_data_html(authorization: Optional[str] = Header(None)):
"""
Generiert eine HTML-Datenauskunft (Preview oder Alternative zu PDF).
Returns:
HTML-Dokument mit allen gespeicherten Nutzerdaten
"""
token = get_user_token(authorization)
try:
html_content = await gdpr_service.generate_user_data_html(token)
return HTMLResponse(
content=html_content,
headers={
"Cache-Control": "no-store, no-cache, must-revalidate",
"Pragma": "no-cache"
}
)
except Exception as e:
raise HTTPException(
status_code=500,
detail={
"error": "HTML-Generierung fehlgeschlagen",
"message": str(e)
}
)
# ==========================================
# Datenkategorien & Löschfristen
# ==========================================
@router.get("/data-categories")
async def get_data_categories(
filter: Optional[str] = Query(None, description="Filter: 'essential', 'optional', oder leer für alle")
):
"""
Gibt alle Datenkategorien mit ihren Löschfristen zurück.
Diese Information wird auch im PDF-Export angezeigt und gibt Nutzern
Transparenz darüber, welche Daten wie lange gespeichert werden.
Query Parameters:
filter: 'essential' für Pflicht-Daten, 'optional' für Opt-in Daten
"""
if filter == "essential":
categories = get_essential_data_categories()
elif filter == "optional":
categories = get_optional_data_categories()
else:
categories = get_data_retention_policies()
return {
"categories": categories,
"total_count": len(categories),
"filter_applied": filter,
"info": {
"essential": "Diese Daten sind für den Betrieb des Dienstes erforderlich",
"optional": "Diese Daten werden nur bei expliziter Zustimmung erhoben"
}
}
@router.get("/data-categories/{category}")
async def get_data_category_details(category: str):
"""
Gibt Details zu einer spezifischen Datenkategorie zurück.
"""
all_categories = get_data_retention_policies()
for cat in all_categories:
if cat["category"] == category:
return cat
raise HTTPException(
status_code=404,
detail=f"Datenkategorie '{category}' nicht gefunden"
)
# ==========================================
# DSGVO Art. 17: Recht auf Löschung
# ==========================================
@router.post("/request-deletion")
async def request_data_deletion(
request: DeletionRequest,
authorization: Optional[str] = Header(None)
):
"""
Reicht einen Antrag auf Datenlöschung ein (DSGVO Art. 17).
Der Antrag wird protokolliert und innerhalb von 30 Tagen bearbeitet.
Bestimmte Daten müssen aufgrund gesetzlicher Aufbewahrungsfristen
möglicherweise länger gespeichert werden.
Body:
reason: Optionaler Grund für die Löschung
confirm: Muss true sein zur Bestätigung
"""
if not request.confirm:
raise HTTPException(
status_code=400,
detail={
"error": "Bestätigung erforderlich",
"message": "Bitte bestätigen Sie den Löschantrag mit 'confirm: true'",
"warning": "Die Löschung Ihrer Daten kann nicht rückgängig gemacht werden!"
}
)
token = get_user_token(authorization)
# TODO: Hier sollte der Löschantrag an den Go-Service weitergeleitet werden
# Für jetzt: Platzhalter-Response
return {
"status": "pending",
"message": "Ihr Löschantrag wurde eingereicht",
"details": {
"request_date": "2024-01-15T10:30:00Z",
"expected_completion": "Innerhalb von 30 Tagen",
"reason_provided": request.reason is not None,
"exceptions": [
"Consent-Nachweise (3 Jahre Aufbewahrungspflicht)",
"Anonymisierte Audit-Logs (Compliance)"
]
},
"next_steps": [
"Sie erhalten eine Bestätigungs-E-Mail",
"Die Löschung wird nach Prüfung durchgeführt",
"Bei vollständiger Löschung wird Ihr Account deaktiviert"
]
}
# ==========================================
# Admin Endpoints für GDPR
# ==========================================
admin_router = APIRouter(prefix="/consent/admin/privacy", tags=["gdpr-admin"])
def get_admin_token(authorization: Optional[str]) -> str:
"""Extrahiert Admin-Token aus Authorization Header oder generiert einen für Dev."""
if authorization:
parts = authorization.split(" ")
if len(parts) == 2 and parts[0] == "Bearer":
return parts[1]
return generate_jwt_token(
user_id=ADMIN_USER_UUID,
email="admin@breakpilot.app",
role="admin"
)
@admin_router.get("/export-pdf/{user_id}", response_class=Response)
async def admin_export_user_data_pdf(
user_id: str,
authorization: Optional[str] = Header(None)
):
"""
[Admin] Generiert PDF-Datenauskunft für einen beliebigen Nutzer.
Nur für Admins: Ermöglicht Export von Nutzerdaten für Support-Anfragen
oder Behördenanfragen.
"""
if not WEASYPRINT_AVAILABLE:
raise HTTPException(
status_code=501,
detail="PDF-Export nicht verfügbar. WeasyPrint fehlt."
)
# Für Admin-Export: Generiere Token für den angegebenen Nutzer
# In Produktion sollte hier eine echte Nutzer-Abfrage stattfinden
token = generate_jwt_token(
user_id=user_id,
email=f"user-{user_id[:8]}@breakpilot.app",
role="user"
)
try:
pdf_bytes = await gdpr_service.generate_user_data_pdf(token)
return Response(
content=pdf_bytes,
media_type="application/pdf",
headers={
"Content-Disposition": f"attachment; filename=datenauskunft_{user_id[:8]}.pdf",
"Cache-Control": "no-store"
}
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@admin_router.get("/deletion-requests")
async def admin_get_deletion_requests(
status: Optional[str] = Query(None, description="Filter: pending, processing, completed"),
page: int = Query(1, ge=1),
per_page: int = Query(20, ge=1, le=100),
authorization: Optional[str] = Header(None)
):
"""
[Admin] Gibt alle Löschanträge zurück.
"""
# TODO: Implementierung mit echtem Backend
return {
"requests": [],
"total": 0,
"page": page,
"per_page": per_page,
"status_filter": status,
"message": "Löschanträge-Verwaltung noch nicht implementiert"
}
@admin_router.post("/deletion-requests/{request_id}/process")
async def admin_process_deletion_request(
request_id: str,
authorization: Optional[str] = Header(None)
):
"""
[Admin] Bearbeitet einen Löschantrag.
"""
# TODO: Implementierung mit echtem Backend
return {
"request_id": request_id,
"status": "processing",
"message": "Löschantrag wird bearbeitet"
}
@admin_router.get("/retention-stats")
async def admin_get_retention_stats(authorization: Optional[str] = Header(None)):
"""
[Admin] Gibt Statistiken über Daten und Löschfristen zurück.
"""
# TODO: Implementierung mit echtem Backend
categories = get_data_retention_policies()
return {
"total_categories": len(categories),
"essential_categories": len(get_essential_data_categories()),
"optional_categories": len(get_optional_data_categories()),
"categories": [
{
"name": cat["name_de"],
"retention_days": cat.get("retention_days"),
"is_essential": cat.get("is_essential", True)
}
for cat in categories
],
"message": "Detaillierte Statistiken noch nicht implementiert"
}