Files
Cloud-Tour-to-Libo/app/auth.py

102 lines
3.3 KiB
Python

"""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)]