feat: Initial commit - Task Reporter incident response system
Complete implementation of the production line incident response system (生產線異常即時反應系統) including: Backend (FastAPI): - User authentication with AD integration and session management - Chat room management (create, list, update, members, roles) - Real-time messaging via WebSocket (typing indicators, reactions) - File storage with MinIO (upload, download, image preview) Frontend (React + Vite): - Authentication flow with token management - Room list with filtering, search, and pagination - Real-time chat interface with WebSocket - File upload with drag-and-drop and image preview - Member management and room settings - Breadcrumb navigation - 53 unit tests (Vitest) Specifications: - authentication: AD auth, sessions, JWT tokens - chat-room: rooms, members, templates - realtime-messaging: WebSocket, messages, reactions - file-storage: MinIO integration, file management - frontend-core: React SPA structure 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
98
app/modules/auth/services/ad_client.py
Normal file
98
app/modules/auth/services/ad_client.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user