This commit is contained in:
2026-03-27 10:20:35 +01:00
parent b922a1ced0
commit fa9d7c7f43
48 changed files with 67256 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
COMPOSE_PROJECT_NAME=dragonbank
POSTGRES_DB=dragonbank
POSTGRES_USER=dragonadmin
+43
View File
@@ -0,0 +1,43 @@
FROM python:3.12-slim
LABEL maintainer="DragonBank Team"
LABEL description="DragonBank Backend API"
# Variables d'environnement
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
# Répertoire de travail
WORKDIR /app
# Installation des dépendances système
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
libpq-dev \
curl \
&& rm -rf /var/lib/apt/lists/*
# Copie des fichiers de dépendances
COPY requirements.txt .
# Installation des dépendances Python
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir -r requirements.txt
# Copie du code source
COPY app.py .
# Création d'un utilisateur non-root (sécurité)
RUN adduser --disabled-password --gecos '' appuser && \
chown -R appuser:appuser /app
USER appuser
# Exposition du port
EXPOSE 5000
# Healthcheck
HEALTHCHECK --interval=15s --timeout=5s --retries=3 \
CMD curl -f http://localhost:5000/api/health || exit 1
# Commande de démarrage
CMD ["python", "app.py"]
File diff suppressed because it is too large Load Diff
+6
View File
@@ -0,0 +1,6 @@
Flask==3.0.0
Flask-CORS==4.0.0
psycopg2-binary==2.9.9
bcrypt==4.1.2
PyJWT==2.8.0
gunicorn==21.2.0
+14
View File
@@ -0,0 +1,14 @@
FROM postgres:16-alpine
LABEL maintainer="DragonBank Team"
LABEL description="DragonBank PostgreSQL Database"
# Copie du script d'initialisation
COPY init.sql /docker-entrypoint-initdb.d/
# Exposition du port
EXPOSE 5432
# Healthcheck intégré
HEALTHCHECK --interval=10s --timeout=5s --retries=5 \
CMD pg_isready -U dragonadmin -d dragonbank || exit 1
+232
View File
@@ -0,0 +1,232 @@
-- ============================================
-- DragonBank - Schéma de Base de Données
-- ============================================
-- Extension pour UUID
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
-- ============================================
-- TABLE: Utilisateurs
-- ============================================
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL,
phone VARCHAR(20),
address TEXT,
date_of_birth DATE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
is_active BOOLEAN DEFAULT TRUE,
last_login TIMESTAMP WITH TIME ZONE
);
-- ============================================
-- TABLE: Types de Comptes
-- ============================================
CREATE TYPE account_type AS ENUM ('courant', 'livret_a', 'assurance_vie');
CREATE TYPE account_status AS ENUM ('active', 'closed', 'frozen');
-- ============================================
-- TABLE: Comptes Bancaires
-- ============================================
CREATE TABLE IF NOT EXISTS accounts (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
account_number VARCHAR(20) UNIQUE NOT NULL,
account_type account_type NOT NULL DEFAULT 'courant',
balance DECIMAL(15, 2) NOT NULL DEFAULT 0.00,
currency VARCHAR(3) DEFAULT 'EUR',
status account_status DEFAULT 'active',
interest_rate DECIMAL(5, 4) DEFAULT 0.0000,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
CONSTRAINT positive_balance CHECK (balance >= 0)
);
-- ============================================
-- TABLE: Bénéficiaires
-- ============================================
CREATE TYPE beneficiary_status AS ENUM ('pending', 'approved', 'rejected');
CREATE TABLE IF NOT EXISTS beneficiaries (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
beneficiary_name VARCHAR(200) NOT NULL,
bank_name VARCHAR(200) DEFAULT 'DragonBank',
account_number VARCHAR(34) NOT NULL,
iban VARCHAR(34),
bic VARCHAR(11),
status beneficiary_status DEFAULT 'approved',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(user_id, account_number)
);
-- ============================================
-- TABLE: Transactions / Virements
-- ============================================
CREATE TYPE transaction_type AS ENUM (
'virement_interne',
'virement_entre_personnes',
'virement_externe',
'depot',
'retrait',
'interets'
);
CREATE TYPE transaction_status AS ENUM ('pending', 'completed', 'failed', 'cancelled');
CREATE TABLE IF NOT EXISTS transactions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
from_account_id UUID REFERENCES accounts(id),
to_account_id UUID REFERENCES accounts(id),
transaction_type transaction_type NOT NULL,
amount DECIMAL(15, 2) NOT NULL,
currency VARCHAR(3) DEFAULT 'EUR',
description TEXT,
status transaction_status DEFAULT 'pending',
reference VARCHAR(50) UNIQUE DEFAULT uuid_generate_v4()::text,
external_bank_name VARCHAR(200),
external_account_number VARCHAR(34),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
executed_at TIMESTAMP WITH TIME ZONE,
CONSTRAINT positive_amount CHECK (amount > 0)
);
-- ============================================
-- TABLE: Historique des Intérêts
-- ============================================
CREATE TABLE IF NOT EXISTS interest_history (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
account_id UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
amount DECIMAL(15, 2) NOT NULL,
rate DECIMAL(5, 4) NOT NULL,
balance_before DECIMAL(15, 2) NOT NULL,
balance_after DECIMAL(15, 2) NOT NULL,
calculated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- ============================================
-- TABLE: Sessions (pour la sécurité)
-- ============================================
CREATE TABLE IF NOT EXISTS sessions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token VARCHAR(500) NOT NULL,
ip_address VARCHAR(45),
user_agent TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
is_active BOOLEAN DEFAULT TRUE
);
-- ============================================
-- TABLE: Logs d'audit
-- ============================================
CREATE TABLE IF NOT EXISTS audit_logs (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID REFERENCES users(id),
action VARCHAR(100) NOT NULL,
details JSONB,
ip_address VARCHAR(45),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- ============================================
-- INDEX pour performance
-- ============================================
CREATE INDEX idx_accounts_user_id ON accounts(user_id);
CREATE INDEX idx_transactions_from ON transactions(from_account_id);
CREATE INDEX idx_transactions_to ON transactions(to_account_id);
CREATE INDEX idx_transactions_status ON transactions(status);
CREATE INDEX idx_transactions_created ON transactions(created_at);
CREATE INDEX idx_beneficiaries_user_id ON beneficiaries(user_id);
CREATE INDEX idx_sessions_user_id ON sessions(user_id);
CREATE INDEX idx_sessions_token ON sessions(token);
CREATE INDEX idx_audit_user_id ON audit_logs(user_id);
-- ============================================
-- FONCTION: Générer un numéro de compte
-- ============================================
CREATE OR REPLACE FUNCTION generate_account_number()
RETURNS VARCHAR(20) AS $$
DECLARE
new_number VARCHAR(20);
prefix VARCHAR(4) := 'DRG';
BEGIN
new_number := prefix || LPAD(FLOOR(RANDOM() * 10000000000)::TEXT, 13, '0');
WHILE EXISTS (SELECT 1 FROM accounts WHERE account_number = new_number) LOOP
new_number := prefix || LPAD(FLOOR(RANDOM() * 10000000000)::TEXT, 13, '0');
END LOOP;
RETURN new_number;
END;
$$ LANGUAGE plpgsql;
-- ============================================
-- FONCTION: Mise à jour du timestamp
-- ============================================
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Triggers
CREATE TRIGGER update_users_timestamp
BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
CREATE TRIGGER update_accounts_timestamp
BEFORE UPDATE ON accounts
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
-- ============================================
-- DONNÉES DE TEST
-- ============================================
-- Utilisateur de test (mot de passe: "password123")
INSERT INTO users (id, email, password_hash, first_name, last_name, phone, address, date_of_birth)
VALUES (
'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11',
'jean.dupont@email.com',
'$2b$12$LJ3m4ys3GZ5aJfrRkOmU0OYm0MqfGBCqGY5nS5B1VZHvMKvSG1IHa',
'Jean',
'Dupont',
'+33612345678',
'12 Rue de la Paix, 75001 Paris',
'1990-05-15'
);
-- Deuxième utilisateur de test
INSERT INTO users (id, email, password_hash, first_name, last_name, phone, address, date_of_birth)
VALUES (
'b1eebc99-9c0b-4ef8-bb6d-6bb9bd380a22',
'marie.martin@email.com',
'$2b$12$LJ3m4ys3GZ5aJfrRkOmU0OYm0MqfGBCqGY5nS5B1VZHvMKvSG1IHa',
'Marie',
'Martin',
'+33698765432',
'5 Avenue des Champs-Élysées, 75008 Paris',
'1985-11-20'
);
-- Comptes bancaires de test
INSERT INTO accounts (user_id, account_number, account_type, balance, interest_rate)
VALUES
('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 'DRG0000000001234', 'courant', 5000.00, 0.0000),
('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 'DRG0000000001235', 'livret_a', 15000.00, 0.0300),
('b1eebc99-9c0b-4ef8-bb6d-6bb9bd380a22', 'DRG0000000005678', 'courant', 3200.00, 0.0000),
('b1eebc99-9c0b-4ef8-bb6d-6bb9bd380a22', 'DRG0000000005679', 'assurance_vie', 25000.00, 0.0200);
-- Bénéficiaire de test
INSERT INTO beneficiaries (user_id, beneficiary_name, bank_name, account_number)
VALUES
('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 'Marie Martin', 'DragonBank', 'DRG0000000005678');
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO dragonadmin;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO dragonadmin;
+140
View File
@@ -0,0 +1,140 @@
version: '3.8'
services:
# ============================================
# BASE DE DONNÉES POSTGRES
# ============================================
db:
build:
context: ./db
dockerfile: Dockerfile
container_name: dragonbank-db
environment:
POSTGRES_DB: dragonbank
POSTGRES_USER: dragonadmin
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
volumes:
- postgres_data:/var/lib/postgresql/data
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql
networks:
- dragonbank-backend-net
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U dragonadmin -d dragonbank"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
restart: unless-stopped
# ============================================
# BACKEND API PYTHON (FLASK)
# ============================================
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: dragonbank-backend
environment:
DATABASE_URL: postgresql://dragonadmin:dragonpass@db:5432/dragonbank
SECRET_KEY: dragonbank-super-secret-key-2024
FLASK_ENV: production
depends_on:
db:
condition: service_healthy
networks:
- dragonbank-backend-net
- dragonbank-frontend-net
ports:
- "5000:5000"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000/api/health"]
interval: 15s
timeout: 5s
retries: 3
start_period: 20s
restart: unless-stopped
# ============================================
# FRONTEND PYTHON (FLASK)
# ============================================
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
container_name: dragonbank-frontend
environment:
BACKEND_URL: http://backend:5000
SECRET_KEY: frontend-secret-key-2024
depends_on:
backend:
condition: service_healthy
networks:
- dragonbank-frontend-net
ports:
- "8080:8080"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/"]
interval: 15s
timeout: 5s
retries: 3
start_period: 15s
restart: unless-stopped
# ============================================
# SERVICE D'INTÉRÊTS (BONUS)
# ============================================
interests:
build:
context: ./interests
dockerfile: Dockerfile
container_name: dragonbank-interests
environment:
DATABASE_URL: postgresql://dragonadmin:dragonpass@db:5432/dragonbank
INTEREST_RATE_LIVRET_A: 0.03
INTEREST_RATE_ASSURANCE_VIE: 0.02
INTERVAL_SECONDS: 86400
depends_on:
db:
condition: service_healthy
networks:
- dragonbank-backend-net
healthcheck:
test: ["CMD", "python", "-c", "import psycopg2; psycopg2.connect('postgresql://dragonadmin:dragonpass@db:5432/dragonbank')"]
interval: 30s
timeout: 10s
retries: 3
start_period: 15s
restart: unless-stopped
# ============================================
# SECRETS
# ============================================
secrets:
db_password:
file: ./secrets/db_password.txt
# ============================================
# VOLUMES
# ============================================
volumes:
postgres_data:
driver: local
# ============================================
# NETWORKS
# ============================================
networks:
dragonbank-backend-net:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
dragonbank-frontend-net:
driver: bridge
ipam:
config:
- subnet: 172.21.0.0/16
+30
View File
@@ -0,0 +1,30 @@
FROM python:3.12-slim
LABEL maintainer="DragonBank Team"
LABEL description="DragonBank Frontend Web App"
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir -r requirements.txt
COPY . .
RUN adduser --disabled-password --gecos '' appuser && \
chown -R appuser:appuser /app
USER appuser
EXPOSE 8080
HEALTHCHECK --interval=15s --timeout=5s --retries=3 \
CMD curl -f http://localhost:8080/ || exit 1
CMD ["python", "app.py"]
+357
View File
@@ -0,0 +1,357 @@
"""
DragonBank - Frontend Web Application
======================================
Interface web pour l'application bancaire DragonBank.
"""
import os
from functools import wraps
import requests
from flask import Flask, render_template, request, redirect, url_for, session, flash
app = Flask(__name__)
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'frontend-secret')
BACKEND_URL = os.environ.get('BACKEND_URL', 'http://backend:5000')
# ============================================
# UTILITAIRES
# ============================================
def api_request(method, endpoint, data=None, token=None):
"""Effectue une requête vers le backend API."""
url = f"{BACKEND_URL}{endpoint}"
headers = {'Content-Type': 'application/json'}
if token:
headers['Authorization'] = f'Bearer {token}'
try:
if method == 'GET':
resp = requests.get(url, headers=headers, timeout=10)
elif method == 'POST':
resp = requests.post(url, json=data, headers=headers, timeout=10)
elif method == 'DELETE':
resp = requests.delete(url, headers=headers, timeout=10)
else:
return None, 'Méthode non supportée'
return resp.json(), resp.status_code
except requests.exceptions.ConnectionError:
return {'error': 'Impossible de contacter le serveur'}, 503
except Exception as e:
return {'error': str(e)}, 500
def login_required(f):
"""Décorateur pour protéger les routes nécessitant une connexion."""
@wraps(f)
def decorated(*args, **kwargs):
if 'token' not in session:
flash('Veuillez vous connecter', 'warning')
return redirect(url_for('login'))
return f(*args, **kwargs)
return decorated
# ============================================
# ROUTES PUBLIQUES
# ============================================
@app.route('/')
def home():
"""Page d'accueil."""
if 'token' in session:
return redirect(url_for('dashboard'))
return render_template('login.html')
@app.route('/login', methods=['GET', 'POST'])
def login():
"""Page de connexion."""
if request.method == 'POST':
email = request.form.get('email')
password = request.form.get('password')
data, status = api_request('POST', '/api/auth/login', {
'email': email,
'password': password
})
if status == 200:
session['token'] = data['token']
session['user'] = data['user']
flash(f'Bienvenue {data["user"]["first_name"]} ! 🐉', 'success')
return redirect(url_for('dashboard'))
else:
flash(data.get('error', 'Erreur de connexion'), 'danger')
return render_template('login.html')
@app.route('/register', methods=['GET', 'POST'])
def register():
"""Page d'inscription."""
if request.method == 'POST':
form_data = {
'email': request.form.get('email'),
'password': request.form.get('password'),
'first_name': request.form.get('first_name'),
'last_name': request.form.get('last_name'),
'phone': request.form.get('phone', ''),
'address': request.form.get('address', ''),
'date_of_birth': request.form.get('date_of_birth') or None
}
# Vérifier confirmation mot de passe
if request.form.get('password') != request.form.get('confirm_password'):
flash('Les mots de passe ne correspondent pas', 'danger')
return render_template('register.html')
data, status = api_request('POST', '/api/auth/register', form_data)
if status == 201:
session['token'] = data['token']
session['user'] = data['user']
flash('Compte créé avec succès ! Un compte courant a été ouvert automatiquement. 🎉', 'success')
return redirect(url_for('dashboard'))
else:
flash(data.get('error', 'Erreur lors de l\'inscription'), 'danger')
return render_template('register.html')
@app.route('/logout')
def logout():
"""Déconnexion."""
session.clear()
flash('Vous êtes déconnecté', 'info')
return redirect(url_for('login'))
# ============================================
# ROUTES PROTÉGÉES - DASHBOARD
# ============================================
@app.route('/dashboard')
@login_required
def dashboard():
"""Tableau de bord principal."""
token = session['token']
accounts_data, _ = api_request('GET', '/api/accounts', token=token)
stats_data, _ = api_request('GET', '/api/stats', token=token)
transactions_data, _ = api_request('GET', '/api/transactions?limit=5', token=token)
return render_template('dashboard.html',
user=session.get('user', {}),
accounts=accounts_data.get('accounts', []),
stats=stats_data.get('stats', {}),
recent_transactions=transactions_data.get('transactions', [])
)
# ============================================
# ROUTES - COMPTES
# ============================================
@app.route('/accounts')
@login_required
def accounts():
"""Liste des comptes bancaires."""
token = session['token']
data, status = api_request('GET', '/api/accounts', token=token)
return render_template('accounts.html',
user=session.get('user', {}),
accounts=data.get('accounts', [])
)
@app.route('/accounts/open', methods=['GET', 'POST'])
@login_required
def open_account():
"""Ouverture d'un nouveau compte."""
if request.method == 'POST':
token = session['token']
form_data = {
'account_type': request.form.get('account_type'),
'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')
return redirect(url_for('accounts'))
else:
flash(data.get('error', 'Erreur lors de l\'ouverture du compte'), 'danger')
return render_template('open_account.html', user=session.get('user', {}))
# ============================================
# ROUTES - BÉNÉFICIAIRES
# ============================================
@app.route('/beneficiaries')
@login_required
def beneficiaries():
"""Liste des bénéficiaires."""
token = session['token']
data, status = api_request('GET', '/api/beneficiaries', token=token)
return render_template('beneficiaries.html',
user=session.get('user', {}),
beneficiaries=data.get('beneficiaries', [])
)
@app.route('/beneficiaries/add', methods=['GET', 'POST'])
@login_required
def add_beneficiary():
"""Ajout d'un bénéficiaire."""
if request.method == 'POST':
token = session['token']
form_data = {
'beneficiary_name': request.form.get('beneficiary_name'),
'account_number': request.form.get('account_number'),
'bank_name': request.form.get('bank_name', 'DragonBank'),
'iban': request.form.get('iban', ''),
'bic': request.form.get('bic', '')
}
data, status = api_request('POST', '/api/beneficiaries', data=form_data, token=token)
if status == 201:
flash('Bénéficiaire ajouté avec succès !', 'success')
return redirect(url_for('beneficiaries'))
else:
flash(data.get('error', 'Erreur'), 'danger')
return render_template('add_beneficiary.html', user=session.get('user', {}))
@app.route('/beneficiaries/delete/<beneficiary_id>')
@login_required
def delete_beneficiary(beneficiary_id):
"""Suppression d'un bénéficiaire."""
token = session['token']
data, status = api_request('DELETE', f'/api/beneficiaries/{beneficiary_id}', token=token)
if status == 200:
flash('Bénéficiaire supprimé', 'success')
else:
flash(data.get('error', 'Erreur'), 'danger')
return redirect(url_for('beneficiaries'))
# ============================================
# ROUTES - VIREMENTS
# ============================================
@app.route('/transfer', methods=['GET', 'POST'])
@login_required
def transfer():
"""Page de virement (interne et entre personnes)."""
token = session['token']
accounts_data, _ = api_request('GET', '/api/accounts', token=token)
beneficiaries_data, _ = api_request('GET', '/api/beneficiaries', token=token)
if request.method == 'POST':
transfer_type = request.form.get('transfer_type')
amount = float(request.form.get('amount', 0))
from_account_id = request.form.get('from_account_id')
description = request.form.get('description', '')
if transfer_type == 'internal':
to_account_id = request.form.get('to_account_id')
data, status = api_request('POST', '/api/transfers/internal', {
'from_account_id': from_account_id,
'to_account_id': to_account_id,
'amount': amount,
'description': description
}, token=token)
elif transfer_type == 'person':
beneficiary_id = request.form.get('beneficiary_id')
data, status = api_request('POST', '/api/transfers/person', {
'from_account_id': from_account_id,
'beneficiary_id': beneficiary_id,
'amount': amount,
'description': description
}, token=token)
else:
flash('Type de virement invalide', 'danger')
return redirect(url_for('transfer'))
if status == 200:
flash(data.get('message', 'Virement effectué !'), 'success')
return redirect(url_for('dashboard'))
else:
flash(data.get('error', 'Erreur lors du virement'), 'danger')
return render_template('transfer.html',
user=session.get('user', {}),
accounts=accounts_data.get('accounts', []),
beneficiaries=beneficiaries_data.get('beneficiaries', [])
)
@app.route('/transfer/external', methods=['GET', 'POST'])
@login_required
def transfer_external():
"""Virement externe (depuis/vers autre banque)."""
token = session['token']
accounts_data, _ = api_request('GET', '/api/accounts', token=token)
if request.method == 'POST':
form_data = {
'account_id': request.form.get('account_id'),
'amount': float(request.form.get('amount', 0)),
'external_bank_name': request.form.get('external_bank_name'),
'external_account_number': request.form.get('external_account_number'),
'direction': request.form.get('direction'),
'description': request.form.get('description', '')
}
data, status = api_request('POST', '/api/transfers/external', form_data, token=token)
if status == 200:
flash(data.get('message', 'Virement externe effectué !'), 'success')
return redirect(url_for('dashboard'))
else:
flash(data.get('error', 'Erreur'), 'danger')
return render_template('transfer_external.html',
user=session.get('user', {}),
accounts=accounts_data.get('accounts', [])
)
# ============================================
# ROUTES - HISTORIQUE
# ============================================
@app.route('/transactions')
@login_required
def transactions():
"""Historique des transactions."""
token = session['token']
account_id = request.args.get('account_id', '')
endpoint = '/api/transactions?limit=100'
if account_id:
endpoint += f'&account_id={account_id}'
data, status = api_request('GET', endpoint, token=token)
accounts_data, _ = api_request('GET', '/api/accounts', token=token)
return render_template('transactions.html',
user=session.get('user', {}),
transactions=data.get('transactions', []),
accounts=accounts_data.get('accounts', []),
selected_account=account_id
)
# ============================================
# DÉMARRAGE
# ============================================
if __name__ == '__main__':
print("🐉 DragonBank Frontend starting on port 8080...")
app.run(host='0.0.0.0', port=8080, debug=False)
+2
View File
@@ -0,0 +1,2 @@
Flask==3.0.0
requests==2.31.0
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,81 @@
{% extends "base.html" %}
{% block title %}Mes Comptes{% endblock %}
{% block content %}
<div class="main-content">
<div class="page-header d-flex justify-content-between align-items-center">
<div>
<h1><i class="bi bi-wallet2"></i> Mes Comptes</h1>
<p>Gérez vos comptes bancaires</p>
</div>
<a href="{{ url_for('open_account') }}" class="btn btn-dragon">
<i class="bi bi-plus-circle"></i> Ouvrir un compte
</a>
</div>
<div class="row g-4">
{% for account in accounts %}
<div class="col-lg-6 fade-in delay-{{ loop.index }}">
<div class="account-card {{ account.account_type }}" style="padding: 2rem;">
<div class="d-flex justify-content-between align-items-start mb-3">
<span class="account-type-badge {{ account.account_type }}">
{% if account.account_type == 'courant' %}
<i class="bi bi-credit-card"></i> Compte Courant
{% elif account.account_type == 'livret_a' %}
<i class="bi bi-piggy-bank"></i> Livret A
{% elif account.account_type == 'assurance_vie' %}
<i class="bi bi-shield-check"></i> Assurance Vie
{% endif %}
</span>
<span class="status-badge completed">
<i class="bi bi-check-circle"></i> {{ account.status }}
</span>
</div>
<div class="account-balance mb-2">
{{ "%.2f"|format(account.balance) }} €
</div>
<div class="account-number mb-3">
<i class="bi bi-hash"></i> {{ account.account_number }}
</div>
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">
<i class="bi bi-calendar"></i>
Ouvert le {{ account.created_at[:10] if account.created_at else 'N/A' }}
</small>
{% if account.interest_rate > 0 %}
<small class="text-success fw-bold">
<i class="bi bi-graph-up-arrow"></i>
Taux: {{ "%.2f"|format(account.interest_rate * 100) }}%
</small>
{% endif %}
</div>
<div class="mt-3 d-flex gap-2">
<a href="{{ url_for('transactions', account_id=account.id) }}"
class="btn btn-sm btn-dragon-outline">
<i class="bi bi-clock-history"></i> Historique
</a>
<a href="{{ url_for('transfer') }}" class="btn btn-sm btn-dragon">
<i class="bi bi-arrow-left-right"></i> Virement
</a>
</div>
</div>
</div>
{% else %}
<div class="col-12">
<div class="section-card text-center py-5">
<i class="bi bi-wallet" style="font-size: 4rem; color: var(--dragon-text-light);"></i>
<h3 class="mt-3">Aucun compte bancaire</h3>
<p class="text-muted">Ouvrez votre premier compte pour commencer</p>
<a href="{{ url_for('open_account') }}" class="btn btn-dragon mt-2">
<i class="bi bi-plus-circle"></i> Ouvrir un compte
</a>
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}
@@ -0,0 +1,70 @@
{% extends "base.html" %}
{% block title %}Ajouter un Bénéficiaire{% endblock %}
{% block content %}
<div class="main-content">
<div class="page-header">
<h1><i class="bi bi-person-plus"></i> Ajouter un Bénéficiaire</h1>
<p>Enregistrez un nouveau contact pour vos virements</p>
</div>
<div class="row justify-content-center">
<div class="col-md-6">
<div class="section-card fade-in">
<form method="POST" action="{{ url_for('add_beneficiary') }}">
<div class="form-group">
<label class="form-label">
<i class="bi bi-person"></i> Nom du bénéficiaire *
</label>
<input type="text" name="beneficiary_name" class="form-control"
placeholder="Ex: Marie Martin" required>
</div>
<div class="form-group">
<label class="form-label">
<i class="bi bi-bank"></i> Banque
</label>
<input type="text" name="bank_name" class="form-control"
value="DragonBank" placeholder="DragonBank">
</div>
<div class="form-group">
<label class="form-label">
<i class="bi bi-hash"></i> Numéro de compte *
</label>
<input type="text" name="account_number" class="form-control"
placeholder="Ex: DRG0000000005678" required>
<small class="text-muted">
Pour DragonBank, le numéro commence par DRG
</small>
</div>
<div class="form-group">
<label class="form-label">
<i class="bi bi-upc"></i> IBAN (optionnel)
</label>
<input type="text" name="iban" class="form-control"
placeholder="FR76 XXXX XXXX XXXX">
</div>
<div class="form-group">
<label class="form-label">
<i class="bi bi-building"></i> BIC (optionnel)
</label>
<input type="text" name="bic" class="form-control"
placeholder="DRGBFRPP">
</div>
<button type="submit" class="btn btn-dragon w-100 mt-3">
<i class="bi bi-check-circle"></i> Ajouter le bénéficiaire
</button>
<a href="{{ url_for('beneficiaries') }}" class="btn btn-dragon-outline w-100 mt-2">
<i class="bi bi-arrow-left"></i> Retour
</a>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
+123
View File
@@ -0,0 +1,123 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🐉 DragonBank - {% block title %}Accueil{% endblock %}</title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Bootstrap Icons -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/font/bootstrap-icons.css" rel="stylesheet">
<!-- Custom CSS -->
<link href="{{ url_for('static', filename='style.css') }}" rel="stylesheet">
</head>
<body>
{% if session.get('token') %}
<!-- NAVBAR CONNECTÉ -->
<nav class="navbar navbar-expand-lg navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="{{ url_for('dashboard') }}">
<span class="dragon-icon">🐉</span> DragonBank
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'dashboard' %}active{% endif %}"
href="{{ url_for('dashboard') }}">
<i class="bi bi-speedometer2"></i> Tableau de bord
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint in ['accounts', 'open_account'] %}active{% endif %}"
href="{{ url_for('accounts') }}">
<i class="bi bi-wallet2"></i> Mes comptes
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint in ['beneficiaries', 'add_beneficiary'] %}active{% endif %}"
href="{{ url_for('beneficiaries') }}">
<i class="bi bi-people"></i> Bénéficiaires
</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle {% if request.endpoint in ['transfer', 'transfer_external'] %}active{% endif %}"
href="#" role="button" data-bs-toggle="dropdown">
<i class="bi bi-arrow-left-right"></i> Virements
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="{{ url_for('transfer') }}">
<i class="bi bi-arrow-repeat"></i> Virement interne / Personne
</a></li>
<li><a class="dropdown-item" href="{{ url_for('transfer_external') }}">
<i class="bi bi-bank"></i> Virement externe
</a></li>
</ul>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'transactions' %}active{% endif %}"
href="{{ url_for('transactions') }}">
<i class="bi bi-clock-history"></i> Historique
</a>
</li>
</ul>
<ul class="navbar-nav">
<li class="nav-item me-3 d-flex align-items-center">
<span class="user-badge">
<i class="bi bi-person-circle"></i>
{{ session.get('user', {}).get('first_name', '') }}
{{ session.get('user', {}).get('last_name', '') }}
</span>
</li>
<li class="nav-item">
<a class="nav-link text-danger" href="{{ url_for('logout') }}">
<i class="bi bi-box-arrow-right"></i> Déconnexion
</a>
</li>
</ul>
</div>
</div>
</nav>
{% endif %}
<!-- FLASH MESSAGES -->
<div class="container-fluid mt-3">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{% if category == 'success' %}
<i class="bi bi-check-circle-fill"></i>
{% elif category == 'danger' %}
<i class="bi bi-exclamation-triangle-fill"></i>
{% elif category == 'warning' %}
<i class="bi bi-exclamation-circle-fill"></i>
{% else %}
<i class="bi bi-info-circle-fill"></i>
{% endif %}
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
</div>
<!-- CONTENU PRINCIPAL -->
{% block content %}{% endblock %}
{% if session.get('token') %}
<footer class="footer">
<p>🐉 <strong>DragonBank</strong> &copy; 2024 — Votre banque de confiance |
Sécurisé par <a href="#">Dragon Shield™</a></p>
</footer>
{% endif %}
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
{% block scripts %}{% endblock %}
</body>
</html>
@@ -0,0 +1,154 @@
{% extends "base.html" %}
{% block title %}Tableau de bord{% endblock %}
{% block content %}
<div class="main-content">
<!-- HEADER -->
<div class="page-header fade-in">
<h1>Bienvenue {{ user.get('first_name', '') }} 🐉</h1>
<p>Voici un aperçu de vos finances</p>
</div>
<!-- STATISTIQUES -->
<div class="row g-4 mb-4">
<div class="col-lg-3 col-md-6 fade-in delay-1">
<div class="stat-card primary">
<div class="stat-icon"><i class="bi bi-wallet2"></i></div>
<div class="stat-value">{{ "%.2f"|format(stats.get('total_balance', 0)) }} €</div>
<div class="stat-label">Solde total</div>
</div>
</div>
<div class="col-lg-3 col-md-6 fade-in delay-2">
<div class="stat-card success">
<div class="stat-icon"><i class="bi bi-credit-card"></i></div>
<div class="stat-value">{{ stats.get('total_accounts', 0) }}</div>
<div class="stat-label">Comptes actifs</div>
</div>
</div>
<div class="col-lg-3 col-md-6 fade-in delay-3">
<div class="stat-card info">
<div class="stat-icon"><i class="bi bi-arrow-left-right"></i></div>
<div class="stat-value">{{ stats.get('monthly_transactions', 0) }}</div>
<div class="stat-label">Transactions ce mois</div>
</div>
</div>
<div class="col-lg-3 col-md-6 fade-in delay-4">
<div class="stat-card warning">
<div class="stat-icon"><i class="bi bi-people"></i></div>
<div class="stat-value">{{ stats.get('total_beneficiaries', 0) }}</div>
<div class="stat-label">Bénéficiaires</div>
</div>
</div>
</div>
<div class="row g-4">
<!-- MES COMPTES -->
<div class="col-lg-7 fade-in delay-2">
<div class="section-card">
<div class="d-flex justify-content-between align-items-center mb-3">
<h3 class="section-title mb-0" style="border:none; padding:0;">
<i class="bi bi-wallet2"></i> Mes Comptes
</h3>
<a href="{{ url_for('open_account') }}" class="btn btn-sm btn-dragon">
<i class="bi bi-plus-circle"></i> Ouvrir un compte
</a>
</div>
{% if accounts %}
{% for account in accounts %}
<div class="account-card {{ account.account_type }}">
<div class="d-flex justify-content-between align-items-start">
<div>
<span class="account-type-badge {{ account.account_type }}">
{% if account.account_type == 'courant' %}
<i class="bi bi-credit-card"></i> Compte Courant
{% elif account.account_type == 'livret_a' %}
<i class="bi bi-piggy-bank"></i> Livret A
{% elif account.account_type == 'assurance_vie' %}
<i class="bi bi-shield-check"></i> Assurance Vie
{% endif %}
</span>
<div class="account-number mt-2">
{{ account.account_number }}
</div>
</div>
<div class="text-end">
<div class="account-balance">
{{ "%.2f"|format(account.balance) }} €
</div>
{% if account.interest_rate > 0 %}
<small class="text-success">
<i class="bi bi-graph-up-arrow"></i>
{{ "%.2f"|format(account.interest_rate * 100) }}% / jour
</small>
{% endif %}
</div>
</div>
</div>
{% endfor %}
{% else %}
<div class="text-center py-4 text-muted">
<i class="bi bi-wallet" style="font-size: 3rem;"></i>
<p class="mt-2">Aucun compte trouvé</p>
</div>
{% endif %}
</div>
</div>
<!-- ACTIONS RAPIDES + DERNIÈRES TRANSACTIONS -->
<div class="col-lg-5 fade-in delay-3">
<!-- Actions rapides -->
<div class="section-card mb-4">
<h3 class="section-title">
<i class="bi bi-lightning-charge"></i> Actions rapides
</h3>
<div class="d-grid gap-2">
<a href="{{ url_for('transfer') }}" class="btn btn-dragon">
<i class="bi bi-arrow-left-right"></i> Faire un virement
</a>
<a href="{{ url_for('add_beneficiary') }}" class="btn btn-gold">
<i class="bi bi-person-plus"></i> Ajouter un bénéficiaire
</a>
<a href="{{ url_for('transfer_external') }}" class="btn btn-success-custom">
<i class="bi bi-bank"></i> Virement externe
</a>
</div>
</div>
<!-- Dernières transactions -->
<div class="section-card">
<div class="d-flex justify-content-between align-items-center mb-3">
<h3 class="section-title mb-0" style="border:none; padding:0;">
<i class="bi bi-clock-history"></i> Dernières opérations
</h3>
<a href="{{ url_for('transactions') }}" class="btn btn-sm btn-dragon-outline">
Voir tout
</a>
</div>
{% if recent_transactions %}
{% for tx in recent_transactions %}
<div class="d-flex justify-content-between align-items-center py-2 border-bottom">
<div>
<div class="fw-bold" style="font-size: 0.9rem;">
{{ tx.description[:40] }}{% if tx.description|length > 40 %}...{% endif %}
</div>
<small class="text-muted">
{{ tx.created_at[:10] if tx.created_at else '' }}
</small>
</div>
<div>
<span class="fw-bold" style="color: {% if tx.transaction_type in ['depot', 'interets'] %}var(--dragon-success){% else %}var(--dragon-accent){% endif %};">
{{ "%.2f"|format(tx.amount) }} €
</span>
</div>
</div>
{% endfor %}
{% else %}
<p class="text-center text-muted py-3">Aucune transaction récente</p>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}
+68
View File
@@ -0,0 +1,68 @@
{% extends "base.html" %}
{% block title %}Connexion{% endblock %}
{% block content %}
<div class="login-page">
<div class="login-container">
<div class="login-header">
<span class="dragon-logo">🐉</span>
<h1>DragonBank</h1>
<p>Votre banque. Votre pouvoir.</p>
</div>
<div class="login-card fade-in">
<h2>Connexion</h2>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} mb-3">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST" action="{{ url_for('login') }}">
<div class="form-group">
<label class="form-label">
<i class="bi bi-envelope"></i> Email
</label>
<input type="email" name="email" class="form-control"
placeholder="votre@email.com" required>
</div>
<div class="form-group">
<label class="form-label">
<i class="bi bi-lock"></i> Mot de passe
</label>
<input type="password" name="password" class="form-control"
placeholder="••••••••" required>
</div>
<button type="submit" class="btn btn-dragon w-100 mt-3">
<i class="bi bi-box-arrow-in-right"></i> Se connecter
</button>
</form>
<hr class="my-4">
<p class="text-center text-muted mb-2">Pas encore de compte ?</p>
<a href="{{ url_for('register') }}" class="btn btn-dragon-outline w-100">
<i class="bi bi-person-plus"></i> Créer un compte
</a>
<div class="text-center mt-4">
<small class="text-muted">
<i class="bi bi-shield-check"></i>
Connexion sécurisée par Dragon Shield™
</small>
</div>
</div>
<div class="text-center mt-3">
<small style="color: rgba(255,255,255,0.5);">
Compte test: jean.dupont@email.com / password123
</small>
</div>
</div>
</div>
{% endblock %}
+102
View File
@@ -0,0 +1,102 @@
{% extends "base.html" %}
{% block title %}Inscription{% endblock %}
{% block content %}
<div class="login-page">
<div class="login-container" style="max-width: 550px;">
<div class="login-header">
<span class="dragon-logo">🐉</span>
<h1>DragonBank</h1>
<p>Rejoignez le clan du Dragon</p>
</div>
<div class="login-card fade-in">
<h2>Créer un compte</h2>
<form method="POST" action="{{ url_for('register') }}">
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label class="form-label">Prénom *</label>
<input type="text" name="first_name" class="form-control"
placeholder="Jean" required>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label class="form-label">Nom *</label>
<input type="text" name="last_name" class="form-control"
placeholder="Dupont" required>
</div>
</div>
</div>
<div class="form-group">
<label class="form-label">
<i class="bi bi-envelope"></i> Email *
</label>
<input type="email" name="email" class="form-control"
placeholder="votre@email.com" required>
</div>
<div class="form-group">
<label class="form-label">
<i class="bi bi-telephone"></i> Téléphone
</label>
<input type="tel" name="phone" class="form-control"
placeholder="+33 6 12 34 56 78">
</div>
<div class="form-group">
<label class="form-label">
<i class="bi bi-geo-alt"></i> Adresse
</label>
<input type="text" name="address" class="form-control"
placeholder="12 Rue de la Paix, 75001 Paris">
</div>
<div class="form-group">
<label class="form-label">
<i class="bi bi-calendar"></i> Date de naissance
</label>
<input type="date" name="date_of_birth" class="form-control">
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label class="form-label">
<i class="bi bi-lock"></i> Mot de passe *
</label>
<input type="password" name="password" class="form-control"
placeholder="Min. 6 caractères" minlength="6" required>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label class="form-label">
<i class="bi bi-lock-fill"></i> Confirmer *
</label>
<input type="password" name="confirm_password" class="form-control"
placeholder="Confirmer" required>
</div>
</div>
</div>
<button type="submit" class="btn btn-dragon w-100 mt-3">
<i class="bi bi-person-plus"></i> Créer mon compte
</button>
</form>
<hr class="my-3">
<p class="text-center">
<a href="{{ url_for('login') }}" class="text-decoration-none"
style="color: var(--dragon-accent);">
<i class="bi bi-arrow-left"></i> Déjà un compte ? Se connecter
</a>
</p>
</div>
</div>
</div>
{% endblock %}
@@ -0,0 +1,98 @@
{% extends "base.html" %}
{% block title %}Virement Externe{% endblock %}
{% block content %}
<div class="main-content">
<div class="page-header">
<h1><i class="bi bi-bank"></i> Virement Externe</h1>
<p>Envoyer ou recevoir un virement depuis une autre banque</p>
</div>
<div class="row justify-content-center">
<div class="col-md-7">
<div class="section-card fade-in">
<form method="POST" action="{{ url_for('transfer_external') }}">
<!-- Direction -->
<div class="form-group">
<label class="form-label fw-bold">
<i class="bi bi-arrow-down-up"></i> Direction
</label>
<select name="direction" class="form-select" required>
<option value="">-- Choisir --</option>
<option value="incoming">Réception (depuis une autre banque)</option>
<option value="outgoing">Envoi (vers une autre banque)</option>
</select>
</div>
<!-- Mon compte -->
<div class="form-group">
<label class="form-label fw-bold">
<i class="bi bi-wallet2"></i> Mon compte DragonBank
</label>
<select name="account_id" class="form-select" required>
<option value="">-- Choisir --</option>
{% for account in accounts %}
<option value="{{ account.id }}">
{{ account.account_type | upper }} -
{{ account.account_number }}
({{ "%.2f"|format(account.balance) }} €)
</option>
{% endfor %}
</select>
</div>
<!-- Banque externe -->
<div class="form-group">
<label class="form-label fw-bold">
<i class="bi bi-building"></i> Nom de la banque externe
</label>
<input type="text" name="external_bank_name" class="form-control"
placeholder="Ex: BNP Paribas, Société Générale..." required>
</div>
<!-- Compte externe -->
<div class="form-group">
<label class="form-label fw-bold">
<i class="bi bi-hash"></i> Numéro de compte externe
</label>
<input type="text" name="external_account_number" class="form-control"
placeholder="IBAN ou numéro de compte" required>
</div>
<!-- Montant -->
<div class="form-group">
<label class="form-label fw-bold">
<i class="bi bi-currency-euro"></i> Montant (€)
</label>
<input type="number" name="amount" class="form-control"
placeholder="0.00" min="0.01" step="0.01" required>
</div>
<!-- Description -->
<div class="form-group">
<label class="form-label">
<i class="bi bi-chat-left-text"></i> Motif (optionnel)
</label>
<input type="text" name="description" class="form-control"
placeholder="Ex: Transfert épargne">
</div>
<div class="alert alert-info mt-3">
<i class="bi bi-info-circle"></i>
<strong>Note:</strong> Les virements externes sont simulés dans cette démo.
En production, ils seraient traités via le réseau bancaire SEPA.
</div>
<button type="submit" class="btn btn-dragon w-100 mt-2">
<i class="bi bi-send"></i> Effectuer le virement
</button>
<a href="{{ url_for('dashboard') }}" class="btn btn-dragon-outline w-100 mt-2">
<i class="bi bi-arrow-left"></i> Annuler
</a>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
+29
View File
@@ -0,0 +1,29 @@
FROM python:3.12-slim
LABEL maintainer="DragonBank Team"
LABEL description="DragonBank Interest Calculator Service"
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir -r requirements.txt
COPY app.py .
RUN adduser --disabled-password --gecos '' appuser && \
chown -R appuser:appuser /app
USER appuser
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
CMD python -c "import psycopg2; psycopg2.connect('${DATABASE_URL}')" || exit 1
CMD ["python", "app.py"]
+191
View File
@@ -0,0 +1,191 @@
"""
DragonBank - Service de Calcul des Intérêts
============================================
Service indépendant qui calcule et applique les intérêts
sur les comptes épargne (Livret A, Assurance Vie).
Bonus: +3% journalier sur les comptes épargne.
"""
import os
import time
import decimal
from datetime import datetime, timezone
import psycopg2
import psycopg2.extras
import schedule
# ============================================
# CONFIGURATION
# ============================================
DATABASE_URL = os.environ.get(
'DATABASE_URL',
'postgresql://dragonadmin:dragonpass@db:5432/dragonbank'
)
INTEREST_RATE_LIVRET_A = float(os.environ.get('INTEREST_RATE_LIVRET_A', 0.03))
INTEREST_RATE_ASSURANCE_VIE = float(os.environ.get('INTEREST_RATE_ASSURANCE_VIE', 0.02))
INTERVAL_SECONDS = int(os.environ.get('INTERVAL_SECONDS', 86400)) # 24h par défaut
def get_db_connection():
"""Crée une connexion à la base de données."""
conn = psycopg2.connect(
DATABASE_URL,
cursor_factory=psycopg2.extras.RealDictCursor
)
return conn
def calculate_interests():
"""
Calcule et applique les intérêts sur tous les comptes épargne actifs.
- Livret A: INTEREST_RATE_LIVRET_A (par défaut 3%)
- Assurance Vie: INTEREST_RATE_ASSURANCE_VIE (par défaut 2%)
"""
print(f"\n{'='*60}")
print(f"🐉 CALCUL DES INTÉRÊTS - {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')}")
print(f"{'='*60}")
conn = get_db_connection()
try:
cur = conn.cursor()
# Récupérer tous les comptes épargne actifs
cur.execute('''
SELECT id, user_id, account_number, account_type, balance, interest_rate
FROM accounts
WHERE status = 'active'
AND account_type IN ('livret_a', 'assurance_vie')
AND balance > 0
''')
accounts = cur.fetchall()
total_interest_paid = decimal.Decimal('0.00')
accounts_processed = 0
for account in accounts:
account_id = str(account['id'])
balance = decimal.Decimal(str(account['balance']))
account_type = account['account_type']
# Déterminer le taux d'intérêt
if account_type == 'livret_a':
rate = decimal.Decimal(str(INTEREST_RATE_LIVRET_A))
elif account_type == 'assurance_vie':
rate = decimal.Decimal(str(INTEREST_RATE_ASSURANCE_VIE))
else:
continue
# Calculer les intérêts
interest_amount = (balance * rate).quantize(decimal.Decimal('0.01'))
if interest_amount <= 0:
continue
new_balance = balance + interest_amount
# Appliquer les intérêts
cur.execute(
'UPDATE accounts SET balance = %s, updated_at = NOW() WHERE id = %s',
(float(new_balance), account_id)
)
# Enregistrer dans l'historique des intérêts
cur.execute('''
INSERT INTO interest_history
(account_id, amount, rate, balance_before, balance_after)
VALUES (%s, %s, %s, %s, %s)
''', (
account_id,
float(interest_amount),
float(rate),
float(balance),
float(new_balance)
))
# Créer une transaction pour traçabilité
cur.execute('''
INSERT INTO transactions
(to_account_id, transaction_type, amount, description, status, executed_at)
VALUES (%s, 'interets', %s, %s, 'completed', NOW())
''', (
account_id,
float(interest_amount),
f"Intérêts {account_type.replace('_', ' ').title()} ({float(rate)*100:.1f}%)"
))
total_interest_paid += interest_amount
accounts_processed += 1
print(f"{account['account_number']} ({account_type}): "
f"{float(balance):.2f}€ → {float(new_balance):.2f}"
f"(+{float(interest_amount):.2f}€ à {float(rate)*100:.1f}%)")
conn.commit()
print(f"\n📊 Résumé:")
print(f" Comptes traités: {accounts_processed}")
print(f" Total intérêts versés: {float(total_interest_paid):.2f}")
print(f"{'='*60}\n")
except Exception as e:
conn.rollback()
print(f" ❌ ERREUR lors du calcul des intérêts: {e}")
finally:
conn.close()
def main():
"""Point d'entrée principal du service d'intérêts."""
print("🐉" + "="*58)
print(" DragonBank - Service de Calcul des Intérêts")
print("="*60)
print(f" Taux Livret A: {INTEREST_RATE_LIVRET_A*100:.1f}% / cycle")
print(f" Taux Assurance Vie: {INTEREST_RATE_ASSURANCE_VIE*100:.1f}% / cycle")
print(f" Intervalle: {INTERVAL_SECONDS} secondes")
print(f" Database: {DATABASE_URL.split('@')[1] if '@' in DATABASE_URL else 'configured'}")
print("="*60)
# Attendre que la DB soit prête
print("\n⏳ Attente de la connexion à la base de données...")
max_retries = 30
for i in range(max_retries):
try:
conn = get_db_connection()
conn.close()
print("✅ Connexion à la base de données établie !")
break
except Exception as e:
if i < max_retries - 1:
print(f" Tentative {i+1}/{max_retries} échouée: {e}")
time.sleep(2)
else:
print(f"❌ Impossible de se connecter à la DB après {max_retries} tentatives")
return
# Premier calcul immédiat
print("\n🚀 Premier calcul des intérêts...")
calculate_interests()
# Planifier les calculs suivants
# Pour la démo, on peut réduire l'intervalle
if INTERVAL_SECONDS < 3600:
# Si intervalle court (démo), utiliser un simple sleep
print(f"\n⏰ Mode démo: calcul toutes les {INTERVAL_SECONDS} secondes")
while True:
time.sleep(INTERVAL_SECONDS)
calculate_interests()
else:
# En production, calcul journalier
schedule.every(INTERVAL_SECONDS).seconds.do(calculate_interests)
print(f"\n⏰ Prochain calcul dans {INTERVAL_SECONDS} secondes")
while True:
schedule.run_pending()
time.sleep(60)
if __name__ == '__main__':
main()
+2
View File
@@ -0,0 +1,2 @@
psycopg2-binary==2.9.9
schedule==1.2.1
+1
View File
@@ -0,0 +1 @@
dragonpass
Executable
BIN
View File
Binary file not shown.
+30256
View File
File diff suppressed because one or more lines are too long
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+33264
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+108
View File
@@ -0,0 +1,108 @@
# BUT2 Théorie des langages
# TP du vendredi 13 mars 2026 (FI)
Ce TP revient en détails sur les notions suivantes.
* automate non déterministe
* mise en pratique de la déterminisation
* mise en pratique du test de l'équivalence de 2 automates
Vous devez faire ce TP sur papier ou sur votre ordinateur.
Vous pouvez vous aider de JFLAP.
Il faut écrire vos réponses soit sur un papier, soit dans un fichier en Markdown.
Votre réponse (scannée ou en markdown) sera intégrée dans un répertoire git qui s'appelle :
BUT2_TOMATE_26
Il faut partager ce git avec Florent Madelaine et Yves Kasparian.
## QCM
Pour voir si vous êtes à jour dans votre cours.
Plusieurs bonnes réponses possibles pour une question.
### Question 1.
1. Un automate accepte un mot si on peut dans l'automate fabriquer un chemin étiquetté par les lettres du mot de gauche à droite en suivant des transitions de l'automate depuis l'état initial vers un état acceptant.
1. Un automate est non déterministe si il existe deux états depuis lequel il existe une transition avec la même lettre.
1. Un automate est non déterministe si il existe un état depuis lequel il existe deux transitions avec la même lettre.
1. Un automate non déterministe rejette un mot si on peut dans l'automate fabriquer un chemin étiquetté par les lettres du mot de gauche à droite en suivant des transitions de l'automate depuis l'état initial vers un état non-acceptant.
1. Il existe plusieurs chemins pour un mot dans un automate non-déterministe
1. Il existe plusieurs chemins pour un mot dans un automate déterministe
### Question 2.
1. Il existe un automate non déterministe qui reconnaît un langage L qui ne peut pas être reconnu par un automate déterministe
1. Il existe toujours un automate déterministe qui reconnaît le même langage qu'un automate non déterministe, mais on ne sait pas toujours le construire.
1. Il existe toujours un automate déterministe qui reconnaît le même langage qu'un automate non déterministe, et on peut automatiser la construction.
Dans le pire des cas on va obtenir un automate qui a un nombre d'état qui est $2^n -1$ si l'automate non déterministe de départ a $n$ états.
### Question 3.
1. On peut tester l'équivalence de 2 automates déterministes en testant avec des mots au hasard de taille 42 et conclure ou non que les automates sont équivalents.
1. Si deux automates déterministes ne sont pas équivalents, il existe un mot qui peut servir de contre exemple de longueur au plus $n*m-1$ si $n$ et $m$ sont le nombre d'états respectifs des automates.
1. On ne peut pas tester si deux automates non-déterministes sont équivalents, par contre on peut tester si un automate non déterministe est équivalent à un autre automate qui lui est déterministe.
## Mise en oeuvre : déterminisation.
On considère l'automate TPAutonate1.jff dans le répertoire 2026.
1. Est-ce que cet automate est déterministe?
1. Est-ce que le mot aa est accepté? pourquoi?
1. Même question pour le mot aaba.
1. Pour les 2 mots ci-dessus, écrivez l'arbre de calcul.
1. Est-ce que vous pouvez déterminisez cet automate?
## Mise en oeuvre : déterminisation.
On considère l'automate TPAutonate2.jff dans le répertoire 2026.
1. Est-ce que cet automate est déterministe?
1. Est-ce que le mot aa est accepté? pourquoi?
1. Même question pour le mot aaba.
1. Pour les 2 mots ci-dessus, écrivez l'arbre de calcul.
1. Est-ce que vous pouvez déterminisez cet automate?
## Mise en oeuvre : équivalence
On considère les automates TPAutonate3.jff et TPAutonate4.jff dans le répertoire 2026.
1. Est-ce-que ces automates sont déterministes?
1. Est-ce-que ces automates sont complets?
1. Est-ce que ces automates sont équivalents?
## Mise en oeuvre : équivalence
On revisite les automates TPAutonate1.jff et TPAutonate2.jff dans le répertoire 2026.
1. Est-ce que ces automates sont équivalents?
## Pourquoi?
Reprendre le QCM du départ en justifiant chaque réponse.
## Pour aller plus loin : Équivalence.
On considère 2 programmes en java qui prennent en entrée un entier un int et retourne un booléen.
Est-ce qu'on peut tester si les deux programmes sont équivalents?
Même question pour un programme qui prend en entrée un fichier texte.
+104
View File
@@ -0,0 +1,104 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!--Created with JFLAP 7.1.--><structure>
<type>fa</type>
<automaton>
<!--The list of states.-->
<state id="0" name="q0">
<x>152.0</x>
<y>203.0</y>
<initial/>
</state>
<state id="1" name="q1">
<x>273.0</x>
<y>129.0</y>
</state>
<state id="2" name="q2">
<x>465.0</x>
<y>126.0</y>
<final/>
</state>
<state id="3" name="q3">
<x>278.0</x>
<y>268.0</y>
</state>
<state id="4" name="q4">
<x>467.0</x>
<y>254.0</y>
<final/>
</state>
<state id="5" name="q5">
<x>592.0</x>
<y>189.0</y>
</state>
<!--The list of transitions.-->
<transition>
<from>1</from>
<to>1</to>
<read>a</read>
</transition>
<transition>
<from>1</from>
<to>2</to>
<read>b</read>
</transition>
<transition>
<from>3</from>
<to>3</to>
<read>a</read>
</transition>
<transition>
<from>5</from>
<to>5</to>
<read>a</read>
</transition>
<transition>
<from>1</from>
<to>1</to>
<read>b</read>
</transition>
<transition>
<from>3</from>
<to>3</to>
<read>b</read>
</transition>
<transition>
<from>5</from>
<to>5</to>
<read>b</read>
</transition>
<transition>
<from>0</from>
<to>1</to>
<read>a</read>
</transition>
<transition>
<from>4</from>
<to>5</to>
<read>b</read>
</transition>
<transition>
<from>4</from>
<to>5</to>
<read>a</read>
</transition>
<transition>
<from>0</from>
<to>3</to>
<read>b</read>
</transition>
<transition>
<from>2</from>
<to>5</to>
<read>b</read>
</transition>
<transition>
<from>3</from>
<to>4</to>
<read>a</read>
</transition>
<transition>
<from>2</from>
<to>5</to>
<read>a</read>
</transition>
</automaton>
</structure>
+118
View File
@@ -0,0 +1,118 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!--Created with JFLAP 7.1.--><structure>
<type>fa</type>
<automaton>
<!--The list of states.-->
<state id="0" name="q0">
<x>152.0</x>
<y>203.0</y>
<initial/>
</state>
<state id="1" name="q1">
<x>273.0</x>
<y>129.0</y>
</state>
<state id="2" name="q2">
<x>465.0</x>
<y>126.0</y>
<final/>
</state>
<state id="3" name="q3">
<x>278.0</x>
<y>268.0</y>
</state>
<state id="4" name="q4">
<x>467.0</x>
<y>254.0</y>
<final/>
</state>
<state id="5" name="q5">
<x>592.0</x>
<y>189.0</y>
</state>
<state id="6" name="q6">
<x>709.0</x>
<y>191.0</y>
</state>
<!--The list of transitions.-->
<transition>
<from>1</from>
<to>1</to>
<read>a</read>
</transition>
<transition>
<from>1</from>
<to>2</to>
<read>b</read>
</transition>
<transition>
<from>4</from>
<to>4</to>
<read>a</read>
</transition>
<transition>
<from>5</from>
<to>5</to>
<read>a</read>
</transition>
<transition>
<from>3</from>
<to>3</to>
<read>b</read>
</transition>
<transition>
<from>2</from>
<to>2</to>
<read>b</read>
</transition>
<transition>
<from>2</from>
<to>1</to>
<read>a</read>
</transition>
<transition>
<from>6</from>
<to>6</to>
<read>b</read>
</transition>
<transition>
<from>0</from>
<to>1</to>
<read>a</read>
</transition>
<transition>
<from>5</from>
<to>6</to>
<read>a</read>
</transition>
<transition>
<from>6</from>
<to>5</to>
<read>b</read>
</transition>
<transition>
<from>5</from>
<to>4</to>
<read>a</read>
</transition>
<transition>
<from>0</from>
<to>3</to>
<read>b</read>
</transition>
<transition>
<from>5</from>
<to>2</to>
<read>b</read>
</transition>
<transition>
<from>4</from>
<to>3</to>
<read>b</read>
</transition>
<transition>
<from>3</from>
<to>4</to>
<read>a</read>
</transition>
</automaton>
</structure>
+51
View File
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!--Created with JFLAP 7.1.--><structure>
<type>fa</type>
<automaton>
<!--The list of states.-->
<state id="0" name="q0">
<x>99.0</x>
<y>161.0</y>
<initial/>
</state>
<state id="1" name="q1">
<x>227.0</x>
<y>172.0</y>
</state>
<state id="2" name="q2">
<x>380.0</x>
<y>166.0</y>
<final/>
</state>
<!--The list of transitions.-->
<transition>
<from>0</from>
<to>0</to>
<read>a</read>
</transition>
<transition>
<from>2</from>
<to>2</to>
<read>a</read>
</transition>
<transition>
<from>1</from>
<to>1</to>
<read>b</read>
</transition>
<transition>
<from>1</from>
<to>2</to>
<read>a</read>
</transition>
<transition>
<from>2</from>
<to>1</to>
<read>b</read>
</transition>
<transition>
<from>0</from>
<to>1</to>
<read>b</read>
</transition>
</automaton>
</structure>
+94
View File
@@ -0,0 +1,94 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!--Created with JFLAP 7.1.--><structure>
<type>fa</type>
<automaton>
<!--The list of states.-->
<state id="0" name="q0">
<x>79.0</x>
<y>174.0</y>
<initial/>
</state>
<state id="1" name="q1">
<x>156.0</x>
<y>75.0</y>
</state>
<state id="2" name="q2">
<x>168.0</x>
<y>234.0</y>
</state>
<state id="3" name="q3">
<x>298.0</x>
<y>72.0</y>
<final/>
</state>
<state id="4" name="q4">
<x>291.0</x>
<y>239.0</y>
</state>
<state id="5" name="q5">
<x>406.0</x>
<y>141.0</y>
<final/>
</state>
<!--The list of transitions.-->
<transition>
<from>3</from>
<to>3</to>
<read>a</read>
</transition>
<transition>
<from>2</from>
<to>4</to>
<read>b</read>
</transition>
<transition>
<from>1</from>
<to>1</to>
<read>b</read>
</transition>
<transition>
<from>0</from>
<to>1</to>
<read>b</read>
</transition>
<transition>
<from>3</from>
<to>4</to>
<read>b</read>
</transition>
<transition>
<from>4</from>
<to>5</to>
<read>a</read>
</transition>
<transition>
<from>5</from>
<to>4</to>
<read>b</read>
</transition>
<transition>
<from>4</from>
<to>1</to>
<read>b</read>
</transition>
<transition>
<from>0</from>
<to>2</to>
<read>a</read>
</transition>
<transition>
<from>2</from>
<to>0</to>
<read>a</read>
</transition>
<transition>
<from>1</from>
<to>3</to>
<read>a</read>
</transition>
<transition>
<from>5</from>
<to>3</to>
<read>a</read>
</transition>
</automaton>
</structure>
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.
Binary file not shown.
+3
View File
@@ -0,0 +1,3 @@
P6
960 1138
255
BIN
View File
Binary file not shown.
Submodule
+1
Submodule projet_py added at 3cf8233054