""" 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" }