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>
277 lines
7.7 KiB
Python
277 lines
7.7 KiB
Python
"""
|
|
Consent API Endpoints für BreakPilot
|
|
Stellt die Consent-Funktionalität über das BreakPilot Backend bereit
|
|
"""
|
|
|
|
from fastapi import APIRouter, HTTPException, Header, Query
|
|
from typing import Optional, List
|
|
from pydantic import BaseModel
|
|
|
|
from consent_client import consent_client, DocumentType, generate_demo_token, generate_jwt_token
|
|
|
|
router = APIRouter(prefix="/consent", tags=["consent"])
|
|
|
|
|
|
# Request/Response Models
|
|
class ConsentRequest(BaseModel):
|
|
document_type: str
|
|
version_id: str
|
|
consented: bool = True
|
|
|
|
|
|
class CookieConsentItem(BaseModel):
|
|
category_id: str
|
|
consented: bool
|
|
|
|
|
|
class CookieConsentRequest(BaseModel):
|
|
categories: List[CookieConsentItem]
|
|
|
|
|
|
class DataDeletionRequest(BaseModel):
|
|
reason: Optional[str] = None
|
|
|
|
|
|
# Helper für JWT Token
|
|
def get_token(authorization: Optional[str], allow_demo: bool = True) -> str:
|
|
"""
|
|
Extrahiert JWT Token aus Authorization Header.
|
|
Falls kein Token vorhanden und allow_demo=True, wird ein Demo-Token generiert.
|
|
"""
|
|
if authorization:
|
|
parts = authorization.split(" ")
|
|
if len(parts) == 2 and parts[0] == "Bearer":
|
|
return parts[1]
|
|
|
|
if allow_demo:
|
|
# Generiere einen Demo-Token für nicht-authentifizierte Benutzer
|
|
return generate_demo_token()
|
|
|
|
raise HTTPException(status_code=401, detail="Authorization header required")
|
|
|
|
|
|
# ==========================================
|
|
# Token Endpoints
|
|
# ==========================================
|
|
|
|
@router.get("/token/demo")
|
|
async def get_demo_token():
|
|
"""
|
|
Generiert einen Demo-Token für nicht-authentifizierte Benutzer.
|
|
Dieser Token ermöglicht das Lesen von öffentlichen Dokumenten.
|
|
"""
|
|
token = generate_demo_token()
|
|
return {
|
|
"token": token,
|
|
"type": "Bearer",
|
|
"expires_in": 86400 # 24 Stunden
|
|
}
|
|
|
|
|
|
# ==========================================
|
|
# Public Endpoints
|
|
# ==========================================
|
|
|
|
@router.get("/check/{document_type}")
|
|
async def check_consent(
|
|
document_type: str,
|
|
language: str = Query("de"),
|
|
authorization: Optional[str] = Header(None)
|
|
):
|
|
"""
|
|
Prüft ob der Benutzer einem Dokument zugestimmt hat.
|
|
Gibt zurück ob Zustimmung vorliegt und ob sie aktualisiert werden muss.
|
|
"""
|
|
token = get_token(authorization)
|
|
|
|
try:
|
|
doc_type = DocumentType(document_type)
|
|
except ValueError:
|
|
raise HTTPException(status_code=400, detail=f"Invalid document type: {document_type}")
|
|
|
|
status = await consent_client.check_consent(token, doc_type, language)
|
|
|
|
return {
|
|
"has_consent": status.has_consent,
|
|
"current_version_id": status.current_version_id,
|
|
"consented_version": status.consented_version,
|
|
"needs_update": status.needs_update,
|
|
"consented_at": status.consented_at
|
|
}
|
|
|
|
|
|
@router.get("/pending")
|
|
async def get_pending_consents(
|
|
language: str = Query("de"),
|
|
authorization: Optional[str] = Header(None)
|
|
):
|
|
"""
|
|
Gibt alle Dokumente zurück, die noch Zustimmung benötigen.
|
|
Nützlich für Anzeige beim Login oder in den Einstellungen.
|
|
"""
|
|
token = get_token(authorization)
|
|
pending = await consent_client.get_pending_consents(token, language)
|
|
|
|
return {
|
|
"pending_consents": pending,
|
|
"has_pending": len(pending) > 0
|
|
}
|
|
|
|
|
|
@router.get("/documents/{document_type}/latest")
|
|
async def get_latest_document(
|
|
document_type: str,
|
|
language: str = Query("de"),
|
|
authorization: Optional[str] = Header(None)
|
|
):
|
|
"""Holt die aktuellste Version eines rechtlichen Dokuments"""
|
|
token = get_token(authorization)
|
|
|
|
doc = await consent_client.get_latest_document(token, document_type, language)
|
|
|
|
if not doc:
|
|
raise HTTPException(status_code=404, detail="Document not found")
|
|
|
|
return {
|
|
"id": doc.id,
|
|
"document_id": doc.document_id,
|
|
"version": doc.version,
|
|
"language": doc.language,
|
|
"title": doc.title,
|
|
"content": doc.content,
|
|
"summary": doc.summary
|
|
}
|
|
|
|
|
|
@router.post("/give")
|
|
async def give_consent(
|
|
request: ConsentRequest,
|
|
authorization: Optional[str] = Header(None)
|
|
):
|
|
"""Speichert die Zustimmung des Benutzers zu einem Dokument"""
|
|
token = get_token(authorization)
|
|
|
|
success = await consent_client.give_consent(
|
|
token,
|
|
request.document_type,
|
|
request.version_id,
|
|
request.consented
|
|
)
|
|
|
|
if not success:
|
|
raise HTTPException(status_code=500, detail="Failed to save consent")
|
|
|
|
return {"success": True, "message": "Consent saved successfully"}
|
|
|
|
|
|
# ==========================================
|
|
# Cookie Consent Endpoints
|
|
# ==========================================
|
|
|
|
@router.get("/cookies/categories")
|
|
async def get_cookie_categories(
|
|
language: str = Query("de"),
|
|
authorization: Optional[str] = Header(None)
|
|
):
|
|
"""Holt alle Cookie-Kategorien für das Cookie-Banner"""
|
|
token = get_token(authorization)
|
|
categories = await consent_client.get_cookie_categories(token, language)
|
|
|
|
return {"categories": categories}
|
|
|
|
|
|
@router.post("/cookies")
|
|
async def set_cookie_consent(
|
|
request: CookieConsentRequest,
|
|
authorization: Optional[str] = Header(None)
|
|
):
|
|
"""Speichert die Cookie-Präferenzen des Benutzers"""
|
|
token = get_token(authorization)
|
|
|
|
categories = [
|
|
{"category_id": cat.category_id, "consented": cat.consented}
|
|
for cat in request.categories
|
|
]
|
|
|
|
success = await consent_client.set_cookie_consent(token, categories)
|
|
|
|
if not success:
|
|
raise HTTPException(status_code=500, detail="Failed to save cookie preferences")
|
|
|
|
return {"success": True, "message": "Cookie preferences saved"}
|
|
|
|
|
|
# ==========================================
|
|
# GDPR / Privacy Endpoints
|
|
# ==========================================
|
|
|
|
@router.get("/privacy/my-data")
|
|
async def get_my_data(authorization: Optional[str] = Header(None)):
|
|
"""
|
|
GDPR Art. 15: Auskunftsrecht
|
|
Gibt alle über den Benutzer gespeicherten Daten zurück.
|
|
"""
|
|
token = get_token(authorization)
|
|
data = await consent_client.get_my_data(token)
|
|
|
|
if not data:
|
|
raise HTTPException(status_code=500, detail="Failed to retrieve data")
|
|
|
|
return data
|
|
|
|
|
|
@router.post("/privacy/export")
|
|
async def request_data_export(authorization: Optional[str] = Header(None)):
|
|
"""
|
|
GDPR Art. 20: Recht auf Datenübertragbarkeit
|
|
Fordert einen Export aller Benutzerdaten an.
|
|
"""
|
|
token = get_token(authorization)
|
|
request_id = await consent_client.request_data_export(token)
|
|
|
|
if not request_id:
|
|
raise HTTPException(status_code=500, detail="Failed to create export request")
|
|
|
|
return {
|
|
"success": True,
|
|
"request_id": request_id,
|
|
"message": "Export request created. You will be notified when ready."
|
|
}
|
|
|
|
|
|
@router.post("/privacy/delete")
|
|
async def request_data_deletion(
|
|
request: DataDeletionRequest,
|
|
authorization: Optional[str] = Header(None)
|
|
):
|
|
"""
|
|
GDPR Art. 17: Recht auf Löschung
|
|
Fordert die Löschung aller Benutzerdaten an.
|
|
"""
|
|
token = get_token(authorization)
|
|
request_id = await consent_client.request_data_deletion(token, request.reason)
|
|
|
|
if not request_id:
|
|
raise HTTPException(status_code=500, detail="Failed to create deletion request")
|
|
|
|
return {
|
|
"success": True,
|
|
"request_id": request_id,
|
|
"message": "Deletion request created. We will process it within 30 days."
|
|
}
|
|
|
|
|
|
# ==========================================
|
|
# Health Check
|
|
# ==========================================
|
|
|
|
@router.get("/health")
|
|
async def consent_health():
|
|
"""Prüft die Verbindung zum Consent Service"""
|
|
is_healthy = await consent_client.health_check()
|
|
|
|
return {
|
|
"consent_service": "healthy" if is_healthy else "unavailable",
|
|
"connected": is_healthy
|
|
}
|