Files
Dragonbank/DragonBank/backend/models/utilisateur.py
T

244 lines
8.2 KiB
Python
Raw Normal View History

2026-03-27 17:52:41 +01:00
"""
DragonBank - Modele Utilisateur
================================
Classe DAO (Data Access Object) gerant toutes les operations
en base de donnees relatives aux utilisateurs.
Pattern DAO : separe la logique d'acces aux donnees
de la logique metier et des routes Flask.
Version : 3.0
"""
import uuid
import logging
from database import serialiser_ligne, enregistrer_audit
from auth import hacher_mot_de_passe, verifier_mot_de_passe, simuler_verification_bcrypt
journaliseur = logging.getLogger('dragonbank.utilisateur')
class UtilisateurDAO:
"""
Objet d'acces aux donnees pour la table users.
Chaque methode recoit une connexion active en parametre.
La gestion des transactions (commit/rollback) reste
a la charge de la route appelante.
"""
# =========================================================
# CREATION
# =========================================================
def creer(self, connexion, email, mot_de_passe, prenom, nom,
telephone=None, adresse=None, date_naissance=None):
"""
Cree un nouvel utilisateur en base de donnees.
Args:
connexion : Connexion PostgreSQL active.
email (str) : Adresse email (deja validee et normalisee).
mot_de_passe (str): Mot de passe en clair (sera hache ici).
prenom (str) : Prenom de l'utilisateur.
nom (str) : Nom de famille.
telephone (str): Numero de telephone (optionnel).
adresse (str) : Adresse postale (optionnel).
date_naissance : Date de naissance au format YYYY-MM-DD (optionnel).
Returns:
dict: Les donnees de l'utilisateur cree (sans le hash du mot de passe).
Raises:
Exception: Si l'insertion echoue (email duplique, etc.).
"""
id_utilisateur = str(uuid.uuid4())
hash_mdp = hacher_mot_de_passe(mot_de_passe)
curseur = connexion.cursor()
curseur.execute(
"""
INSERT INTO users
(id, email, password_hash, first_name, last_name,
phone, address, date_of_birth)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
RETURNING id, email, first_name, last_name, created_at
""",
(
id_utilisateur, email, hash_mdp,
prenom, nom,
telephone or None,
adresse or None,
date_naissance or None
)
)
utilisateur = curseur.fetchone()
journaliseur.info("Nouvel utilisateur cree : %s", email)
return serialiser_ligne(utilisateur)
# =========================================================
# LECTURE
# =========================================================
def trouver_par_email(self, connexion, email):
"""
Recherche un utilisateur par son adresse email.
Args:
connexion : Connexion PostgreSQL active.
email (str): Email a rechercher.
Returns:
dict : Donnees de l'utilisateur (avec hash) si trouve.
None : Si aucun utilisateur ne correspond.
"""
curseur = connexion.cursor()
curseur.execute(
"""
SELECT id, email, password_hash, first_name, last_name, is_active
FROM users
WHERE email = %s
""",
(email,)
)
return curseur.fetchone()
def trouver_par_id(self, connexion, id_utilisateur):
"""
Recupere le profil complet d'un utilisateur actif par son UUID.
N'inclut pas le hash du mot de passe dans le resultat.
Args:
connexion : Connexion PostgreSQL active.
id_utilisateur : UUID de l'utilisateur.
Returns:
dict : Profil de l'utilisateur.
None : Si introuvable ou desactive.
"""
curseur = connexion.cursor()
curseur.execute(
"""
SELECT id, email, first_name, last_name, phone, address,
date_of_birth, created_at, last_login
FROM users
WHERE id = %s AND is_active = TRUE
""",
(id_utilisateur,)
)
return serialiser_ligne(curseur.fetchone())
def email_existe(self, connexion, email):
"""
Verifie si une adresse email est deja utilisee.
Args:
connexion : Connexion PostgreSQL active.
email (str): Email a verifier.
Returns:
bool: True si l'email existe deja, False sinon.
"""
curseur = connexion.cursor()
curseur.execute('SELECT id FROM users WHERE email = %s', (email,))
return curseur.fetchone() is not None
# =========================================================
# MISE A JOUR
# =========================================================
def mettre_a_jour_derniere_connexion(self, connexion, id_utilisateur):
"""
Met a jour la date de derniere connexion de l'utilisateur.
Args:
connexion : Connexion PostgreSQL active.
id_utilisateur : UUID de l'utilisateur.
"""
curseur = connexion.cursor()
curseur.execute(
'UPDATE users SET last_login = NOW() WHERE id = %s',
(str(id_utilisateur),)
)
def mettre_a_jour_profil(self, connexion, id_utilisateur, champs):
"""
Met a jour les champs modifiables du profil utilisateur.
Seuls les champs fournis en parametre sont modifies.
Les champs sensibles (email, mot de passe) sont ignores.
Args:
connexion : Connexion PostgreSQL active.
id_utilisateur : UUID de l'utilisateur.
champs (dict) : Dictionnaire {nom_champ: nouvelle_valeur}.
Returns:
dict: Profil mis a jour.
"""
clause_set = ', '.join(champ + ' = %s' for champ in champs)
valeurs = list(champs.values())
valeurs.append(id_utilisateur)
curseur = connexion.cursor()
curseur.execute(
'UPDATE users SET ' + clause_set
+ ' WHERE id = %s'
' RETURNING id, email, first_name, last_name, phone, address',
valeurs
)
return serialiser_ligne(curseur.fetchone())
def mettre_a_jour_mot_de_passe(self, connexion, id_utilisateur, nouveau_hash):
"""Met a jour le l'empreinte bcrypt de l'utilisateur."""
curseur = connexion.cursor()
curseur.execute('UPDATE users SET password_hash = %s WHERE id = %s', (nouveau_hash, id_utilisateur))
def mettre_a_jour_email(self, connexion, id_utilisateur, nouvel_email):
"""Met a jour l'adresse e-mail."""
curseur = connexion.cursor()
curseur.execute('UPDATE users SET email = %s WHERE id = %s', (nouvel_email, id_utilisateur))
# =========================================================
# AUTHENTIFICATION
# =========================================================
def authentifier(self, connexion, email, mot_de_passe):
"""
Verifie les identifiants d'un utilisateur.
Gere la protection contre les attaques temporelles :
si l'email est inconnu, une verification bcrypt fictive est
effectuee pour uniformiser le temps de reponse.
Args:
connexion : Connexion PostgreSQL active.
email (str) : Email soumis.
mot_de_passe (str) : Mot de passe en clair soumis.
Returns:
dict : Donnees de l'utilisateur si authentification reussie.
None : Si les identifiants sont incorrects.
Raises:
PermissionError: Si le compte est desactive.
"""
utilisateur = self.trouver_par_email(connexion, email)
if not utilisateur:
# On simule le temps bcrypt pour eviter la timing attack
simuler_verification_bcrypt()
return None
if not utilisateur['is_active']:
raise PermissionError("Ce compte a ete desactive. Contactez le support.")
if not verifier_mot_de_passe(mot_de_passe, utilisateur['password_hash']):
journaliseur.warning("Echec de connexion pour : %s", email)
return None
return utilisateur