""" 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 }