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:
373
backend-core/auth_api.py
Normal file
373
backend-core/auth_api.py
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user