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>
416 lines
14 KiB
Python
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)
|