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