from passlib.context import CryptContext from datetime import datetime, timedelta from typing import Optional, Dict import jwt import secrets import hashlib # Configuration SECRET_KEY = "VOTRE_SECRET_KEY_A_METTRE_EN_.ENV" # À remplacer par settings.jwt_secret ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES = 30 REFRESH_TOKEN_EXPIRE_DAYS = 7 pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") # === Hachage de mots de passe === def hash_password(password: str) -> str: """Hash un mot de passe avec bcrypt""" return pwd_context.hash(password) def verify_password(plain_password: str, hashed_password: str) -> bool: """Vérifie un mot de passe contre son hash""" return pwd_context.verify(plain_password, hashed_password) # === Génération de tokens aléatoires === def generate_verification_token() -> str: """Génère un token de vérification email sécurisé""" return secrets.token_urlsafe(32) def generate_reset_token() -> str: """Génère un token de réinitialisation mot de passe""" return secrets.token_urlsafe(32) def hash_token(token: str) -> str: """Hash un refresh token pour stockage en DB""" return hashlib.sha256(token.encode()).hexdigest() # === JWT Access Token === def create_access_token(data: Dict, expires_delta: Optional[timedelta] = None) -> str: """ Crée un JWT access token Args: data: Payload (doit contenir 'sub' = user_id) expires_delta: Durée de validité personnalisée """ to_encode = data.copy() if expires_delta: expire = datetime.utcnow() + expires_delta else: expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) to_encode.update({ "exp": expire, "iat": datetime.utcnow(), "type": "access" }) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt def create_refresh_token(user_id: str) -> str: """ Crée un refresh token (JWT long terme) Returns: Token JWT non hashé (à hasher avant stockage DB) """ expire = datetime.utcnow() + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS) to_encode = { "sub": user_id, "exp": expire, "iat": datetime.utcnow(), "type": "refresh", "jti": secrets.token_urlsafe(16) # Unique ID } encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt def decode_token(token: str) -> Optional[Dict]: """ Décode et valide un JWT Returns: Payload si valide, None sinon """ try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) return payload except jwt.ExpiredSignatureError: return None except jwt.JWTError: return None # === Validation mot de passe === def validate_password_strength(password: str) -> tuple[bool, str]: """ Valide la robustesse d'un mot de passe Returns: (is_valid, error_message) """ if len(password) < 8: return False, "Le mot de passe doit contenir au moins 8 caractères" if not any(c.isupper() for c in password): return False, "Le mot de passe doit contenir au moins une majuscule" if not any(c.islower() for c in password): return False, "Le mot de passe doit contenir au moins une minuscule" if not any(c.isdigit() for c in password): return False, "Le mot de passe doit contenir au moins un chiffre" special_chars = "!@#$%^&*()_+-=[]{}|;:,.<>?" if not any(c in special_chars for c in password): return False, "Le mot de passe doit contenir au moins un caractère spécial" return True, ""