""" Data Subject Request (DSR) Admin API - Betroffenenanfragen-Verwaltung Admin-Endpunkte für die Verwaltung von Betroffenenanfragen nach DSGVO """ from fastapi import APIRouter, HTTPException, Header, Query from typing import Optional, List, Dict, Any from pydantic import BaseModel import httpx import os from consent_client import generate_jwt_token, JWT_SECRET # Consent Service URL CONSENT_SERVICE_URL = os.getenv("CONSENT_SERVICE_URL", "http://localhost:8081") router = APIRouter(prefix="/v1/admin/dsr", tags=["dsr-admin"]) # Admin User UUID (muss in der DB existieren!) ADMIN_USER_UUID = "a0000000-0000-0000-0000-000000000001" # Request Models class CreateDSRRequest(BaseModel): """Admin-Anfrage zum manuellen Erstellen einer Betroffenenanfrage""" request_type: str requester_email: str requester_name: Optional[str] = None requester_phone: Optional[str] = None request_details: Optional[Dict[str, Any]] = None priority: Optional[str] = None # normal, high, expedited source: Optional[str] = "admin_panel" class UpdateDSRRequest(BaseModel): """Anfrage zum Aktualisieren einer Betroffenenanfrage""" status: Optional[str] = None priority: Optional[str] = None processing_notes: Optional[str] = None class UpdateStatusRequest(BaseModel): """Anfrage zum Ändern des Status""" status: str comment: Optional[str] = None class VerifyIdentityRequest(BaseModel): """Anfrage zur Identitätsverifizierung""" method: str # id_card, passport, video_call, email, phone, other class AssignRequest(BaseModel): """Anfrage zur Zuweisung""" assignee_id: str class ExtendDeadlineRequest(BaseModel): """Anfrage zur Fristverlängerung""" reason: str days: Optional[int] = 60 class CompleteDSRRequest(BaseModel): """Anfrage zum Abschließen einer Betroffenenanfrage""" summary: str result_data: Optional[Dict[str, Any]] = None class RejectDSRRequest(BaseModel): """Anfrage zum Ablehnen einer Betroffenenanfrage""" reason: str legal_basis: str # Art. 17(3)a, Art. 17(3)b, Art. 17(3)c, Art. 17(3)d, Art. 17(3)e, Art. 12(5) class SendCommunicationRequest(BaseModel): """Anfrage zum Senden einer Kommunikation""" communication_type: str template_version_id: Optional[str] = None custom_subject: Optional[str] = None custom_body: Optional[str] = None variables: Optional[Dict[str, str]] = None class UpdateExceptionCheckRequest(BaseModel): """Anfrage zum Aktualisieren einer Ausnahmeprüfung""" applies: bool notes: Optional[str] = None class CreateTemplateVersionRequest(BaseModel): """Anfrage zum Erstellen einer Vorlagen-Version""" version: str language: Optional[str] = "de" subject: str body_html: str body_text: Optional[str] = None # Helper für Admin Token def get_admin_token(authorization: Optional[str]) -> str: if authorization: parts = authorization.split(" ") if len(parts) == 2 and parts[0] == "Bearer": return parts[1] # Für Entwicklung: Generiere einen Admin-Token return generate_jwt_token( user_id=ADMIN_USER_UUID, email="admin@breakpilot.app", role="admin" ) async def proxy_request(method: str, path: str, token: str, json_data=None, query_params=None): """Proxied Anfragen an den Go Consent Service""" url = f"{CONSENT_SERVICE_URL}/api/v1/admin{path}" headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json" } async with httpx.AsyncClient() as client: try: if method == "GET": response = await client.get(url, headers=headers, params=query_params, timeout=30.0) elif method == "POST": response = await client.post(url, headers=headers, json=json_data, timeout=30.0) elif method == "PUT": response = await client.put(url, headers=headers, json=json_data, timeout=30.0) elif method == "DELETE": response = await client.delete(url, headers=headers, timeout=30.0) else: raise HTTPException(status_code=400, detail="Invalid method") if response.status_code >= 400: error_detail = response.json() if response.content else {"error": "Unknown error"} raise HTTPException(status_code=response.status_code, detail=error_detail) return response.json() if response.content else {"success": True} except httpx.RequestError as e: raise HTTPException(status_code=503, detail=f"Consent Service unavailable: {str(e)}") # ========================================== # DSR List & Statistics # ========================================== @router.get("") async def admin_list_dsr( status: Optional[str] = Query(None, description="Filter by status"), request_type: Optional[str] = Query(None, description="Filter by request type"), assigned_to: Optional[str] = Query(None, description="Filter by assignee"), priority: Optional[str] = Query(None, description="Filter by priority"), overdue_only: bool = Query(False, description="Only overdue requests"), search: Optional[str] = Query(None, description="Search term"), from_date: Optional[str] = Query(None, description="From date (YYYY-MM-DD)"), to_date: Optional[str] = Query(None, description="To date (YYYY-MM-DD)"), limit: int = Query(20, ge=1, le=100), offset: int = Query(0, ge=0), authorization: Optional[str] = Header(None) ): """Gibt alle Betroffenenanfragen mit Filtern zurück""" token = get_admin_token(authorization) params = {"limit": limit, "offset": offset} if status: params["status"] = status if request_type: params["request_type"] = request_type if assigned_to: params["assigned_to"] = assigned_to if priority: params["priority"] = priority if overdue_only: params["overdue_only"] = "true" if search: params["search"] = search if from_date: params["from_date"] = from_date if to_date: params["to_date"] = to_date return await proxy_request("GET", "/dsr", token, query_params=params) @router.get("/stats") async def admin_get_dsr_stats(authorization: Optional[str] = Header(None)): """Gibt Dashboard-Statistiken für Betroffenenanfragen zurück""" token = get_admin_token(authorization) return await proxy_request("GET", "/dsr/stats", token) # ========================================== # Single DSR Management # ========================================== @router.get("/{dsr_id}") async def admin_get_dsr(dsr_id: str, authorization: Optional[str] = Header(None)): """Gibt Details einer Betroffenenanfrage zurück""" token = get_admin_token(authorization) return await proxy_request("GET", f"/dsr/{dsr_id}", token) @router.post("") async def admin_create_dsr( request: CreateDSRRequest, authorization: Optional[str] = Header(None) ): """Erstellt eine Betroffenenanfrage manuell""" token = get_admin_token(authorization) return await proxy_request("POST", "/dsr", token, request.dict(exclude_none=True)) @router.put("/{dsr_id}") async def admin_update_dsr( dsr_id: str, request: UpdateDSRRequest, authorization: Optional[str] = Header(None) ): """Aktualisiert eine Betroffenenanfrage""" token = get_admin_token(authorization) return await proxy_request("PUT", f"/dsr/{dsr_id}", token, request.dict(exclude_none=True)) @router.post("/{dsr_id}/status") async def admin_update_dsr_status( dsr_id: str, request: UpdateStatusRequest, authorization: Optional[str] = Header(None) ): """Ändert den Status einer Betroffenenanfrage""" token = get_admin_token(authorization) return await proxy_request("POST", f"/dsr/{dsr_id}/status", token, request.dict(exclude_none=True)) # ========================================== # DSR Workflow Actions # ========================================== @router.post("/{dsr_id}/verify-identity") async def admin_verify_identity( dsr_id: str, request: VerifyIdentityRequest, authorization: Optional[str] = Header(None) ): """Verifiziert die Identität des Antragstellers""" token = get_admin_token(authorization) return await proxy_request("POST", f"/dsr/{dsr_id}/verify-identity", token, request.dict()) @router.post("/{dsr_id}/assign") async def admin_assign_dsr( dsr_id: str, request: AssignRequest, authorization: Optional[str] = Header(None) ): """Weist eine Betroffenenanfrage einem Bearbeiter zu""" token = get_admin_token(authorization) return await proxy_request("POST", f"/dsr/{dsr_id}/assign", token, request.dict()) @router.post("/{dsr_id}/extend") async def admin_extend_deadline( dsr_id: str, request: ExtendDeadlineRequest, authorization: Optional[str] = Header(None) ): """Verlängert die Bearbeitungsfrist (max. 2 weitere Monate nach Art. 12(3))""" token = get_admin_token(authorization) return await proxy_request("POST", f"/dsr/{dsr_id}/extend", token, request.dict()) @router.post("/{dsr_id}/complete") async def admin_complete_dsr( dsr_id: str, request: CompleteDSRRequest, authorization: Optional[str] = Header(None) ): """Schließt eine Betroffenenanfrage erfolgreich ab""" token = get_admin_token(authorization) return await proxy_request("POST", f"/dsr/{dsr_id}/complete", token, request.dict(exclude_none=True)) @router.post("/{dsr_id}/reject") async def admin_reject_dsr( dsr_id: str, request: RejectDSRRequest, authorization: Optional[str] = Header(None) ): """Lehnt eine Betroffenenanfrage mit Rechtsgrundlage ab""" token = get_admin_token(authorization) return await proxy_request("POST", f"/dsr/{dsr_id}/reject", token, request.dict()) # ========================================== # DSR History & Communications # ========================================== @router.get("/{dsr_id}/history") async def admin_get_dsr_history(dsr_id: str, authorization: Optional[str] = Header(None)): """Gibt die Status-Historie einer Betroffenenanfrage zurück""" token = get_admin_token(authorization) return await proxy_request("GET", f"/dsr/{dsr_id}/history", token) @router.get("/{dsr_id}/communications") async def admin_get_dsr_communications(dsr_id: str, authorization: Optional[str] = Header(None)): """Gibt die Kommunikationshistorie einer Betroffenenanfrage zurück""" token = get_admin_token(authorization) return await proxy_request("GET", f"/dsr/{dsr_id}/communications", token) @router.post("/{dsr_id}/communicate") async def admin_send_communication( dsr_id: str, request: SendCommunicationRequest, authorization: Optional[str] = Header(None) ): """Sendet eine Kommunikation zum Antragsteller""" token = get_admin_token(authorization) return await proxy_request("POST", f"/dsr/{dsr_id}/communicate", token, request.dict(exclude_none=True)) # ========================================== # Exception Checks (Art. 17) # ========================================== @router.get("/{dsr_id}/exception-checks") async def admin_get_exception_checks(dsr_id: str, authorization: Optional[str] = Header(None)): """Gibt die Ausnahmeprüfungen für Löschanfragen (Art. 17) zurück""" token = get_admin_token(authorization) return await proxy_request("GET", f"/dsr/{dsr_id}/exception-checks", token) @router.post("/{dsr_id}/exception-checks/init") async def admin_init_exception_checks(dsr_id: str, authorization: Optional[str] = Header(None)): """Initialisiert die Ausnahmeprüfungen für eine Löschanfrage""" token = get_admin_token(authorization) return await proxy_request("POST", f"/dsr/{dsr_id}/exception-checks/init", token) @router.put("/{dsr_id}/exception-checks/{check_id}") async def admin_update_exception_check( dsr_id: str, check_id: str, request: UpdateExceptionCheckRequest, authorization: Optional[str] = Header(None) ): """Aktualisiert eine einzelne Ausnahmeprüfung""" token = get_admin_token(authorization) return await proxy_request("PUT", f"/dsr/{dsr_id}/exception-checks/{check_id}", token, request.dict(exclude_none=True)) # ========================================== # Deadline Processing # ========================================== @router.post("/deadlines/process") async def admin_process_deadlines(authorization: Optional[str] = Header(None)): """Verarbeitet Fristen und sendet Warnungen (für Cronjob)""" token = get_admin_token(authorization) return await proxy_request("POST", "/dsr/deadlines/process", token) # ========================================== # DSR Templates Router # ========================================== templates_router = APIRouter(prefix="/v1/admin/dsr-templates", tags=["dsr-templates"]) @templates_router.get("") async def admin_get_templates(authorization: Optional[str] = Header(None)): """Gibt alle DSR-Vorlagen zurück""" token = get_admin_token(authorization) return await proxy_request("GET", "/dsr-templates", token) @templates_router.get("/published") async def admin_get_published_templates( request_type: Optional[str] = Query(None, description="Filter by request type"), language: str = Query("de", description="Language"), authorization: Optional[str] = Header(None) ): """Gibt alle veröffentlichten Vorlagen für die Auswahl zurück""" token = get_admin_token(authorization) params = {"language": language} if request_type: params["request_type"] = request_type return await proxy_request("GET", "/dsr-templates/published", token, query_params=params) @templates_router.get("/{template_id}/versions") async def admin_get_template_versions(template_id: str, authorization: Optional[str] = Header(None)): """Gibt alle Versionen einer Vorlage zurück""" token = get_admin_token(authorization) return await proxy_request("GET", f"/dsr-templates/{template_id}/versions", token) @templates_router.post("/{template_id}/versions") async def admin_create_template_version( template_id: str, request: CreateTemplateVersionRequest, authorization: Optional[str] = Header(None) ): """Erstellt eine neue Version einer Vorlage""" token = get_admin_token(authorization) return await proxy_request("POST", f"/dsr-templates/{template_id}/versions", token, request.dict(exclude_none=True)) @templates_router.post("/versions/{version_id}/publish") async def admin_publish_template_version(version_id: str, authorization: Optional[str] = Header(None)): """Veröffentlicht eine Vorlagen-Version""" token = get_admin_token(authorization) return await proxy_request("POST", f"/dsr-template-versions/{version_id}/publish", token)