Sage100-vps/core/dependencies.py
2026-01-19 20:38:01 +03:00

213 lines
7.3 KiB
Python

from fastapi import Depends, HTTPException, status, Request
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from database import get_session, User
from security.auth import decode_token
from typing import Optional
from datetime import datetime
import logging
logger = logging.getLogger(__name__)
security = HTTPBearer(auto_error=False) # ⚠️ auto_error=False pour gérer manuellement
async def get_current_user_hybrid(
request: Request,
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
session: AsyncSession = Depends(get_session),
) -> User:
"""
VERSION HYBRIDE: Accepte JWT OU API Key
Priorité:
1. JWT (Authorization: Bearer)
2. API Key (déjà validée par middleware)
Si API Key utilisée, retourne un "user virtuel" basé sur la clé
"""
# ============================================
# OPTION 1: JWT (comportement standard)
# ============================================
if credentials and credentials.credentials:
token = credentials.credentials
payload = decode_token(token)
if not payload:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token invalide ou expiré",
headers={"WWW-Authenticate": "Bearer"},
)
if payload.get("type") != "access":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Type de token incorrect",
headers={"WWW-Authenticate": "Bearer"},
)
user_id: str = payload.get("sub")
if not user_id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token malformé",
headers={"WWW-Authenticate": "Bearer"},
)
result = await session.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Utilisateur introuvable",
headers={"WWW-Authenticate": "Bearer"},
)
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail="Compte désactivé"
)
if not user.is_verified:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Email non vérifié. Consultez votre boîte de réception.",
)
if user.locked_until and user.locked_until > datetime.now():
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Compte temporairement verrouillé suite à trop de tentatives échouées",
)
logger.debug(f"🔐 Authentifié via JWT: {user.email}")
return user
# ============================================
# OPTION 2: API Key (validée par middleware)
# ============================================
api_key_obj = getattr(request.state, "api_key", None)
if api_key_obj:
# Créer un "user virtuel" basé sur la clé API
# Cela permet aux routes existantes de fonctionner sans modification
# Si la clé est associée à un vrai user, le récupérer
if api_key_obj.user_id:
result = await session.execute(
select(User).where(User.id == api_key_obj.user_id)
)
user = result.scalar_one_or_none()
if user:
logger.debug(
f"🔑 Authentifié via API Key ({api_key_obj.name}) → User: {user.email}"
)
return user
# Sinon, créer un user virtuel (pour les clés API sans user associé)
from database import User as UserModel
virtual_user = UserModel(
id=f"api_key_{api_key_obj.id}",
email=f"{api_key_obj.name.lower().replace(' ', '_')}@api-key.local",
nom="API Key",
prenom=api_key_obj.name,
role="api_client", # Rôle spécial pour les API keys
is_verified=True,
is_active=True,
)
# Marquer que c'est un user virtuel
virtual_user._is_api_key_user = True
virtual_user._api_key_obj = api_key_obj
logger.debug(f"🔑 Authentifié via API Key: {api_key_obj.name} (user virtuel)")
return virtual_user
# ============================================
# AUCUNE AUTHENTIFICATION
# ============================================
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Authentification requise (JWT ou API Key)",
headers={"WWW-Authenticate": "Bearer"},
)
async def get_current_user_optional_hybrid(
request: Request,
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
session: AsyncSession = Depends(get_session),
) -> Optional[User]:
"""Version optionnelle de get_current_user_hybrid (ne lève pas d'erreur)"""
try:
return await get_current_user_hybrid(request, credentials, session)
except HTTPException:
return None
def require_role_hybrid(*allowed_roles: str):
"""
VERSION HYBRIDE: Vérification de rôle compatible avec API Keys
Notes:
- Les users via JWT ont leur vrai rôle
- Les users via API Key ont le rôle "api_client" par défaut
- Vous pouvez autoriser "api_client" dans allowed_roles pour permettre l'accès aux API Keys
"""
async def role_checker(
request: Request, user: User = Depends(get_current_user_hybrid)
) -> User:
# Vérifier si c'est un user API Key
is_api_key_user = getattr(user, "_is_api_key_user", False)
if is_api_key_user:
# Pour les API Keys, vérifier si "api_client" est autorisé
if "api_client" not in allowed_roles and "*" not in allowed_roles:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Accès refusé. Rôles requis: {', '.join(allowed_roles)}. API Keys non autorisées.",
)
logger.debug(f"✅ API Key autorisée pour cette route")
return user
# Pour les vrais users, vérification standard
if user.role not in allowed_roles and "*" not in allowed_roles:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Accès refusé. Rôles requis: {', '.join(allowed_roles)}",
)
return user
return role_checker
# ============================================
# HELPERS
# ============================================
def is_api_key_user(user: User) -> bool:
"""Vérifie si l'utilisateur est authentifié via API Key"""
return getattr(user, "_is_api_key_user", False)
def get_api_key_from_user(user: User):
"""Récupère l'objet API Key depuis un user virtuel"""
return getattr(user, "_api_key_obj", None)
# ============================================
# RÉTROCOMPATIBILITÉ
# ============================================
# Alias pour garder la compatibilité avec le code existant
get_current_user = get_current_user_hybrid
get_current_user_optional = get_current_user_optional_hybrid