This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/backend/consent_admin_api.py
Benjamin Admin 21a844cb8a 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>
2026-02-09 09:51:32 +01:00

447 lines
14 KiB
Python

"""
Consent Admin API Endpoints für BreakPilot
Stellt Admin-Funktionalität für die Verwaltung rechtlicher Dokumente bereit
"""
from fastapi import APIRouter, HTTPException, Header, Query, UploadFile, File
from typing import Optional, List
from pydantic import BaseModel
import httpx
import os
import io
from consent_client import generate_jwt_token, JWT_SECRET
# Try to import mammoth for Word document conversion
try:
import mammoth
MAMMOTH_AVAILABLE = True
except ImportError:
MAMMOTH_AVAILABLE = False
# Consent Service URL
CONSENT_SERVICE_URL = os.getenv("CONSENT_SERVICE_URL", "http://localhost:8081")
router = APIRouter(prefix="/consent/admin", tags=["consent-admin"])
# Request Models
class CreateDocumentRequest(BaseModel):
type: str
name: str
description: Optional[str] = None
is_mandatory: bool = True
class UpdateDocumentRequest(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
is_mandatory: Optional[bool] = None
is_active: Optional[bool] = None
sort_order: Optional[int] = None
class CreateVersionRequest(BaseModel):
document_id: str
version: str
language: str = "de"
title: str
content: str
summary: Optional[str] = None
class UpdateVersionRequest(BaseModel):
title: Optional[str] = None
content: Optional[str] = None
summary: Optional[str] = None
class CreateCookieCategoryRequest(BaseModel):
name: str
display_name_de: str
display_name_en: Optional[str] = None
description_de: Optional[str] = None
description_en: Optional[str] = None
is_mandatory: bool = False
sort_order: int = 0
# Admin User UUID (muss in der DB existieren!)
ADMIN_USER_UUID = "a0000000-0000-0000-0000-000000000001"
# Helper für Admin Token
def get_admin_token(authorization: Optional[str]) -> str:
"""
Extrahiert Admin-Token aus Authorization Header oder generiert einen für Dev.
In Produktion sollte hier eine echte Admin-Authentifizierung stattfinden.
"""
if authorization:
parts = authorization.split(" ")
if len(parts) == 2 and parts[0] == "Bearer":
return parts[1]
# Für Entwicklung: Generiere einen Admin-Token mit gültiger UUID
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):
"""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, timeout=10.0)
elif method == "POST":
response = await client.post(url, headers=headers, json=json_data, timeout=10.0)
elif method == "PUT":
response = await client.put(url, headers=headers, json=json_data, timeout=10.0)
elif method == "DELETE":
response = await client.delete(url, headers=headers, timeout=10.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)}")
# ==========================================
# Document Management
# ==========================================
@router.get("/documents")
async def admin_get_documents(authorization: Optional[str] = Header(None)):
"""Gibt alle Dokumente zurück (inkl. inaktive)"""
token = get_admin_token(authorization)
return await proxy_request("GET", "/documents", token)
@router.post("/documents")
async def admin_create_document(
request: CreateDocumentRequest,
authorization: Optional[str] = Header(None)
):
"""Erstellt ein neues rechtliches Dokument"""
token = get_admin_token(authorization)
return await proxy_request("POST", "/documents", token, request.dict())
@router.put("/documents/{doc_id}")
async def admin_update_document(
doc_id: str,
request: UpdateDocumentRequest,
authorization: Optional[str] = Header(None)
):
"""Aktualisiert ein rechtliches Dokument"""
token = get_admin_token(authorization)
# Nur nicht-None Werte senden
data = {k: v for k, v in request.dict().items() if v is not None}
return await proxy_request("PUT", f"/documents/{doc_id}", token, data)
@router.delete("/documents/{doc_id}")
async def admin_delete_document(
doc_id: str,
authorization: Optional[str] = Header(None)
):
"""Deaktiviert ein rechtliches Dokument (Soft-Delete)"""
token = get_admin_token(authorization)
return await proxy_request("DELETE", f"/documents/{doc_id}", token)
# ==========================================
# Version Management
# ==========================================
@router.get("/documents/{doc_id}/versions")
async def admin_get_versions(
doc_id: str,
authorization: Optional[str] = Header(None)
):
"""Gibt alle Versionen eines Dokuments zurück"""
token = get_admin_token(authorization)
return await proxy_request("GET", f"/documents/{doc_id}/versions", token)
@router.post("/versions")
async def admin_create_version(
request: CreateVersionRequest,
authorization: Optional[str] = Header(None)
):
"""Erstellt eine neue Dokumentversion"""
token = get_admin_token(authorization)
return await proxy_request("POST", "/versions", token, request.dict())
@router.put("/versions/{version_id}")
async def admin_update_version(
version_id: str,
request: UpdateVersionRequest,
authorization: Optional[str] = Header(None)
):
"""Aktualisiert eine Dokumentversion (nur draft Status)"""
token = get_admin_token(authorization)
data = {k: v for k, v in request.dict().items() if v is not None}
return await proxy_request("PUT", f"/versions/{version_id}", token, data)
@router.post("/versions/{version_id}/publish")
async def admin_publish_version(
version_id: str,
authorization: Optional[str] = Header(None)
):
"""Veröffentlicht eine Dokumentversion"""
token = get_admin_token(authorization)
return await proxy_request("POST", f"/versions/{version_id}/publish", token)
@router.post("/versions/{version_id}/archive")
async def admin_archive_version(
version_id: str,
authorization: Optional[str] = Header(None)
):
"""Archiviert eine Dokumentversion"""
token = get_admin_token(authorization)
return await proxy_request("POST", f"/versions/{version_id}/archive", token)
@router.delete("/versions/{version_id}")
async def admin_delete_version(
version_id: str,
authorization: Optional[str] = Header(None)
):
"""
Löscht eine Dokumentversion dauerhaft.
Nur Versionen im Status 'draft' oder 'rejected' können gelöscht werden.
Veröffentlichte Versionen müssen stattdessen archiviert werden.
Die Versionsnummer wird nach dem Löschen wieder frei.
"""
token = get_admin_token(authorization)
return await proxy_request("DELETE", f"/versions/{version_id}", token)
# ==========================================
# Version Approval Workflow (DSB)
# ==========================================
class ApprovalCommentRequest(BaseModel):
comment: Optional[str] = None
scheduled_publish_at: Optional[str] = None # ISO 8601: "2026-01-01T00:00:00Z"
class RejectRequest(BaseModel):
comment: str
@router.post("/versions/{version_id}/submit-review")
async def admin_submit_for_review(
version_id: str,
authorization: Optional[str] = Header(None)
):
"""Reicht eine Version zur DSB-Prüfung ein"""
token = get_admin_token(authorization)
return await proxy_request("POST", f"/versions/{version_id}/submit-review", token)
@router.post("/versions/{version_id}/approve")
async def admin_approve_version(
version_id: str,
request: ApprovalCommentRequest = None,
authorization: Optional[str] = Header(None)
):
"""
Genehmigt eine Version (nur DSB).
Mit scheduled_publish_at kann ein Veröffentlichungszeitpunkt festgelegt werden.
Format: ISO 8601 (z.B. "2026-01-01T00:00:00Z")
"""
token = get_admin_token(authorization)
data = request.dict(exclude_none=True) if request else {}
return await proxy_request("POST", f"/versions/{version_id}/approve", token, data)
@router.post("/versions/{version_id}/reject")
async def admin_reject_version(
version_id: str,
request: RejectRequest,
authorization: Optional[str] = Header(None)
):
"""Lehnt eine Version ab (nur DSB)"""
token = get_admin_token(authorization)
return await proxy_request("POST", f"/versions/{version_id}/reject", token, request.dict())
@router.get("/versions/{version_id}/compare")
async def admin_compare_versions(
version_id: str,
authorization: Optional[str] = Header(None)
):
"""Vergleicht Version mit aktuell veröffentlichter Version"""
token = get_admin_token(authorization)
return await proxy_request("GET", f"/versions/{version_id}/compare", token)
@router.get("/versions/{version_id}/approval-history")
async def admin_get_approval_history(
version_id: str,
authorization: Optional[str] = Header(None)
):
"""Gibt die Genehmigungshistorie einer Version zurück"""
token = get_admin_token(authorization)
return await proxy_request("GET", f"/versions/{version_id}/approval-history", token)
# ==========================================
# Word Document Import
# ==========================================
@router.post("/versions/upload-word")
async def upload_word_document(
file: UploadFile = File(...),
authorization: Optional[str] = Header(None)
):
"""
Konvertiert ein Word-Dokument (.docx) zu HTML.
Erfordert mammoth Library.
"""
if not MAMMOTH_AVAILABLE:
raise HTTPException(
status_code=501,
detail="Word-Import nicht verfügbar. Bitte installieren Sie: pip install mammoth"
)
# Prüfe Dateityp
if not file.filename.lower().endswith(('.docx', '.doc')):
raise HTTPException(
status_code=400,
detail="Nur .docx und .doc Dateien werden unterstützt"
)
try:
# Lese Datei-Inhalt
content = await file.read()
# Konvertiere zu HTML mit mammoth
result = mammoth.convert_to_html(io.BytesIO(content))
html = result.value
# Bereinige das HTML für bessere Darstellung
# Entferne leere Paragraphen
html = html.replace('<p></p>', '')
# Sammle Warnungen (z.B. nicht unterstützte Formate)
messages = [msg.message for msg in result.messages]
return {
"html": html,
"warnings": messages if messages else None,
"filename": file.filename
}
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Fehler bei der Konvertierung: {str(e)}"
)
# ==========================================
# Scheduled Publishing
# ==========================================
@router.get("/scheduled-versions")
async def admin_get_scheduled_versions(authorization: Optional[str] = Header(None)):
"""Gibt alle für Veröffentlichung geplanten Versionen zurück"""
token = get_admin_token(authorization)
return await proxy_request("GET", "/scheduled-versions", token)
@router.post("/scheduled-publishing/process")
async def admin_process_scheduled_publishing(authorization: Optional[str] = Header(None)):
"""
Verarbeitet alle fälligen geplanten Veröffentlichungen.
Sollte von einem Cronjob regelmäßig aufgerufen werden.
"""
token = get_admin_token(authorization)
return await proxy_request("POST", "/scheduled-publishing/process", token)
# ==========================================
# Cookie Category Management
# ==========================================
@router.get("/cookies/categories")
async def admin_get_cookie_categories(authorization: Optional[str] = Header(None)):
"""Gibt alle Cookie-Kategorien zurück"""
token = get_admin_token(authorization)
return await proxy_request("GET", "/cookies/categories", token)
@router.post("/cookies/categories")
async def admin_create_cookie_category(
request: CreateCookieCategoryRequest,
authorization: Optional[str] = Header(None)
):
"""Erstellt eine neue Cookie-Kategorie"""
token = get_admin_token(authorization)
return await proxy_request("POST", "/cookies/categories", token, request.dict())
@router.put("/cookies/categories/{cat_id}")
async def admin_update_cookie_category(
cat_id: str,
request: dict,
authorization: Optional[str] = Header(None)
):
"""Aktualisiert eine Cookie-Kategorie"""
token = get_admin_token(authorization)
return await proxy_request("PUT", f"/cookies/categories/{cat_id}", token, request)
@router.delete("/cookies/categories/{cat_id}")
async def admin_delete_cookie_category(
cat_id: str,
authorization: Optional[str] = Header(None)
):
"""Deaktiviert eine Cookie-Kategorie"""
token = get_admin_token(authorization)
return await proxy_request("DELETE", f"/cookies/categories/{cat_id}", token)
# ==========================================
# Statistics
# ==========================================
@router.get("/statistics")
async def admin_get_statistics(authorization: Optional[str] = Header(None)):
"""Gibt Statistiken über Consents zurück"""
token = get_admin_token(authorization)
return await proxy_request("GET", "/statistics", token)
@router.get("/audit-log")
async def admin_get_audit_log(
page: int = Query(1, ge=1),
per_page: int = Query(50, ge=1, le=100),
authorization: Optional[str] = Header(None)
):
"""Gibt das Audit-Log zurück"""
token = get_admin_token(authorization)
return await proxy_request("GET", f"/audit-log?page={page}&per_page={per_page}", token)