A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
364 lines
11 KiB
Python
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"
|
|
}
|