maj
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
"""
|
||||
DragonBank - Modele Beneficiaire
|
||||
==================================
|
||||
Classe DAO gerant les operations en base de donnees
|
||||
relatives aux beneficiaires de virements.
|
||||
|
||||
Version : 3.0
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from database import serialiser_ligne, serialiser_lignes
|
||||
|
||||
journaliseur = logging.getLogger('dragonbank.beneficiaire')
|
||||
|
||||
|
||||
class BeneficiaireDAO:
|
||||
"""
|
||||
Objet d'acces aux donnees pour la table beneficiaries.
|
||||
|
||||
Applique les regles metier :
|
||||
- Interdiction de s'ajouter soi-meme comme beneficiaire.
|
||||
- Unicite du numero de compte par utilisateur.
|
||||
"""
|
||||
|
||||
# =========================================================
|
||||
# LECTURE
|
||||
# =========================================================
|
||||
|
||||
def lister_par_utilisateur(self, connexion, id_utilisateur):
|
||||
"""
|
||||
Retourne tous les beneficiaires enregistres par un utilisateur.
|
||||
|
||||
Args:
|
||||
connexion : Connexion PostgreSQL active.
|
||||
id_utilisateur : UUID de l'utilisateur.
|
||||
|
||||
Returns:
|
||||
list[dict]: Liste des beneficiaires tries alphabetiquement.
|
||||
"""
|
||||
curseur = connexion.cursor()
|
||||
curseur.execute(
|
||||
"""
|
||||
SELECT id, beneficiary_name, bank_name, account_number,
|
||||
iban, bic, status, created_at
|
||||
FROM beneficiaries
|
||||
WHERE user_id = %s
|
||||
ORDER BY beneficiary_name ASC
|
||||
""",
|
||||
(id_utilisateur,)
|
||||
)
|
||||
return serialiser_lignes(curseur.fetchall())
|
||||
|
||||
def trouver_approuve(self, connexion, id_beneficiaire, id_utilisateur):
|
||||
"""
|
||||
Recupere un beneficiaire approuve appartenant a l'utilisateur.
|
||||
|
||||
Utilise lors des virements pour verifier que le beneficiaire
|
||||
est bien enregistre et approuve.
|
||||
|
||||
Args:
|
||||
connexion : Connexion PostgreSQL active.
|
||||
id_beneficiaire : UUID du beneficiaire.
|
||||
id_utilisateur : UUID de l'utilisateur proprietaire.
|
||||
|
||||
Returns:
|
||||
dict : Donnees du beneficiaire.
|
||||
None : Si introuvable, non approuve ou acces refuse.
|
||||
"""
|
||||
curseur = connexion.cursor()
|
||||
curseur.execute(
|
||||
"""
|
||||
SELECT id, beneficiary_name, account_number, bank_name
|
||||
FROM beneficiaries
|
||||
WHERE id = %s AND user_id = %s AND status = 'approved'
|
||||
""",
|
||||
(id_beneficiaire, id_utilisateur)
|
||||
)
|
||||
return curseur.fetchone()
|
||||
|
||||
# =========================================================
|
||||
# CREATION
|
||||
# =========================================================
|
||||
|
||||
def ajouter(self, connexion, id_utilisateur, nom, numero_compte,
|
||||
nom_banque='DragonBank', iban=None, bic=None):
|
||||
"""
|
||||
Enregistre un nouveau beneficiaire.
|
||||
|
||||
Verifie que l'utilisateur ne s'ajoute pas lui-meme
|
||||
et qu'il n'y a pas de doublon sur le numero de compte.
|
||||
|
||||
Args:
|
||||
connexion : Connexion PostgreSQL active.
|
||||
id_utilisateur : UUID de l'utilisateur.
|
||||
nom (str) : Nom du beneficiaire.
|
||||
numero_compte (str): Numero de compte du beneficiaire.
|
||||
nom_banque (str) : Nom de la banque (defaut: DragonBank).
|
||||
iban (str) : Code IBAN (optionnel).
|
||||
bic (str) : Code BIC/SWIFT (optionnel).
|
||||
|
||||
Returns:
|
||||
dict: Les donnees du beneficiaire cree.
|
||||
|
||||
Raises:
|
||||
ValueError: Si auto-ajout ou doublon detecte.
|
||||
"""
|
||||
# Interdiction de s'ajouter soi-meme
|
||||
if self._est_propre_compte(connexion, id_utilisateur, numero_compte):
|
||||
raise ValueError(
|
||||
"Vous ne pouvez pas vous ajouter vous-meme comme beneficiaire"
|
||||
)
|
||||
|
||||
# Verification de l'absence de doublon
|
||||
if self._existe_deja(connexion, id_utilisateur, numero_compte):
|
||||
raise ValueError("Ce beneficiaire est deja enregistre")
|
||||
|
||||
curseur = connexion.cursor()
|
||||
curseur.execute(
|
||||
"""
|
||||
INSERT INTO beneficiaries
|
||||
(user_id, beneficiary_name, bank_name, account_number, iban, bic)
|
||||
VALUES (%s, %s, %s, %s, %s, %s)
|
||||
RETURNING id, beneficiary_name, bank_name, account_number,
|
||||
iban, bic, status, created_at
|
||||
""",
|
||||
(id_utilisateur, nom, nom_banque, numero_compte,
|
||||
iban or None, bic or None)
|
||||
)
|
||||
journaliseur.info("Beneficiaire '%s' ajoute pour l'utilisateur %s", nom, id_utilisateur)
|
||||
return serialiser_ligne(curseur.fetchone())
|
||||
|
||||
# =========================================================
|
||||
# SUPPRESSION
|
||||
# =========================================================
|
||||
|
||||
def supprimer(self, connexion, id_beneficiaire, id_utilisateur):
|
||||
"""
|
||||
Supprime un beneficiaire en verifiant la propriete.
|
||||
|
||||
Args:
|
||||
connexion : Connexion PostgreSQL active.
|
||||
id_beneficiaire : UUID du beneficiaire.
|
||||
id_utilisateur : UUID de l'utilisateur proprietaire.
|
||||
|
||||
Returns:
|
||||
dict : Donnees du beneficiaire supprime (nom inclus).
|
||||
None : Si introuvable ou acces refuse.
|
||||
"""
|
||||
curseur = connexion.cursor()
|
||||
curseur.execute(
|
||||
'DELETE FROM beneficiaries WHERE id = %s AND user_id = %s'
|
||||
' RETURNING id, beneficiary_name',
|
||||
(id_beneficiaire, id_utilisateur)
|
||||
)
|
||||
return curseur.fetchone()
|
||||
|
||||
# =========================================================
|
||||
# UTILITAIRES PRIVES
|
||||
# =========================================================
|
||||
|
||||
def _est_propre_compte(self, connexion, id_utilisateur, numero_compte):
|
||||
"""
|
||||
Verifie si le numero correspond a un compte de l'utilisateur.
|
||||
|
||||
Args:
|
||||
connexion : Connexion PostgreSQL active.
|
||||
id_utilisateur : UUID de l'utilisateur.
|
||||
numero_compte : Numero a verifier.
|
||||
|
||||
Returns:
|
||||
bool: True si c'est l'un de ses propres comptes.
|
||||
"""
|
||||
curseur = connexion.cursor()
|
||||
curseur.execute(
|
||||
'SELECT id FROM accounts WHERE user_id = %s AND account_number = %s',
|
||||
(id_utilisateur, numero_compte)
|
||||
)
|
||||
return curseur.fetchone() is not None
|
||||
|
||||
def _existe_deja(self, connexion, id_utilisateur, numero_compte):
|
||||
"""
|
||||
Verifie si un beneficiaire avec ce numero est deja enregistre.
|
||||
|
||||
Args:
|
||||
connexion : Connexion PostgreSQL active.
|
||||
id_utilisateur : UUID de l'utilisateur.
|
||||
numero_compte : Numero a verifier.
|
||||
|
||||
Returns:
|
||||
bool: True si le beneficiaire existe deja.
|
||||
"""
|
||||
curseur = connexion.cursor()
|
||||
curseur.execute(
|
||||
'SELECT id FROM beneficiaries WHERE user_id = %s AND account_number = %s',
|
||||
(id_utilisateur, numero_compte)
|
||||
)
|
||||
return curseur.fetchone() is not None
|
||||
@@ -0,0 +1,264 @@
|
||||
"""
|
||||
DragonBank - Modele Compte Bancaire
|
||||
=====================================
|
||||
Classe DAO gerant toutes les operations en base de donnees
|
||||
relatives aux comptes bancaires (courant, livret A, assurance vie).
|
||||
|
||||
Version : 3.0
|
||||
"""
|
||||
|
||||
import uuid
|
||||
import decimal
|
||||
import logging
|
||||
|
||||
from database import serialiser_ligne, serialiser_lignes, enregistrer_audit
|
||||
from config import TAUX_INTERETS, TYPES_COMPTE_UNIQUES
|
||||
|
||||
journaliseur = logging.getLogger('dragonbank.compte')
|
||||
|
||||
|
||||
class CompteDAO:
|
||||
"""
|
||||
Objet d'acces aux donnees pour la table accounts.
|
||||
|
||||
Encapsule toutes les requetes SQL liees aux comptes bancaires
|
||||
et applique les regles metier (unicite du Livret A, validation
|
||||
du solde avant virement...).
|
||||
"""
|
||||
|
||||
# =========================================================
|
||||
# LECTURE
|
||||
# =========================================================
|
||||
|
||||
def lister_par_utilisateur(self, connexion, id_utilisateur):
|
||||
"""
|
||||
Retourne tous les comptes actifs d'un utilisateur.
|
||||
|
||||
Args:
|
||||
connexion : Connexion PostgreSQL active.
|
||||
id_utilisateur : UUID de l'utilisateur.
|
||||
|
||||
Returns:
|
||||
list[dict]: Liste des comptes tries par date de creation.
|
||||
"""
|
||||
curseur = connexion.cursor()
|
||||
curseur.execute(
|
||||
"""
|
||||
SELECT id, account_number, account_type, balance, currency,
|
||||
status, interest_rate, created_at
|
||||
FROM accounts
|
||||
WHERE user_id = %s AND status = 'active'
|
||||
ORDER BY created_at ASC
|
||||
""",
|
||||
(id_utilisateur,)
|
||||
)
|
||||
return serialiser_lignes(curseur.fetchall())
|
||||
|
||||
def trouver_par_id(self, connexion, id_compte, id_utilisateur):
|
||||
"""
|
||||
Recupere un compte par son UUID en verifiant la propriete.
|
||||
|
||||
Args:
|
||||
connexion : Connexion PostgreSQL active.
|
||||
id_compte : UUID du compte.
|
||||
id_utilisateur : UUID du proprietaire attendu.
|
||||
|
||||
Returns:
|
||||
dict : Donnees du compte.
|
||||
None : Si introuvable ou n'appartient pas a l'utilisateur.
|
||||
"""
|
||||
curseur = connexion.cursor()
|
||||
curseur.execute(
|
||||
"""
|
||||
SELECT id, account_number, account_type, balance, currency,
|
||||
status, interest_rate, created_at, updated_at
|
||||
FROM accounts
|
||||
WHERE id = %s AND user_id = %s
|
||||
""",
|
||||
(id_compte, id_utilisateur)
|
||||
)
|
||||
return serialiser_ligne(curseur.fetchone())
|
||||
|
||||
def trouver_actif(self, connexion, id_compte, id_utilisateur):
|
||||
"""
|
||||
Recupere un compte actif en verifiant la propriete.
|
||||
|
||||
Utilise lors des virements pour s'assurer que le compte
|
||||
est actif (ni ferme ni gele) et appartient a l'utilisateur.
|
||||
|
||||
Args:
|
||||
connexion : Connexion PostgreSQL active.
|
||||
id_compte : UUID du compte.
|
||||
id_utilisateur : UUID du proprietaire.
|
||||
|
||||
Returns:
|
||||
dict : Donnees du compte actif.
|
||||
None : Si introuvable, inactif ou acces refuse.
|
||||
"""
|
||||
curseur = connexion.cursor()
|
||||
curseur.execute(
|
||||
"""
|
||||
SELECT id, balance, account_number, account_type
|
||||
FROM accounts
|
||||
WHERE id = %s AND user_id = %s AND status = 'active'
|
||||
""",
|
||||
(id_compte, id_utilisateur)
|
||||
)
|
||||
return curseur.fetchone()
|
||||
|
||||
def trouver_par_numero(self, connexion, numero_compte):
|
||||
"""
|
||||
Recherche un compte actif par son numero de compte.
|
||||
|
||||
Utilise lors des virements vers un beneficiaire pour trouver
|
||||
le compte destination a partir du numero enregistre.
|
||||
|
||||
Args:
|
||||
connexion : Connexion PostgreSQL active.
|
||||
numero_compte (str): Numero de compte (format DRGxxxxx).
|
||||
|
||||
Returns:
|
||||
dict : Donnees du compte.
|
||||
None : Si introuvable ou inactif.
|
||||
"""
|
||||
curseur = connexion.cursor()
|
||||
curseur.execute(
|
||||
"SELECT id FROM accounts WHERE account_number = %s AND status = 'active'",
|
||||
(numero_compte,)
|
||||
)
|
||||
return curseur.fetchone()
|
||||
|
||||
# =========================================================
|
||||
# CREATION
|
||||
# =========================================================
|
||||
|
||||
def ouvrir(self, connexion, id_utilisateur, type_compte, depot_initial=None):
|
||||
"""
|
||||
Ouvre un nouveau compte bancaire.
|
||||
|
||||
Applique les regles metier :
|
||||
- Unicite du Livret A et de l'Assurance Vie.
|
||||
- Enregistrement du depot initial comme transaction si > 0.
|
||||
|
||||
Args:
|
||||
connexion : Connexion PostgreSQL active.
|
||||
id_utilisateur : UUID de l'utilisateur.
|
||||
type_compte (str): 'courant', 'livret_a' ou 'assurance_vie'.
|
||||
depot_initial : Montant en euros (Decimal ou None).
|
||||
|
||||
Returns:
|
||||
dict: Les donnees du compte cree.
|
||||
|
||||
Raises:
|
||||
ValueError: Si le type est invalide ou si un compte unique existe deja.
|
||||
"""
|
||||
if type_compte not in TAUX_INTERETS:
|
||||
raise ValueError(
|
||||
"Type de compte invalide. Valeurs acceptees : "
|
||||
+ ", ".join(TAUX_INTERETS.keys())
|
||||
)
|
||||
|
||||
if type_compte in TYPES_COMPTE_UNIQUES:
|
||||
if self._existe_compte_unique(connexion, id_utilisateur, type_compte):
|
||||
raise ValueError("Vous possedez deja un compte " + type_compte)
|
||||
|
||||
if depot_initial is None:
|
||||
depot_initial = decimal.Decimal('0.00')
|
||||
|
||||
# Bonus d'ouverture de 100e uniquement pour le compte courant
|
||||
if type_compte == 'courant':
|
||||
depot_initial += decimal.Decimal('100.00')
|
||||
|
||||
taux = float(TAUX_INTERETS[type_compte])
|
||||
numero = 'DRG' + str(uuid.uuid4().int)[:13].zfill(13)
|
||||
|
||||
curseur = connexion.cursor()
|
||||
curseur.execute(
|
||||
"""
|
||||
INSERT INTO accounts
|
||||
(user_id, account_number, account_type, balance, interest_rate)
|
||||
VALUES (%s, %s, %s, %s, %s)
|
||||
RETURNING id, account_number, account_type, balance, interest_rate, created_at
|
||||
""",
|
||||
(id_utilisateur, numero, type_compte, float(depot_initial), taux)
|
||||
)
|
||||
compte = curseur.fetchone()
|
||||
|
||||
# Enregistrement du depot initial et bonus comme transaction tracable
|
||||
if depot_initial > 0:
|
||||
description_depot = "Depot initial a l'ouverture du compte"
|
||||
if type_compte == 'courant':
|
||||
description_depot += " (incluant 100e de bonus)"
|
||||
|
||||
curseur.execute(
|
||||
"""
|
||||
INSERT INTO transactions
|
||||
(to_account_id, transaction_type, amount, description, status, executed_at)
|
||||
VALUES (%s, 'depot', %s, %s, 'completed', NOW())
|
||||
""",
|
||||
(str(compte['id']), float(depot_initial), description_depot)
|
||||
)
|
||||
|
||||
journaliseur.info("Compte %s ouvert pour l'utilisateur %s", type_compte, id_utilisateur)
|
||||
return serialiser_ligne(compte)
|
||||
|
||||
# =========================================================
|
||||
# MISE A JOUR DU SOLDE
|
||||
# =========================================================
|
||||
|
||||
def debiter(self, connexion, id_compte, montant):
|
||||
"""
|
||||
Deduit un montant du solde d'un compte.
|
||||
|
||||
Args:
|
||||
connexion : Connexion PostgreSQL active.
|
||||
id_compte : UUID du compte a debiter.
|
||||
montant : Montant a deduire (Decimal ou float).
|
||||
"""
|
||||
connexion.cursor().execute(
|
||||
'UPDATE accounts SET balance = balance - %s WHERE id = %s',
|
||||
(float(montant), id_compte)
|
||||
)
|
||||
|
||||
def crediter(self, connexion, id_compte, montant):
|
||||
"""
|
||||
Ajoute un montant au solde d'un compte.
|
||||
|
||||
Args:
|
||||
connexion : Connexion PostgreSQL active.
|
||||
id_compte : UUID du compte a crediter.
|
||||
montant : Montant a ajouter (Decimal ou float).
|
||||
"""
|
||||
connexion.cursor().execute(
|
||||
'UPDATE accounts SET balance = balance + %s WHERE id = %s',
|
||||
(float(montant), id_compte)
|
||||
)
|
||||
|
||||
# =========================================================
|
||||
# UTILITAIRES PRIVES
|
||||
# =========================================================
|
||||
|
||||
def _existe_compte_unique(self, connexion, id_utilisateur, type_compte):
|
||||
"""
|
||||
Verifie si l'utilisateur possede deja un compte du type donne.
|
||||
|
||||
Methode privee (prefixe _) appelee avant l'ouverture
|
||||
d'un Livret A ou d'une Assurance Vie.
|
||||
|
||||
Args:
|
||||
connexion : Connexion PostgreSQL active.
|
||||
id_utilisateur : UUID de l'utilisateur.
|
||||
type_compte (str): Type de compte a verifier.
|
||||
|
||||
Returns:
|
||||
bool: True si un compte actif de ce type existe deja.
|
||||
"""
|
||||
curseur = connexion.cursor()
|
||||
curseur.execute(
|
||||
"""
|
||||
SELECT id FROM accounts
|
||||
WHERE user_id = %s AND account_type = %s AND status = 'active'
|
||||
""",
|
||||
(id_utilisateur, type_compte)
|
||||
)
|
||||
return curseur.fetchone() is not None
|
||||
@@ -0,0 +1,113 @@
|
||||
"""
|
||||
DragonBank - Simulateur d'Epargne
|
||||
===================================
|
||||
Classe de logique pure pour la simulation de croissance d'epargne.
|
||||
|
||||
Cette classe ne fait aucun acces a la base de donnees.
|
||||
Elle contient uniquement les formules mathematiques de calcul
|
||||
des interets composes avec versements mensuels reguliers.
|
||||
|
||||
Version : 3.0
|
||||
"""
|
||||
|
||||
|
||||
class Simulateur:
|
||||
"""
|
||||
Calcule la croissance d'un capital epargne par interets composes.
|
||||
|
||||
Formule appliquee pour chaque mois :
|
||||
solde = solde * (1 + taux_mensuel) + versement_mensuel
|
||||
|
||||
Ou taux_mensuel = taux_annuel / 100 / 12.
|
||||
|
||||
Cette approche modelise les interets composes mensuels,
|
||||
qui correspondent au fonctionnement reel du Livret A et
|
||||
de l'assurance vie en fonds euros.
|
||||
"""
|
||||
|
||||
def simuler(self, capital_initial, taux_annuel, duree_annees,
|
||||
versement_mensuel=0.0):
|
||||
"""
|
||||
Simule la croissance d'un capital sur une duree donnee.
|
||||
|
||||
Calcule le capital annee par annee en appliquant les
|
||||
interets composes chaque mois et en ajoutant les versements.
|
||||
|
||||
Args:
|
||||
capital_initial (float) : Montant de depart en euros.
|
||||
taux_annuel (float) : Taux d'interet annuel en %.
|
||||
duree_annees (int) : Duree de la simulation en annees.
|
||||
versement_mensuel (float): Versement mensuel regulier (defaut: 0).
|
||||
|
||||
Returns:
|
||||
dict: Resultats complets de la simulation :
|
||||
- capital_initial (float) : Mise de depart.
|
||||
- taux_annuel (float) : Taux utilise.
|
||||
- duree_annees (int) : Duree de la simulation.
|
||||
- versement_mensuel (float) : Versement mensuel.
|
||||
- capital_final (float) : Capital total a la fin.
|
||||
- total_verse (float) : Somme totale versee (capital + versements).
|
||||
- total_interets (float) : Interets generes.
|
||||
- gain_pourcentage (float) : Gain en % par rapport au total verse.
|
||||
- courbe (list[dict]) : Evolution annee par annee.
|
||||
"""
|
||||
taux_mensuel = taux_annuel / 100.0 / 12.0
|
||||
solde_courant = float(capital_initial)
|
||||
total_verse = float(capital_initial)
|
||||
courbe = []
|
||||
|
||||
for annee in range(1, duree_annees + 1):
|
||||
solde_courant, total_verse = self._calculer_annee(
|
||||
solde_courant, total_verse, taux_mensuel, versement_mensuel
|
||||
)
|
||||
|
||||
courbe.append({
|
||||
'annee': annee,
|
||||
'solde': round(solde_courant, 2),
|
||||
'total_verse': round(total_verse, 2),
|
||||
'interets': round(solde_courant - total_verse, 2)
|
||||
})
|
||||
|
||||
capital_final = round(solde_courant, 2)
|
||||
total_interets = round(solde_courant - total_verse, 2)
|
||||
gain_pct = round((total_interets / max(total_verse, 1)) * 100, 2)
|
||||
|
||||
return {
|
||||
'capital_initial': capital_initial,
|
||||
'taux_annuel': taux_annuel,
|
||||
'duree_annees': duree_annees,
|
||||
'versement_mensuel': versement_mensuel,
|
||||
'capital_final': capital_final,
|
||||
'total_verse': round(total_verse, 2),
|
||||
'total_interets': total_interets,
|
||||
'gain_pourcentage': gain_pct,
|
||||
'courbe': courbe
|
||||
}
|
||||
|
||||
# =========================================================
|
||||
# UTILITAIRE PRIVE
|
||||
# =========================================================
|
||||
|
||||
def _calculer_annee(self, solde, total_verse, taux_mensuel, versement_mensuel):
|
||||
"""
|
||||
Calcule le solde et le total verse apres 12 mois.
|
||||
|
||||
Applique la formule des interets composes mois par mois,
|
||||
puis ajoute le versement mensuel.
|
||||
|
||||
Args:
|
||||
solde (float) : Solde en debut d'annee.
|
||||
total_verse (float) : Total deja verse en debut d'annee.
|
||||
taux_mensuel (float) : Taux mensuel (taux_annuel / 12 / 100).
|
||||
versement_mensuel (float): Versement ajoute chaque mois.
|
||||
|
||||
Returns:
|
||||
tuple: (nouveau_solde, nouveau_total_verse) apres 12 mois.
|
||||
"""
|
||||
for mois in range(12):
|
||||
if taux_mensuel > 0:
|
||||
solde = solde * (1.0 + taux_mensuel)
|
||||
solde = solde + versement_mensuel
|
||||
total_verse = total_verse + versement_mensuel
|
||||
|
||||
return solde, total_verse
|
||||
@@ -0,0 +1,447 @@
|
||||
"""
|
||||
DragonBank - Modele Transaction
|
||||
=================================
|
||||
Classe DAO gerant les virements et l'historique des transactions.
|
||||
|
||||
Chaque virement est atomique : debit et credit s'effectuent dans
|
||||
la meme transaction SQL. Si l'une echoue, tout est annule.
|
||||
|
||||
Version : 3.0
|
||||
"""
|
||||
|
||||
import decimal
|
||||
import logging
|
||||
import io
|
||||
import csv
|
||||
|
||||
from flask import Response
|
||||
|
||||
from database import serialiser_ligne, serialiser_lignes
|
||||
from config import LIMITE_TRANSACTIONS_DEFAUT, LIMITE_TRANSACTIONS_MAX
|
||||
|
||||
journaliseur = logging.getLogger('dragonbank.transaction')
|
||||
|
||||
# Noms lisibles pour les types de transactions (utilises dans l'export CSV).
|
||||
NOMS_TYPES = {
|
||||
'virement_interne': 'Virement interne',
|
||||
'virement_entre_personnes': 'Virement entre personnes',
|
||||
'virement_externe': 'Virement externe',
|
||||
'depot': 'Depot',
|
||||
'retrait': 'Retrait',
|
||||
'interets': 'Interets'
|
||||
}
|
||||
|
||||
|
||||
class TransactionDAO:
|
||||
"""
|
||||
Objet d'acces aux donnees pour la table transactions.
|
||||
|
||||
Contient la logique des trois types de virements :
|
||||
- Interne : entre les propres comptes de l'utilisateur.
|
||||
- Entre personnes : vers un beneficiaire DragonBank.
|
||||
- Externe : depuis/vers une banque tierce (simule).
|
||||
"""
|
||||
|
||||
# =========================================================
|
||||
# LECTURE
|
||||
# =========================================================
|
||||
|
||||
def lister(self, connexion, id_utilisateur, id_compte=None,
|
||||
type_transaction=None, limite=None):
|
||||
"""
|
||||
Retourne l'historique des transactions d'un utilisateur.
|
||||
|
||||
Args:
|
||||
connexion : Connexion PostgreSQL active.
|
||||
id_utilisateur : UUID de l'utilisateur.
|
||||
id_compte (str) : Filtrer par compte (optionnel).
|
||||
type_transaction : Filtrer par type (optionnel).
|
||||
limite (int) : Nombre max de resultats (defaut: 50).
|
||||
|
||||
Returns:
|
||||
list[dict]: Transactions triees par date decroissante.
|
||||
"""
|
||||
if limite is None:
|
||||
limite = LIMITE_TRANSACTIONS_DEFAUT
|
||||
if limite > LIMITE_TRANSACTIONS_MAX:
|
||||
limite = LIMITE_TRANSACTIONS_MAX
|
||||
|
||||
requete_base = """
|
||||
SELECT t.id, t.from_account_id, t.to_account_id, t.transaction_type,
|
||||
t.amount, t.currency, t.description, t.status, t.reference,
|
||||
t.external_bank_name, t.external_account_number,
|
||||
t.created_at, t.executed_at,
|
||||
fa.account_number AS from_account_number,
|
||||
ta.account_number AS to_account_number
|
||||
FROM transactions t
|
||||
LEFT JOIN accounts fa ON t.from_account_id = fa.id
|
||||
LEFT JOIN accounts ta ON t.to_account_id = ta.id
|
||||
"""
|
||||
|
||||
if id_compte:
|
||||
clause_where = "WHERE (t.from_account_id = %s OR t.to_account_id = %s)"
|
||||
parametres = [id_compte, id_compte]
|
||||
else:
|
||||
clause_where = "WHERE (fa.user_id = %s OR ta.user_id = %s)"
|
||||
parametres = [id_utilisateur, id_utilisateur]
|
||||
|
||||
if type_transaction:
|
||||
clause_where = clause_where + " AND t.transaction_type = %s"
|
||||
parametres.append(type_transaction)
|
||||
|
||||
parametres.append(limite)
|
||||
|
||||
curseur = connexion.cursor()
|
||||
curseur.execute(
|
||||
requete_base + ' ' + clause_where + ' ORDER BY t.created_at DESC LIMIT %s',
|
||||
parametres
|
||||
)
|
||||
return serialiser_lignes(curseur.fetchall())
|
||||
|
||||
def historique_solde(self, connexion, id_compte, solde_actuel):
|
||||
"""
|
||||
Reconstitue l'evolution du solde d'un compte dans le temps.
|
||||
|
||||
Utilise une back-calculation : on part du solde actuel
|
||||
et on remonte les transactions pour retrouver les soldes passes.
|
||||
|
||||
Args:
|
||||
connexion : Connexion PostgreSQL active.
|
||||
id_compte : UUID du compte.
|
||||
solde_actuel : Solde actuel du compte (float).
|
||||
|
||||
Returns:
|
||||
list[dict]: Points {date, solde} en ordre chronologique.
|
||||
"""
|
||||
curseur = connexion.cursor()
|
||||
curseur.execute(
|
||||
"""
|
||||
SELECT created_at, amount, from_account_id, to_account_id
|
||||
FROM transactions
|
||||
WHERE (from_account_id = %s OR to_account_id = %s)
|
||||
AND status = 'completed'
|
||||
ORDER BY created_at DESC
|
||||
""",
|
||||
(id_compte, id_compte)
|
||||
)
|
||||
transactions = curseur.fetchall()
|
||||
|
||||
solde_courant = float(solde_actuel)
|
||||
points = []
|
||||
|
||||
for transaction in transactions:
|
||||
montant = float(transaction['amount'])
|
||||
est_debit = str(transaction['from_account_id']) == id_compte
|
||||
|
||||
points.append({
|
||||
'date': str(transaction['created_at'])[:10],
|
||||
'solde': round(solde_courant, 2)
|
||||
})
|
||||
|
||||
if est_debit:
|
||||
solde_courant = solde_courant + montant
|
||||
else:
|
||||
solde_courant = solde_courant - montant
|
||||
|
||||
points.append({'date': 'Ouverture', 'solde': round(solde_courant, 2)})
|
||||
points.reverse()
|
||||
return points
|
||||
|
||||
# =========================================================
|
||||
# VIREMENTS
|
||||
# =========================================================
|
||||
|
||||
def virement_interne(self, connexion, id_source, id_destination,
|
||||
montant, libelle, compte_dao):
|
||||
"""
|
||||
Effectue un virement entre deux comptes du meme utilisateur.
|
||||
|
||||
Verifie le solde puis debite la source et credite la destination
|
||||
dans la meme transaction SQL (atomicite).
|
||||
|
||||
Args:
|
||||
connexion : Connexion PostgreSQL active.
|
||||
id_source (str) : UUID du compte source.
|
||||
id_destination : UUID du compte destination.
|
||||
montant : Montant (Decimal).
|
||||
libelle (str) : Description du virement.
|
||||
compte_dao : Instance de CompteDAO pour les mises a jour.
|
||||
|
||||
Returns:
|
||||
dict: La transaction creee.
|
||||
|
||||
Raises:
|
||||
ValueError: Si le solde est insuffisant.
|
||||
"""
|
||||
self._verifier_solde_suffisant(connexion, id_source, montant)
|
||||
|
||||
compte_dao.debiter(connexion, id_source, montant)
|
||||
compte_dao.crediter(connexion, id_destination, montant)
|
||||
|
||||
return self._enregistrer(
|
||||
connexion,
|
||||
from_id=id_source,
|
||||
to_id=id_destination,
|
||||
type_t='virement_interne',
|
||||
montant=montant,
|
||||
libelle=libelle
|
||||
)
|
||||
|
||||
def virement_entre_personnes(self, connexion, id_source, id_destination,
|
||||
montant, libelle, compte_dao):
|
||||
"""
|
||||
Effectue un virement vers un beneficiaire DragonBank.
|
||||
|
||||
Args:
|
||||
connexion : Connexion PostgreSQL active.
|
||||
id_source (str) : UUID du compte source.
|
||||
id_destination : UUID du compte destination (beneficiaire).
|
||||
montant : Montant (Decimal).
|
||||
libelle (str) : Description du virement.
|
||||
compte_dao : Instance de CompteDAO.
|
||||
|
||||
Returns:
|
||||
dict: La transaction creee.
|
||||
|
||||
Raises:
|
||||
ValueError: Si le solde est insuffisant.
|
||||
"""
|
||||
self._verifier_solde_suffisant(connexion, id_source, montant)
|
||||
|
||||
compte_dao.debiter(connexion, id_source, montant)
|
||||
compte_dao.crediter(connexion, id_destination, montant)
|
||||
|
||||
return self._enregistrer(
|
||||
connexion,
|
||||
from_id=id_source,
|
||||
to_id=id_destination,
|
||||
type_t='virement_entre_personnes',
|
||||
montant=montant,
|
||||
libelle=libelle
|
||||
)
|
||||
|
||||
def virement_externe(self, connexion, id_compte, montant, direction,
|
||||
nom_banque, numero_externe, libelle, compte_dao):
|
||||
"""
|
||||
Simule un virement depuis ou vers une banque externe.
|
||||
|
||||
Args:
|
||||
connexion : Connexion PostgreSQL active.
|
||||
id_compte (str) : UUID du compte DragonBank.
|
||||
montant : Montant (Decimal).
|
||||
direction (str) : 'outgoing' ou 'incoming'.
|
||||
nom_banque (str): Nom de la banque externe.
|
||||
numero_externe : Numero de compte externe.
|
||||
libelle (str) : Description.
|
||||
compte_dao : Instance de CompteDAO.
|
||||
|
||||
Returns:
|
||||
dict: La transaction creee.
|
||||
|
||||
Raises:
|
||||
ValueError: Si le solde est insuffisant (direction outgoing).
|
||||
"""
|
||||
if direction == 'outgoing':
|
||||
self._verifier_solde_suffisant(connexion, id_compte, montant)
|
||||
compte_dao.debiter(connexion, id_compte, montant)
|
||||
from_id = id_compte
|
||||
to_id = None
|
||||
else:
|
||||
compte_dao.crediter(connexion, id_compte, montant)
|
||||
from_id = None
|
||||
to_id = id_compte
|
||||
|
||||
return self._enregistrer(
|
||||
connexion,
|
||||
from_id=from_id,
|
||||
to_id=to_id,
|
||||
type_t='virement_externe',
|
||||
montant=montant,
|
||||
libelle=libelle,
|
||||
banque_externe=nom_banque,
|
||||
numero_externe=numero_externe
|
||||
)
|
||||
|
||||
# =========================================================
|
||||
# EXPORT CSV
|
||||
# =========================================================
|
||||
|
||||
def exporter_csv(self, connexion, id_utilisateur, id_compte=None):
|
||||
"""
|
||||
Genere un fichier CSV de l'historique des transactions.
|
||||
|
||||
Le fichier est encode en UTF-8 avec BOM (utf-8-sig) pour
|
||||
une compatibilite optimale avec Microsoft Excel sous Windows.
|
||||
Le separateur est le point-virgule (standard europeen).
|
||||
|
||||
Args:
|
||||
connexion : Connexion PostgreSQL active.
|
||||
id_utilisateur : UUID de l'utilisateur.
|
||||
id_compte (str) : Restreindre a un compte (optionnel).
|
||||
|
||||
Returns:
|
||||
flask.Response: Reponse HTTP avec le fichier CSV en piece jointe.
|
||||
"""
|
||||
if id_compte:
|
||||
curseur = connexion.cursor()
|
||||
curseur.execute(
|
||||
"""
|
||||
SELECT t.created_at, t.transaction_type, t.description,
|
||||
t.amount, t.status,
|
||||
fa.account_number AS from_account,
|
||||
ta.account_number AS to_account,
|
||||
t.external_bank_name
|
||||
FROM transactions t
|
||||
LEFT JOIN accounts fa ON t.from_account_id = fa.id
|
||||
LEFT JOIN accounts ta ON t.to_account_id = ta.id
|
||||
WHERE (t.from_account_id = %s OR t.to_account_id = %s)
|
||||
ORDER BY t.created_at DESC
|
||||
""",
|
||||
(id_compte, id_compte)
|
||||
)
|
||||
else:
|
||||
curseur = connexion.cursor()
|
||||
curseur.execute(
|
||||
"""
|
||||
SELECT t.created_at, t.transaction_type, t.description,
|
||||
t.amount, t.status,
|
||||
fa.account_number AS from_account,
|
||||
ta.account_number AS to_account,
|
||||
t.external_bank_name
|
||||
FROM transactions t
|
||||
LEFT JOIN accounts fa ON t.from_account_id = fa.id
|
||||
LEFT JOIN accounts ta ON t.to_account_id = ta.id
|
||||
WHERE (fa.user_id = %s OR ta.user_id = %s)
|
||||
ORDER BY t.created_at DESC
|
||||
""",
|
||||
(id_utilisateur, id_utilisateur)
|
||||
)
|
||||
|
||||
lignes = curseur.fetchall()
|
||||
|
||||
sortie = io.StringIO()
|
||||
writer = csv.writer(sortie, delimiter=';', quoting=csv.QUOTE_ALL)
|
||||
|
||||
writer.writerow([
|
||||
'Date', 'Type', 'Libelle', 'Montant (EUR)',
|
||||
'Statut', 'Compte source', 'Compte destination', 'Banque externe'
|
||||
])
|
||||
|
||||
for ligne in lignes:
|
||||
date = str(ligne['created_at'])[:16].replace('T', ' ') if ligne['created_at'] else ''
|
||||
writer.writerow([
|
||||
date,
|
||||
NOMS_TYPES.get(ligne['transaction_type'], ligne['transaction_type']),
|
||||
ligne['description'] or '',
|
||||
'{:.2f}'.format(float(ligne['amount'])),
|
||||
ligne['status'] or '',
|
||||
ligne['from_account'] or '',
|
||||
ligne['to_account'] or '',
|
||||
ligne['external_bank_name'] or ''
|
||||
])
|
||||
|
||||
contenu = sortie.getvalue()
|
||||
sortie.close()
|
||||
|
||||
return Response(
|
||||
contenu.encode('utf-8-sig'),
|
||||
mimetype='text/csv',
|
||||
headers={
|
||||
'Content-Disposition': 'attachment; filename="dragonbank_transactions.csv"',
|
||||
'Content-Type': 'text/csv; charset=utf-8'
|
||||
}
|
||||
)
|
||||
|
||||
# =========================================================
|
||||
# STATISTIQUES
|
||||
# =========================================================
|
||||
|
||||
def statistiques(self, connexion, id_utilisateur):
|
||||
"""
|
||||
Calcule les statistiques de transactions du mois courant.
|
||||
|
||||
Args:
|
||||
connexion : Connexion PostgreSQL active.
|
||||
id_utilisateur : UUID de l'utilisateur.
|
||||
|
||||
Returns:
|
||||
dict: {transactions_mois, total_envoye, total_recu}
|
||||
"""
|
||||
curseur = connexion.cursor()
|
||||
curseur.execute(
|
||||
"""
|
||||
SELECT COUNT(*) AS transactions_mois,
|
||||
COALESCE(SUM(CASE WHEN fa.user_id = %s THEN t.amount ELSE 0 END), 0) AS total_envoye,
|
||||
COALESCE(SUM(CASE WHEN ta.user_id = %s THEN t.amount ELSE 0 END), 0) AS total_recu
|
||||
FROM transactions t
|
||||
LEFT JOIN accounts fa ON t.from_account_id = fa.id
|
||||
LEFT JOIN accounts ta ON t.to_account_id = ta.id
|
||||
WHERE (fa.user_id = %s OR ta.user_id = %s)
|
||||
AND t.created_at >= date_trunc('month', CURRENT_DATE)
|
||||
AND t.status = 'completed'
|
||||
""",
|
||||
(id_utilisateur, id_utilisateur, id_utilisateur, id_utilisateur)
|
||||
)
|
||||
return curseur.fetchone()
|
||||
|
||||
# =========================================================
|
||||
# UTILITAIRES PRIVES
|
||||
# =========================================================
|
||||
|
||||
def _verifier_solde_suffisant(self, connexion, id_compte, montant):
|
||||
"""
|
||||
Verifie que le solde du compte est suffisant pour le debit.
|
||||
|
||||
Args:
|
||||
connexion : Connexion PostgreSQL active.
|
||||
id_compte : UUID du compte a verifier.
|
||||
montant : Montant a debiter (Decimal).
|
||||
|
||||
Raises:
|
||||
ValueError: Si le solde est inferieur au montant.
|
||||
"""
|
||||
curseur = connexion.cursor()
|
||||
curseur.execute('SELECT balance FROM accounts WHERE id = %s', (id_compte,))
|
||||
resultat = curseur.fetchone()
|
||||
if resultat is None:
|
||||
raise ValueError("Compte introuvable")
|
||||
|
||||
solde = decimal.Decimal(str(resultat['balance']))
|
||||
if solde < montant:
|
||||
raise ValueError(
|
||||
"Solde insuffisant (disponible : "
|
||||
+ '{:.2f}'.format(float(solde)) + " euros)"
|
||||
)
|
||||
|
||||
def _enregistrer(self, connexion, from_id, to_id, type_t,
|
||||
montant, libelle, banque_externe=None, numero_externe=None):
|
||||
"""
|
||||
Insere une nouvelle transaction en base de donnees.
|
||||
|
||||
Args:
|
||||
connexion : Connexion PostgreSQL active.
|
||||
from_id : UUID du compte source (ou None).
|
||||
to_id : UUID du compte destination (ou None).
|
||||
type_t (str) : Type de transaction.
|
||||
montant : Montant (Decimal ou float).
|
||||
libelle (str) : Description.
|
||||
banque_externe : Nom de la banque externe (optionnel).
|
||||
numero_externe : Numero de compte externe (optionnel).
|
||||
|
||||
Returns:
|
||||
dict: La transaction inseree.
|
||||
"""
|
||||
curseur = connexion.cursor()
|
||||
curseur.execute(
|
||||
"""
|
||||
INSERT INTO transactions
|
||||
(from_account_id, to_account_id, transaction_type, amount,
|
||||
description, status, external_bank_name,
|
||||
external_account_number, executed_at)
|
||||
VALUES (%s, %s, %s, %s, %s, 'completed', %s, %s, NOW())
|
||||
RETURNING id, amount, description, status, created_at
|
||||
""",
|
||||
(from_id, to_id, type_t, float(montant),
|
||||
libelle, banque_externe, numero_externe)
|
||||
)
|
||||
return serialiser_ligne(curseur.fetchone())
|
||||
@@ -0,0 +1,243 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user