Files

83 lines
3.0 KiB
Python

"""API 层共享依赖:DB session、当前登录用户。"""
from __future__ import annotations
import logging
from typing import Annotated
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from sqlalchemy.orm import Session
from app.core.security import TokenError, decode_token
from app.db.session import get_db
from app.models.user import User
logger = logging.getLogger("shagua.deps")
# auto_error=True → 没传 Authorization header 时直接 403。但我们想 401,
# 所以手动 auto_error=False + raise HTTPException
_bearer = HTTPBearer(auto_error=False, scheme_name="Bearer")
def get_current_user(
credentials: Annotated[HTTPAuthorizationCredentials | None, Depends(_bearer)],
db: Annotated[Session, Depends(get_db)],
) -> User:
"""从 Authorization: Bearer <access_token> 解出当前 user。
失败时统一 401,WWW-Authenticate 头让客户端知道要重新登录。
"""
if credentials is None or credentials.scheme.lower() != "bearer":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="missing bearer token",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = decode_token(credentials.credentials, expected_type="access")
except TokenError as e:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=str(e),
headers={"WWW-Authenticate": "Bearer"},
) from e
user_id = int(payload["sub"])
user = db.get(User, user_id)
if user is None or user.status != "active":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="user not found or disabled",
headers={"WWW-Authenticate": "Bearer"},
)
return user
def get_current_user_optional(
credentials: Annotated[HTTPAuthorizationCredentials | None, Depends(_bearer)],
db: Annotated[Session, Depends(get_db)],
) -> User | None:
"""软鉴权:有合法 Bearer 就返回 user,否则(无 header / 无效 token / 用户禁用)一律返回
None,**不 raise**。
比价 step / finalize 灰度期用:新客户端带 JWT → 拿到 user_id 绑定 harvest 行;
老客户端(不带 JWT)→ None,harvest 行 user_id 暂空,由其后续 /compare/record 上报补齐。
等新版覆盖率够高,再把这几条端点从软鉴权收紧成硬 get_current_user。
"""
if credentials is None or credentials.scheme.lower() != "bearer":
return None
try:
payload = decode_token(credentials.credentials, expected_type="access")
user = db.get(User, int(payload["sub"]))
except (TokenError, KeyError, ValueError, TypeError):
return None
if user is None or user.status != "active":
return None
return user
CurrentUser = Annotated[User, Depends(get_current_user)]
OptionalUser = Annotated[User | None, Depends(get_current_user_optional)]
DbSession = Annotated[Session, Depends(get_db)]