"""AD API client service for authentication 與 Panjit AD API 整合,負責: - 驗證使用者憑證 - 取得 AD token 和使用者名稱 - 處理 API 連線錯誤 """ from datetime import datetime, timedelta import httpx from typing import Dict from app.core.config import get_settings settings = get_settings() class ADAuthService: """Active Directory authentication service""" def __init__(self): self.ad_api_url = settings.AD_API_URL self._client = httpx.AsyncClient(timeout=10.0) async def authenticate(self, username: str, password: str) -> Dict[str, any]: """Authenticate user with AD API Args: username: User email (e.g., ymirliu@panjit.com.tw) password: User password Returns: Dict containing: - token: AD authentication token - username: Display name from AD - expires_at: Estimated token expiry datetime Raises: httpx.HTTPStatusError: If authentication fails (401, 403) httpx.RequestError: If AD API is unreachable """ payload = {"username": username, "password": password} try: response = await self._client.post( self.ad_api_url, json=payload, headers={"Content-Type": "application/json"} ) # Raise exception for 4xx/5xx status codes response.raise_for_status() data = response.json() # Extract token and username from response # Response structure: {"success": true, "data": {"access_token": "...", "userInfo": {"name": "...", "email": "..."}}} if not data.get("success"): raise ValueError("Authentication failed") token_data = data.get("data", {}) ad_token = token_data.get("access_token") user_info = token_data.get("userInfo", {}) display_name = user_info.get("name") or username if not ad_token: raise ValueError("No token received from AD API") # Parse expiry time from response (expiresAt field) expires_at_str = token_data.get("expiresAt") if expires_at_str: # Parse ISO format: "2025-11-16T14:38:37.912Z" try: expires_at = datetime.fromisoformat(expires_at_str.replace("Z", "+00:00")) except: expires_at = datetime.utcnow() + timedelta(hours=1) else: # Fallback: assume 1 hour if not provided expires_at = datetime.utcnow() + timedelta(hours=1) return {"token": ad_token, "username": display_name, "expires_at": expires_at} except httpx.HTTPStatusError as e: # Authentication failed (401) or other HTTP errors if e.response.status_code == 401: raise ValueError("Invalid credentials") from e elif e.response.status_code >= 500: raise ConnectionError("Authentication service error") from e else: raise except httpx.RequestError as e: # Network error, timeout, etc. raise ConnectionError("Authentication service unavailable") from e async def close(self): """Close HTTP client""" await self._client.aclose() # Singleton instance ad_auth_service = ADAuthService()