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