"""Session login for the web UI. A single shared password (AGENTEVAL_ADMIN_PASSWORD) gates the SPA. On success the client receives a stateless HMAC token it sends as X-Auth-Token. Machine callers (e.g. the OpenClaw skill) keep using X-API-Key and are unaffected. """ import hmac import secrets from fastapi import APIRouter, Header, HTTPException, status from pydantic import BaseModel from agenteval.config import get_settings router = APIRouter() _TOKEN_CONTEXT = b"agenteval-web-session" def session_token() -> str: """Derive the stateless session token from configured secrets.""" settings = get_settings() key = (settings.secret_key or settings.admin_password or "").encode() return hmac.new(key, _TOKEN_CONTEXT, "sha256").hexdigest() def verify_session_token(token: str) -> bool: return bool(token) and hmac.compare_digest(token, session_token()) class LoginRequest(BaseModel): password: str @router.post("/login") def login(request: LoginRequest) -> dict: configured = get_settings().admin_password if not configured: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="登录未启用") if not secrets.compare_digest(request.password, configured): raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="密码错误") return {"token": session_token()} @router.get("/status") def auth_status(x_auth_token: str = Header(default="")) -> dict: auth_required = bool(get_settings().admin_password) return { "auth_required": auth_required, "authenticated": (not auth_required) or verify_session_token(x_auth_token), }