133 lines
4.0 KiB
Python
133 lines
4.0 KiB
Python
"""
|
||
鉴权依赖注入
|
||
|
||
JWT 验证、当前用户、角色检查。
|
||
"""
|
||
|
||
from enum import Enum
|
||
|
||
from fastapi import Depends, HTTPException, status
|
||
from fastapi.security import OAuth2PasswordBearer
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
from sqlalchemy.future import select
|
||
|
||
from ..core.database import get_db
|
||
from ..dependencies.tenant import get_tenant_id
|
||
from ..models import User, UserStatus # noqa: E402
|
||
from ..services.auth import AuthService, has_any_role
|
||
from ..utils.audited_write import update_audit_context
|
||
|
||
|
||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login")
|
||
oauth2_scheme_optional = OAuth2PasswordBearer(tokenUrl="auth/login", auto_error=False)
|
||
|
||
|
||
async def get_current_user(
|
||
token: str = Depends(oauth2_scheme),
|
||
db: AsyncSession = Depends(get_db),
|
||
) -> User:
|
||
"""从 JWT 解析并返回当前认证用户"""
|
||
credentials_exception = HTTPException(
|
||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||
detail="Could not validate credentials",
|
||
headers={"WWW-Authenticate": "Bearer"},
|
||
)
|
||
|
||
username = AuthService.verify_token(token)
|
||
if username is None:
|
||
raise credentials_exception
|
||
|
||
result = await db.execute(select(User).where(User.username == username))
|
||
user = result.scalar_one_or_none()
|
||
|
||
if user is None:
|
||
raise credentials_exception
|
||
|
||
try:
|
||
update_audit_context(
|
||
caller="user",
|
||
user_id=int(user.id),
|
||
tenant_id=int(getattr(user, "tenant_id", 1) or 1),
|
||
)
|
||
except Exception:
|
||
pass
|
||
|
||
return user
|
||
|
||
|
||
async def get_optional_current_user(
|
||
token: str | None = Depends(oauth2_scheme_optional),
|
||
db: AsyncSession = Depends(get_db),
|
||
) -> User | None:
|
||
"""可选认证用户:无 token 时返回 None,有 token 时按 get_current_user 逻辑校验。"""
|
||
if not token:
|
||
return None
|
||
credentials_exception = HTTPException(
|
||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||
detail="Could not validate credentials",
|
||
headers={"WWW-Authenticate": "Bearer"},
|
||
)
|
||
username = AuthService.verify_token(token)
|
||
if username is None:
|
||
raise credentials_exception
|
||
result = await db.execute(select(User).where(User.username == username))
|
||
user = result.scalar_one_or_none()
|
||
if user is None:
|
||
raise credentials_exception
|
||
try:
|
||
update_audit_context(
|
||
caller="user",
|
||
user_id=int(user.id),
|
||
tenant_id=int(getattr(user, "tenant_id", 1) or 1),
|
||
)
|
||
except Exception:
|
||
pass
|
||
return user
|
||
|
||
|
||
async def get_current_active_user(
|
||
current_user: User = Depends(get_current_user),
|
||
) -> User:
|
||
"""当前活跃用户(排除 AWAY / OFFLINE)"""
|
||
if (not getattr(current_user, "is_active", True)) or current_user.status != UserStatus.ACTIVE:
|
||
raise HTTPException(status_code=403, detail="User is not active")
|
||
return current_user
|
||
|
||
|
||
def require_roles(*roles: str):
|
||
"""依赖工厂:要求用户具备指定角色之一"""
|
||
|
||
async def role_checker(
|
||
current_user: User = Depends(get_current_user),
|
||
) -> User:
|
||
user_role = (
|
||
current_user.role.value
|
||
if isinstance(current_user.role, Enum)
|
||
else current_user.role
|
||
)
|
||
if not has_any_role(user_role, list(roles)):
|
||
raise HTTPException(
|
||
status_code=status.HTTP_403_FORBIDDEN,
|
||
detail=f"Access denied. Required roles: {', '.join(roles)}",
|
||
)
|
||
return current_user
|
||
|
||
return role_checker
|
||
|
||
|
||
async def get_current_tenant_id(
|
||
current_user: User = Depends(get_current_user),
|
||
header_tenant_id: int = Depends(get_tenant_id),
|
||
) -> int:
|
||
"""兼容旧接口:优先取 JWT 中 tenant_id,没有则回落 Header。"""
|
||
try:
|
||
tenant_id = int(getattr(current_user, "tenant_id", 0) or 0)
|
||
if tenant_id > 0:
|
||
update_audit_context(tenant_id=tenant_id)
|
||
return tenant_id
|
||
except (TypeError, ValueError):
|
||
pass
|
||
fallback = int(header_tenant_id or 1)
|
||
update_audit_context(tenant_id=fallback)
|
||
return fallback
|