Maj
This commit is contained in:
@@ -1,33 +0,0 @@
|
||||
# 🐉 DragonBank - Rapport de Projet
|
||||
|
||||
## 1. Description du Projet et des Conteneurs (Dockers)
|
||||
Le projet DragonBank repose sur une architecture en micro-services conteneurisée à l'aide de **Docker** et **Docker-Compose**. L'architecture comprend les 4 conteneurs suivants :
|
||||
|
||||
- **`db` (Postgres)** : Base de données relationnelle persistante contenant les schémas, les tables, les configurations et l'historique des opérations.
|
||||
- **`backend` (Python/Flask)** : Le "cerveau" de l'application. Cette API RESTful gère toute la logique métier (authentification, gestion des comptes, validations de virements, audits) en utilisant une architecture saine (*Controllers/Services/DAOs*).
|
||||
- **`frontend` (Python/Flask + Jinja2)** : L'interface utilisateur web. Elle agit comme un client qui appelle l'API via des requêtes HTTP JSON, et affiche les informations retournées dans une belle interface graphique dynamique propulsée par Bootstrap.
|
||||
- **`interests` (Python)** : (Conteneur Bonus) Un conteneur "worker" autonome qui calcule et distribue le paiement asynchrone des intérêts journaliers applicables (+3% ou +2%) aux comptes d'épargne (Livret A, Assurance Vie), sans jamais nécessiter d'intervention humaine et sans bloquer l'API principale de la banque.
|
||||
|
||||
## 2. Fonctionnement de Chaque Application
|
||||
- **Frontend** : L'utilisateur se connecte via `/login`. L'application authentifie l'utilisateur, récupère le token JWT de l'API et l'enregistre de manière sécurisée dans la session locale (`session` en base 64 et signée). L'interface requiert ce token pour toutes les vues protégées afin de communiquer avec l'API (tableaux de bord, liste des comptes, demande de virement interne ou de virement intra-partenaires extérieurs).
|
||||
- **Backend API** : L'API intercepte les requêtes JSON. Elle vérifie l'intégrité de l'identité du client via le token JWT, et applique les règles de validations bancaires (fonds suffisants pour valider l'opération de virement, restrictions de l'IBAN bénéficiaire, etc.). La gestion transactionnelle de la base de données via nos services assure que l'architecture restera toujours consistante (propriétés ACID respectées).
|
||||
- **Interests** : Service d'arrière-plan avec un timer. Ce module exécute le SQL pour récupérer les encours de l'épargne sur chaque client de DragonBank et génère un virement interne spécial pour matérialiser ce versement de profit au client de manière silencieuse.
|
||||
|
||||
## 3. Schéma de Base de données
|
||||
Le modèle relationnel (cf. `db/init.sql`) a été pensé pour maximiser la sécurité et la traçabilité de toutes les opérations de la banque :
|
||||
- Les tables fondamentales : **`users`** (les clients, avec la civilité et le mot de passe hashé), **`accounts`** (les comptes bancaires avec association au `user_id` et garantie par une contrainte de balance positive `>= 0`), **`beneficiaries`** (le carnet d'adresses bancaires d'un usager).
|
||||
- La table **`transactions`** : L'historique immuable de chaque transfert d'argent (qui liste le `from_account`, `to_account`, et le type `virement_interne`, `externe` ou `interets`).
|
||||
- Les tables annexes de traçabilité : **`audit_logs`** et **`sessions`** enregistrent toutes les activités douteuses (tentatives d'authentifications répétées, de connexions par d'autres IP, et expiration stricte par token session).
|
||||
|
||||
## 4. Notions de Sécurité Implémentées
|
||||
Pour garantir la fiabilité de cet environnement académique, de nombreuses couches sécuritaires ont été intégrées :
|
||||
1. **Mots de passe Fortement Hachés** : Conservés côté base de données avec `bcrypt` en utilisant des sels d'entropie (pour se prémunir totalement contre les attaques Rainbow Tables).
|
||||
2. **Système de Jetons JSON (JWT)** : Utilisés pour vérifier la validité des accès aux points d'API sans requérir le mot de passe sur les appels courants.
|
||||
3. **Protection et Vérifications des Profils** : Toute intention de changer un e-mail principal ou un mot de passe bancaire (depuis le nouvel Onglet Profil) exigera nécessairement de produire son dernier mot de passe actuel en date.
|
||||
4. **Isolations Docker Secrets (Bonus)** : Pour empêcher la divulgation des identifiants Root de la Base de Données au sein des variables ou du code source, Postgres s'initialise au lancement en lisant le mot de passe confiné au sein d'un "Docker Secret" (`db_password.txt`).
|
||||
|
||||
## 5. Explication de l'implémentation Docker-Compose
|
||||
Le fichier `docker-compose.yml` construit le réseau total de manière hermétique et orchestrée :
|
||||
- **Networks (Cloisonnement)** : Un réseau de Frontend (`dragonbank-frontend-net`) et un réseau de Backend (`dragonbank-backend-net`). Ce bridage fait que le Frontend a le droit de discuter avec l'API, mais empêche physiquement le Frontend et potentiellement des attaques externes de ping la Base de Données directement.
|
||||
- **Volumes** : Un volume permanent `postgres_data` permet un maintien et une persistance sans perte des transactions et liquidités de la banque même si le conteneur subit un crash inopiné ou une mise à jour.
|
||||
- **Healthchecks (Bonus)** : La politique adoptée est "Fail Fast / Recover Quick". Les configurations Healthcheck imposent un ordre strict au démarrage absolu : l'interface Frontend s'interdira de démarrer et restera en pause tant que la route Health API de l'architecture Backend Python n'aura pas donné son aval (`curl -f /api/health`).
|
||||
@@ -20,18 +20,60 @@ class CompteService:
|
||||
conn = creer_connexion()
|
||||
try:
|
||||
import decimal as _d
|
||||
type_compte = donnees.get('account_type', 'courant')
|
||||
depot_brut = donnees.get('initial_deposit')
|
||||
depot = _d.Decimal(str(depot_brut)) if depot_brut else _d.Decimal('0')
|
||||
if depot < 0:
|
||||
raise ValueError('Le depot initial ne peut pas etre negatif')
|
||||
|
||||
# Pour les comptes epargne, le depot est preleve du compte courant
|
||||
if type_compte in ('livret_a', 'assurance_vie') and depot > 0:
|
||||
# Trouver le compte courant actif de l'utilisateur
|
||||
comptes = self.compte_dao.lister_par_utilisateur(conn, id_utilisateur_courant)
|
||||
compte_courant = next(
|
||||
(c for c in comptes if c['account_type'] == 'courant'), None
|
||||
)
|
||||
if not compte_courant:
|
||||
raise ValueError(
|
||||
"Vous devez posséder un compte courant pour alimenter ce compte."
|
||||
)
|
||||
solde_courant = _d.Decimal(str(compte_courant['balance']))
|
||||
if solde_courant < depot:
|
||||
raise ValueError(
|
||||
f"Solde insuffisant sur votre compte courant "
|
||||
f"(disponible : {float(solde_courant):.2f} €)."
|
||||
)
|
||||
# Debiter le compte courant
|
||||
self.compte_dao.debiter(conn, str(compte_courant['id']), depot)
|
||||
|
||||
# Enregistrer la transaction de transfert vers le nouveau compte (fait apres ouvrir)
|
||||
id_courant = str(compte_courant['id'])
|
||||
else:
|
||||
id_courant = None
|
||||
|
||||
compte = self.compte_dao.ouvrir(
|
||||
conn, id_utilisateur_courant,
|
||||
donnees.get('account_type', 'courant'),
|
||||
type_compte,
|
||||
depot
|
||||
)
|
||||
|
||||
# Si on a debite le courant, creer la transaction de transfert
|
||||
if id_courant and depot > 0:
|
||||
curseur = conn.cursor()
|
||||
noms = {'livret_a': 'Livret A', 'assurance_vie': 'Assurance Vie'}
|
||||
libelle = f"Versement initial sur {noms.get(type_compte, type_compte)}"
|
||||
curseur.execute(
|
||||
"""
|
||||
INSERT INTO transactions
|
||||
(from_account_id, to_account_id, transaction_type,
|
||||
amount, description, status, executed_at)
|
||||
VALUES (%s, %s, 'virement_interne', %s, %s, 'completed', NOW())
|
||||
""",
|
||||
(id_courant, str(compte['id']), float(depot), libelle)
|
||||
)
|
||||
|
||||
enregistrer_audit(conn.cursor(), id_utilisateur_courant, 'OPEN_ACCOUNT',
|
||||
{'type': donnees.get('account_type'), 'depot': float(depot)},
|
||||
{'type': type_compte, 'depot': float(depot)},
|
||||
obtenir_ip_client())
|
||||
conn.commit()
|
||||
return compte
|
||||
|
||||
@@ -84,7 +84,7 @@ services:
|
||||
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
|
||||
INTEREST_RATE_LIVRET_A: 0.03
|
||||
INTEREST_RATE_ASSURANCE_VIE: 0.02
|
||||
INTERVAL_SECONDS: 60
|
||||
INTERVAL_SECONDS: 900
|
||||
secrets:
|
||||
- db_password
|
||||
depends_on:
|
||||
|
||||
@@ -234,26 +234,41 @@ def accounts():
|
||||
)
|
||||
|
||||
|
||||
# Labels lisibles pour les types de comptes
|
||||
ACCOUNT_LABELS = {
|
||||
'courant': 'Compte Courant',
|
||||
'livret_a': 'Livret A',
|
||||
'assurance_vie': 'Assurance Vie',
|
||||
}
|
||||
|
||||
|
||||
@app.route('/accounts/open', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def open_account():
|
||||
"""Ouverture d'un nouveau compte."""
|
||||
token = session['token']
|
||||
accounts_data, _ = api_request('GET', '/api/accounts', token=token)
|
||||
accounts_list = accounts_data.get('accounts', [])
|
||||
|
||||
if request.method == 'POST':
|
||||
token = session['token']
|
||||
type_compte = request.form.get('account_type')
|
||||
form_data = {
|
||||
'account_type': request.form.get('account_type'),
|
||||
'account_type': type_compte,
|
||||
'initial_deposit': float(request.form.get('initial_deposit', 0))
|
||||
}
|
||||
|
||||
data, status = api_request('POST', '/api/accounts', data=form_data, token=token)
|
||||
|
||||
if status == 201:
|
||||
flash(f'Compte {form_data["account_type"]} ouvert avec succès ! 🎉', 'success')
|
||||
label = ACCOUNT_LABELS.get(type_compte, type_compte)
|
||||
flash(f'{label} ouvert avec succès ! 🎉', 'success')
|
||||
return redirect(url_for('accounts'))
|
||||
else:
|
||||
flash(data.get('error', 'Erreur lors de l\'ouverture du compte'), 'danger')
|
||||
flash(data.get('error', "Erreur lors de l'ouverture du compte"), 'danger')
|
||||
|
||||
return render_template('open_account.html', user=session.get('user', {}))
|
||||
return render_template('open_account.html',
|
||||
user=session.get('user', {}),
|
||||
accounts=accounts_list)
|
||||
|
||||
|
||||
# ============================================
|
||||
|
||||
@@ -11,6 +11,18 @@
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="section-card fade-in">
|
||||
|
||||
{% set compte_courant = accounts | selectattr('account_type', 'equalto', 'courant') | list | first %}
|
||||
{% if compte_courant %}
|
||||
<div class="alert alert-info d-flex align-items-center mb-3" role="alert">
|
||||
<i class="bi bi-info-circle-fill me-2"></i>
|
||||
<div>
|
||||
Solde disponible sur votre Compte Courant :
|
||||
<strong>{{ "%.2f"|format(compte_courant.balance) }} €</strong>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="POST" action="{{ url_for('open_account') }}">
|
||||
|
||||
<!-- Type de compte -->
|
||||
@@ -18,7 +30,8 @@
|
||||
<label class="form-label">
|
||||
<i class="bi bi-wallet2"></i> Type de compte *
|
||||
</label>
|
||||
<select name="account_type" class="form-select" required>
|
||||
<select name="account_type" id="account_type" class="form-select" required
|
||||
onchange="updateDepositLabel(this.value)">
|
||||
<option value="">-- Choisissez un type --</option>
|
||||
<option value="courant">
|
||||
Compte Courant — taux 0 %
|
||||
@@ -36,17 +49,24 @@
|
||||
</div>
|
||||
|
||||
<!-- Dépôt initial -->
|
||||
<div class="form-group">
|
||||
<div class="form-group" id="depot-group">
|
||||
<label class="form-label">
|
||||
<i class="bi bi-currency-euro"></i> Dépôt initial (optionnel)
|
||||
</label>
|
||||
<input type="number" name="initial_deposit" class="form-control"
|
||||
min="0" step="0.01" placeholder="0.00" value="0">
|
||||
<small class="text-muted">
|
||||
<input type="number" name="initial_deposit" id="initial_deposit"
|
||||
class="form-control" min="0" step="0.01" placeholder="0.00" value="0">
|
||||
<small class="text-muted" id="depot-hint">
|
||||
Laissez 0 pour ouvrir le compte sans dépôt initial.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<!-- Info box dynamique pour livret A / assurance vie -->
|
||||
<div id="epargne-info" class="alert alert-warning d-none mb-3" role="alert">
|
||||
<i class="bi bi-arrow-left-right me-1"></i>
|
||||
Le montant saisi sera <strong>débité de votre Compte Courant</strong>
|
||||
et transféré vers le nouveau compte épargne.
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-dragon w-100 mt-3">
|
||||
<i class="bi bi-check-circle"></i> Ouvrir le compte
|
||||
</button>
|
||||
@@ -61,7 +81,7 @@
|
||||
<!-- Récapitulatif des types de comptes -->
|
||||
<div class="row g-3 mt-2">
|
||||
<div class="col-12">
|
||||
<div class="section-card courant" style="border-left: 4px solid var(--dragon-primary);">
|
||||
<div class="section-card" style="border-left: 4px solid var(--dragon-primary);">
|
||||
<h6><i class="bi bi-credit-card"></i> Compte Courant</h6>
|
||||
<p class="text-muted mb-0 small">
|
||||
Compte de tous les jours pour vos dépenses et virements. Pas d'intérêts.
|
||||
@@ -73,7 +93,9 @@
|
||||
<div class="section-card" style="border-left: 4px solid #28a745;">
|
||||
<h6><i class="bi bi-piggy-bank"></i> Livret A</h6>
|
||||
<p class="text-muted mb-0 small">
|
||||
Épargne réglementée à taux fixe de 3 % par cycle. Un seul Livret A autorisé.
|
||||
Épargne réglementée à taux fixe de 3 % par cycle.
|
||||
Le dépôt initial est prélevé sur votre Compte Courant.
|
||||
Un seul Livret A autorisé.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -82,6 +104,7 @@
|
||||
<h6><i class="bi bi-shield-check"></i> Assurance Vie</h6>
|
||||
<p class="text-muted mb-0 small">
|
||||
Placement long terme à 2 % par cycle sur les fonds euros.
|
||||
Le dépôt initial est prélevé sur votre Compte Courant.
|
||||
Une seule Assurance Vie autorisée.
|
||||
</p>
|
||||
</div>
|
||||
@@ -91,3 +114,20 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function updateDepositLabel(type) {
|
||||
const info = document.getElementById('epargne-info');
|
||||
const hint = document.getElementById('depot-hint');
|
||||
if (type === 'livret_a' || type === 'assurance_vie') {
|
||||
info.classList.remove('d-none');
|
||||
hint.textContent = 'Ce montant sera prélevé sur votre Compte Courant.';
|
||||
} else {
|
||||
info.classList.add('d-none');
|
||||
hint.textContent = 'Laissez 0 pour ouvrir le compte sans dépôt initial.';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -71,8 +71,8 @@ TAUX_LIVRET_A = decimal.Decimal(os.environ.get('INTEREST_RATE_LIVRET_A', '0.03')
|
||||
TAUX_ASSURANCE_VIE = decimal.Decimal(os.environ.get('INTEREST_RATE_ASSURANCE_VIE', '0.02'))
|
||||
|
||||
# Duree en secondes entre deux cycles de calcul des interets.
|
||||
# La valeur 86400 correspond a 24 heures.
|
||||
INTERVALLE_SECONDES = int(os.environ.get('INTERVAL_SECONDS', '86400'))
|
||||
# La valeur 900 correspond a 15 minutes.
|
||||
INTERVALLE_SECONDES = int(os.environ.get('INTERVAL_SECONDS', '900'))
|
||||
|
||||
# Nombre maximum de tentatives de connexion a la DB au demarrage.
|
||||
TENTATIVES_CONNEXION_MAX = int(os.environ.get('DB_RETRY_MAX', '30'))
|
||||
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 166 KiB |
Reference in New Issue
Block a user