Initial commit: breakpilot-core - Shared Infrastructure

Docker Compose with 24+ services:
- PostgreSQL (PostGIS), Valkey, MinIO, Qdrant
- Vault (PKI/TLS), Nginx (Reverse Proxy)
- Backend Core API, Consent Service, Billing Service
- RAG Service, Embedding Service
- Gitea, Woodpecker CI/CD
- Night Scheduler, Health Aggregator
- Jitsi (Web/XMPP/JVB/Jicofo), Mailpit

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Boenisch
2026-02-11 23:47:13 +01:00
commit ad111d5e69
244 changed files with 84288 additions and 0 deletions

373
backend-core/auth_api.py Normal file
View File

@@ -0,0 +1,373 @@
"""
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
}