fix: Restore all files lost during destructive rebase
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>
This commit is contained in:
415
backend/dsr_admin_api.py
Normal file
415
backend/dsr_admin_api.py
Normal file
@@ -0,0 +1,415 @@
|
||||
"""
|
||||
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)
|
||||
Reference in New Issue
Block a user