""" 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('
', '') # 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)