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:
446
backend/consent_admin_api.py
Normal file
446
backend/consent_admin_api.py
Normal file
@@ -0,0 +1,446 @@
|
||||
"""
|
||||
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)
|
||||
Reference in New Issue
Block a user