"""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 解出当前 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)]