This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/backend/auth_api.py
BreakPilot Dev 19855efacc
Some checks failed
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
ci/woodpecker/manual/build-ci-image Pipeline was successful
ci/woodpecker/manual/main Pipeline failed
feat: BreakPilot PWA - Full codebase (clean push without large binaries)
All services: admin-v2, studio-v2, website, ai-compliance-sdk,
consent-service, klausur-service, voice-service, and infrastructure.
Large PDFs and compiled binaries excluded via .gitignore.
2026-02-11 13:25:58 +01:00

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
}