"""JWT auth — DB-backed users, bcrypt password hashing, token + user dependency.""" from __future__ import annotations from datetime import datetime, timedelta, timezone from typing import Annotated import uuid from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer from jose import JWTError, jwt from app import db from app.config import settings from app.security import verify_password oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/v1/admin/auth/login") async def authenticate(username: str, password: str) -> dict | None: """Validate credentials against the DB users table.""" user = await db.get_user_auth(username) if not user: return None if user.get("status") != "active": return None if not verify_password(password, user.get("hashed_password") or ""): return None return { "username": user["username"], "full_name": user.get("full_name"), "roles": list(user.get("roles") or []), } def create_access_token(data: dict) -> str: now = datetime.now(timezone.utc) payload = data.copy() payload.update( { "iss": settings.auth_issuer, "aud": settings.auth_audience, "iat": now, "nbf": now, "jti": str(uuid.uuid4()), "exp": now + timedelta(minutes=settings.auth_token_expire_minutes), } ) return jwt.encode(payload, settings.auth_secret, algorithm=settings.auth_algorithm) async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]) -> dict: """Validate the token and reload status/roles so revocation is immediate.""" credentials_exc = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", headers={"WWW-Authenticate": "Bearer"}, ) try: payload = jwt.decode( token, settings.auth_secret, algorithms=[settings.auth_algorithm], issuer=settings.auth_issuer, audience=settings.auth_audience, ) username: str = payload.get("sub", "") if not username: raise credentials_exc except JWTError: raise credentials_exc user = await db.get_user_auth(username) if not user or user.get("status") != "active": raise credentials_exc return { "username": user["username"], "full_name": user.get("full_name"), "roles": list(user.get("roles") or []), } def require_admin(user: "CurrentUser") -> dict: if "admin" not in set(user.get("roles") or []): raise HTTPException(status_code=403, detail="只有系统管理员可以执行此操作") return user def require_data_viewer(user: "CurrentUser") -> dict: # collector is a retired role key kept as read-only compatibility for # existing accounts. New assignments use admin or operator exclusively. if not ({"admin", "operator", "collector"} & set(user.get("roles") or [])): raise HTTPException( status_code=403, detail="当前账号没有数据查看权限", ) return user CurrentUser = Annotated[dict, Depends(get_current_user)] AdminUser = Annotated[dict, Depends(require_admin)] DataViewerUser = Annotated[dict, Depends(require_data_viewer)]