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>
374 lines
9.6 KiB
Python
374 lines
9.6 KiB
Python
"""
|
|
Authentication API Endpoints für BreakPilot
|
|
Proxy für den Go Consent Service Authentication
|
|
"""
|
|
|
|
import httpx
|
|
from fastapi import APIRouter, HTTPException, Header, Request, Response
|
|
from typing import Optional
|
|
from pydantic import BaseModel, EmailStr
|
|
import os
|
|
|
|
# Consent Service URL
|
|
CONSENT_SERVICE_URL = os.getenv("CONSENT_SERVICE_URL", "http://localhost:8081")
|
|
|
|
router = APIRouter(prefix="/auth", tags=["authentication"])
|
|
|
|
|
|
# ==========================================
|
|
# Request/Response Models
|
|
# ==========================================
|
|
|
|
class RegisterRequest(BaseModel):
|
|
email: EmailStr
|
|
password: str
|
|
name: Optional[str] = None
|
|
|
|
|
|
class LoginRequest(BaseModel):
|
|
email: EmailStr
|
|
password: str
|
|
|
|
|
|
class RefreshTokenRequest(BaseModel):
|
|
refresh_token: str
|
|
|
|
|
|
class VerifyEmailRequest(BaseModel):
|
|
token: str
|
|
|
|
|
|
class ForgotPasswordRequest(BaseModel):
|
|
email: EmailStr
|
|
|
|
|
|
class ResetPasswordRequest(BaseModel):
|
|
token: str
|
|
new_password: str
|
|
|
|
|
|
class ChangePasswordRequest(BaseModel):
|
|
current_password: str
|
|
new_password: str
|
|
|
|
|
|
class UpdateProfileRequest(BaseModel):
|
|
name: Optional[str] = None
|
|
|
|
|
|
class LogoutRequest(BaseModel):
|
|
refresh_token: Optional[str] = None
|
|
|
|
|
|
# ==========================================
|
|
# Helper Functions
|
|
# ==========================================
|
|
|
|
def get_auth_headers(authorization: Optional[str]) -> dict:
|
|
"""Erstellt Header mit Authorization Token"""
|
|
headers = {"Content-Type": "application/json"}
|
|
if authorization:
|
|
headers["Authorization"] = authorization
|
|
return headers
|
|
|
|
|
|
async def proxy_to_consent_service(
|
|
method: str,
|
|
path: str,
|
|
json_data: dict = None,
|
|
headers: dict = None,
|
|
params: dict = None
|
|
) -> dict:
|
|
"""
|
|
Proxy-Aufruf zum Go Consent Service.
|
|
Wirft HTTPException bei Fehlern.
|
|
"""
|
|
url = f"{CONSENT_SERVICE_URL}/api/v1{path}"
|
|
|
|
async with httpx.AsyncClient() as client:
|
|
try:
|
|
if method == "GET":
|
|
response = await client.get(url, headers=headers, params=params, 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, params=params, timeout=10.0)
|
|
else:
|
|
raise ValueError(f"Unsupported HTTP method: {method}")
|
|
|
|
# Parse JSON response
|
|
try:
|
|
data = response.json()
|
|
except:
|
|
data = {"message": response.text}
|
|
|
|
# Handle error responses
|
|
if response.status_code >= 400:
|
|
error_msg = data.get("error", "Unknown error")
|
|
raise HTTPException(status_code=response.status_code, detail=error_msg)
|
|
|
|
return data
|
|
|
|
except httpx.RequestError as e:
|
|
raise HTTPException(
|
|
status_code=503,
|
|
detail=f"Consent Service nicht erreichbar: {str(e)}"
|
|
)
|
|
|
|
|
|
# ==========================================
|
|
# Public Auth Endpoints (No Auth Required)
|
|
# ==========================================
|
|
|
|
@router.post("/register")
|
|
async def register(request: RegisterRequest, req: Request):
|
|
"""
|
|
Registriert einen neuen Benutzer.
|
|
Sendet eine Verifizierungs-E-Mail.
|
|
"""
|
|
data = await proxy_to_consent_service(
|
|
"POST",
|
|
"/auth/register",
|
|
json_data={
|
|
"email": request.email,
|
|
"password": request.password,
|
|
"name": request.name
|
|
}
|
|
)
|
|
return data
|
|
|
|
|
|
@router.post("/login")
|
|
async def login(request: LoginRequest, req: Request):
|
|
"""
|
|
Meldet einen Benutzer an.
|
|
Gibt Access Token und Refresh Token zurück.
|
|
"""
|
|
# Get client info for session tracking
|
|
client_ip = req.client.host if req.client else "unknown"
|
|
user_agent = req.headers.get("user-agent", "unknown")
|
|
|
|
data = await proxy_to_consent_service(
|
|
"POST",
|
|
"/auth/login",
|
|
json_data={
|
|
"email": request.email,
|
|
"password": request.password
|
|
},
|
|
headers={
|
|
"X-Forwarded-For": client_ip,
|
|
"User-Agent": user_agent
|
|
}
|
|
)
|
|
return data
|
|
|
|
|
|
@router.post("/logout")
|
|
async def logout(request: LogoutRequest):
|
|
"""
|
|
Meldet den Benutzer ab und invalidiert den Refresh Token.
|
|
"""
|
|
data = await proxy_to_consent_service(
|
|
"POST",
|
|
"/auth/logout",
|
|
json_data={"refresh_token": request.refresh_token} if request.refresh_token else {}
|
|
)
|
|
return data
|
|
|
|
|
|
@router.post("/refresh")
|
|
async def refresh_token(request: RefreshTokenRequest):
|
|
"""
|
|
Erneuert den Access Token mit einem gültigen Refresh Token.
|
|
"""
|
|
data = await proxy_to_consent_service(
|
|
"POST",
|
|
"/auth/refresh",
|
|
json_data={"refresh_token": request.refresh_token}
|
|
)
|
|
return data
|
|
|
|
|
|
@router.post("/verify-email")
|
|
async def verify_email(request: VerifyEmailRequest):
|
|
"""
|
|
Verifiziert die E-Mail-Adresse mit dem Token aus der E-Mail.
|
|
"""
|
|
data = await proxy_to_consent_service(
|
|
"POST",
|
|
"/auth/verify-email",
|
|
json_data={"token": request.token}
|
|
)
|
|
return data
|
|
|
|
|
|
@router.post("/resend-verification")
|
|
async def resend_verification(email: EmailStr):
|
|
"""
|
|
Sendet die Verifizierungs-E-Mail erneut.
|
|
"""
|
|
data = await proxy_to_consent_service(
|
|
"POST",
|
|
"/auth/resend-verification",
|
|
json_data={"email": email}
|
|
)
|
|
return data
|
|
|
|
|
|
@router.post("/forgot-password")
|
|
async def forgot_password(request: ForgotPasswordRequest, req: Request):
|
|
"""
|
|
Initiiert den Passwort-Reset-Prozess.
|
|
Sendet eine E-Mail mit Reset-Link.
|
|
"""
|
|
client_ip = req.client.host if req.client else "unknown"
|
|
|
|
data = await proxy_to_consent_service(
|
|
"POST",
|
|
"/auth/forgot-password",
|
|
json_data={"email": request.email},
|
|
headers={"X-Forwarded-For": client_ip}
|
|
)
|
|
return data
|
|
|
|
|
|
@router.post("/reset-password")
|
|
async def reset_password(request: ResetPasswordRequest):
|
|
"""
|
|
Setzt das Passwort mit dem Token aus der E-Mail zurück.
|
|
"""
|
|
data = await proxy_to_consent_service(
|
|
"POST",
|
|
"/auth/reset-password",
|
|
json_data={
|
|
"token": request.token,
|
|
"new_password": request.new_password
|
|
}
|
|
)
|
|
return data
|
|
|
|
|
|
# ==========================================
|
|
# Protected Profile Endpoints (Auth Required)
|
|
# ==========================================
|
|
|
|
@router.get("/profile")
|
|
async def get_profile(authorization: Optional[str] = Header(None)):
|
|
"""
|
|
Gibt das Profil des angemeldeten Benutzers zurück.
|
|
"""
|
|
if not authorization:
|
|
raise HTTPException(status_code=401, detail="Authorization header required")
|
|
|
|
data = await proxy_to_consent_service(
|
|
"GET",
|
|
"/profile",
|
|
headers=get_auth_headers(authorization)
|
|
)
|
|
return data
|
|
|
|
|
|
@router.put("/profile")
|
|
async def update_profile(
|
|
request: UpdateProfileRequest,
|
|
authorization: Optional[str] = Header(None)
|
|
):
|
|
"""
|
|
Aktualisiert das Profil des angemeldeten Benutzers.
|
|
"""
|
|
if not authorization:
|
|
raise HTTPException(status_code=401, detail="Authorization header required")
|
|
|
|
data = await proxy_to_consent_service(
|
|
"PUT",
|
|
"/profile",
|
|
json_data={"name": request.name},
|
|
headers=get_auth_headers(authorization)
|
|
)
|
|
return data
|
|
|
|
|
|
@router.put("/profile/password")
|
|
async def change_password(
|
|
request: ChangePasswordRequest,
|
|
authorization: Optional[str] = Header(None)
|
|
):
|
|
"""
|
|
Ändert das Passwort des angemeldeten Benutzers.
|
|
"""
|
|
if not authorization:
|
|
raise HTTPException(status_code=401, detail="Authorization header required")
|
|
|
|
data = await proxy_to_consent_service(
|
|
"PUT",
|
|
"/profile/password",
|
|
json_data={
|
|
"current_password": request.current_password,
|
|
"new_password": request.new_password
|
|
},
|
|
headers=get_auth_headers(authorization)
|
|
)
|
|
return data
|
|
|
|
|
|
@router.get("/profile/sessions")
|
|
async def get_sessions(authorization: Optional[str] = Header(None)):
|
|
"""
|
|
Gibt alle aktiven Sessions des Benutzers zurück.
|
|
"""
|
|
if not authorization:
|
|
raise HTTPException(status_code=401, detail="Authorization header required")
|
|
|
|
data = await proxy_to_consent_service(
|
|
"GET",
|
|
"/profile/sessions",
|
|
headers=get_auth_headers(authorization)
|
|
)
|
|
return data
|
|
|
|
|
|
@router.delete("/profile/sessions/{session_id}")
|
|
async def revoke_session(
|
|
session_id: str,
|
|
authorization: Optional[str] = Header(None)
|
|
):
|
|
"""
|
|
Beendet eine bestimmte Session.
|
|
"""
|
|
if not authorization:
|
|
raise HTTPException(status_code=401, detail="Authorization header required")
|
|
|
|
data = await proxy_to_consent_service(
|
|
"DELETE",
|
|
f"/profile/sessions/{session_id}",
|
|
headers=get_auth_headers(authorization)
|
|
)
|
|
return data
|
|
|
|
|
|
# ==========================================
|
|
# Health Check
|
|
# ==========================================
|
|
|
|
@router.get("/health")
|
|
async def auth_health():
|
|
"""
|
|
Prüft die Verbindung zum Auth Service.
|
|
"""
|
|
try:
|
|
async with httpx.AsyncClient() as client:
|
|
response = await client.get(
|
|
f"{CONSENT_SERVICE_URL}/health",
|
|
timeout=5.0
|
|
)
|
|
is_healthy = response.status_code == 200
|
|
except:
|
|
is_healthy = False
|
|
|
|
return {
|
|
"auth_service": "healthy" if is_healthy else "unavailable",
|
|
"connected": is_healthy
|
|
}
|