"""Authentification JWT + hachage PIN.""" from datetime import datetime, timedelta, timezone from jose import jwt, JWTError from fastapi import Depends, HTTPException, status from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select import bcrypt from app.core.config import settings from app.core.database import get_db security = HTTPBearer() def hash_pin(pin: str) -> str: return bcrypt.hashpw(pin.encode("utf-8"), bcrypt.gensalt(rounds=12)).decode("utf-8") def verify_pin(pin: str, hashed: str) -> bool: return bcrypt.checkpw(pin.encode("utf-8"), hashed.encode("utf-8")) def create_token( membre_id: str, groupe_id: str | None, expire_jours: int = None, admin_id: str | None = None, subject_type: str = "membre", ) -> str: jours = expire_jours or settings.JWT_EXPIRE_DAYS expire = datetime.now(timezone.utc) + timedelta(days=jours) payload = {"sub": str(membre_id), "exp": expire} if subject_type != "membre": payload["typ"] = subject_type if groupe_id: payload["grp"] = str(groupe_id) if admin_id: payload["adm"] = str(admin_id) return jwt.encode(payload, settings.JWT_SECRET, algorithm=settings.JWT_ALGORITHM) def decode_token(token: str) -> dict: try: return jwt.decode(token, settings.JWT_SECRET, algorithms=[settings.JWT_ALGORITHM]) except JWTError: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token invalide ou expiré") def is_sysadmin(membre) -> bool: return bool(getattr(membre, "is_sysadmin", False)) async def get_current_membre( credentials: HTTPAuthorizationCredentials = Depends(security), db: AsyncSession = Depends(get_db), ): from app.models.membre import Membre from app.models.instance_admin import InstanceAdmin payload = decode_token(credentials.credentials) membre_id = payload.get("sub") groupe_id = payload.get("grp") if not membre_id: raise HTTPException(status_code=401, detail="Token invalide") if payload.get("typ") == "instance_admin": result = await db.execute(select(InstanceAdmin).where(InstanceAdmin.id == membre_id, InstanceAdmin.actif == True)) admin = result.scalar_one_or_none() if not admin: raise HTTPException(status_code=401, detail="Sysadmin d'instance introuvable ou inactif") return admin result = await db.execute(select(Membre).where(Membre.id == membre_id, Membre.actif == True)) membre = result.scalar_one_or_none() if not membre: raise HTTPException(status_code=401, detail="Membre introuvable ou inactif") if groupe_id and str(membre.groupe_id) != str(groupe_id): raise HTTPException(status_code=401, detail="Session invalide pour ce groupe") if payload.get("adm"): setattr(membre, "_sysadmin_session", True) return membre async def require_executif( membre=Depends(get_current_membre), db: AsyncSession = Depends(get_db), ): if is_sysadmin(membre): return membre from app.models.affectation import Affectation from app.models.poste import Poste result = await db.execute( select(Affectation).join(Poste).where( Affectation.membre_id == membre.id, Affectation.groupe_id == membre.groupe_id, Affectation.statut == "actif", Poste.groupe_id == membre.groupe_id, Poste.categorie == "executif", ) ) if not result.scalars().first(): raise HTTPException(status_code=403, detail="Réservé à l'exécutif") return membre