from fastapi import Depends, HTTPException, status from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.models.base import async_session from app.models.user import User from app.services.auth import decode_access_token, user_must_set_password from app.services.credit.query_service import attach_credit_snapshot, get_available_credits from app.services.system_config_cache import get_system_config_value security = HTTPBearer(auto_error=False) async def get_db(): async with async_session() as session: try: yield session await session.commit() except Exception: await session.rollback() raise finally: await session.close() async def get_current_user_allow_password_pending( credentials: HTTPAuthorizationCredentials | None = Depends(security), db: AsyncSession = Depends(get_db), ) -> User: if not credentials: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="未登录", ) payload = decode_access_token(credentials.credentials) if not payload: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="登录已过期", ) user_id = payload.get("sub") token_version = payload.get("ver", 0) device_type = payload.get("dev", "pc") # Skip captcha tokens if user_id and user_id.startswith("captcha:"): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="无效的凭证", ) result = await db.execute(select(User).where(User.id == user_id).limit(1)) user = result.scalar_one_or_none() if not user or not user.is_active: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="账号不存在或已禁用", ) # 单设备登录校验 — 根据全局开关 + 用户级覆盖决定是否启用 override = getattr(user, "single_device_login_override", None) if override is True: enabled = True elif override is False: enabled = False else: # 跟随全局设置 config_val = await get_system_config_value(db, "single_device_login_enabled") enabled = config_val is not None and config_val.lower() in ("true", "1", "yes") if enabled: # 按设备类型比对对应版本号 if device_type == "mobile": current_version = user.mobile_token_version else: current_version = user.pc_token_version if token_version != current_version: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="账号已在其他设备登录,请重新登录", ) attach_credit_snapshot(user, await get_available_credits(db, user.id)) return user async def get_current_user( current_user: User = Depends(get_current_user_allow_password_pending), ) -> User: if user_must_set_password(current_user): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail={ "code": "PASSWORD_REQUIRED", "message": "请先设置登录密码", }, ) return current_user async def get_optional_current_user( credentials: HTTPAuthorizationCredentials | None = Depends(security), db: AsyncSession = Depends(get_db), ) -> User | None: if not credentials: return None user_id = decode_access_token(credentials.credentials) if not user_id: return None if user_id.startswith("captcha:"): return None result = await db.execute(select(User).where(User.id == user_id).limit(1)) user = result.scalar_one_or_none() if not user or not user.is_active: return None if user_must_set_password(user): return None attach_credit_snapshot(user, await get_available_credits(db, user.id)) return user async def get_admin_user( current_user: User = Depends(get_current_user_allow_password_pending), ) -> User: """验证后台用户权限。 - user_type="admin" 的后台用户即可通过(含非管理员子账号) - 前端通过 allowed_menus 控制非管理员子账号的菜单可见性 - 非后台用户: 403 拒绝 """ if current_user.user_type != "admin": raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="需要后台用户权限", ) return current_user async def get_backend_user( current_user: User = Depends(get_current_user_allow_password_pending), ) -> User: """与 get_admin_user 等价: 验证 user_type="admin" 的后台用户。""" if current_user.user_type != "admin": raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="需要后台用户权限", ) return current_user