groupe-meditation/backend/app/core/security.py

101 lines
3.6 KiB
Python

"""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