Files
breakpilot-compliance/backend-compliance/dsr_admin_api.py
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

416 lines
14 KiB
Python

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