maj
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
COMPOSE_PROJECT_NAME=dragonbank
|
||||||
|
POSTGRES_DB=dragonbank
|
||||||
|
POSTGRES_USER=dragonadmin
|
||||||
@@ -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
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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;
|
||||||
@@ -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
|
||||||
@@ -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"]
|
||||||
@@ -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)
|
||||||
@@ -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 %}
|
||||||
@@ -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> © 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 %}
|
||||||
@@ -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 %}
|
||||||
@@ -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 %}
|
||||||
@@ -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"]
|
||||||
@@ -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()
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
psycopg2-binary==2.9.9
|
||||||
|
schedule==1.2.1
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
dragonpass
|
||||||
Executable
BIN
Binary file not shown.
Executable
+30256
File diff suppressed because one or more lines are too long
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
+33264
File diff suppressed because it is too large
Load Diff
Executable
BIN
Binary file not shown.
+108
@@ -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.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Executable
+104
@@ -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>
|
||||||
Executable
+118
@@ -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>
|
||||||
Executable
+51
@@ -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>
|
||||||
Executable
+94
@@ -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>
|
||||||
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 219 KiB |
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,3 @@
|
|||||||
|
P6
|
||||||
|
960 1138
|
||||||
|
255
|
||||||
Binary file not shown.
Submodule
+1
Submodule projet_py added at 3cf8233054
Reference in New Issue
Block a user