commit 2057d693cd0b411a9c468ddec6f3732ec7117e03 Author: jfgiraud Date: Wed Jul 22 16:37:53 2026 +0200 Initial commit - Bolsa app avec Tailwind et authentification diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..362ccfc --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +# Environnement virtuel +venv/ +env/ +ENV/ + +# Cache Python +__pycache__/ +*.py[cod] +*$py.class + +# Fichiers de configuration IDE / Système +.vscode/ +.idea/ +.DS_Store + +# Fichiers de base de données locaux (si applicable) +*.sqlite3 + +# Base de données MySQL Docker +mysql_data/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..34eba76 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM python:3.11-slim + +WORKDIR /app + +RUN pip install --no-cache-dir flask flask-sqlalchemy pymysql cryptography + +EXPOSE 5000 + +CMD ["python", "run.py"] diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..26b6cd9 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,38 @@ +import time +from flask import Flask, render_template +from flask_sqlalchemy import SQLAlchemy +from sqlalchemy.exc import OperationalError + +db = SQLAlchemy() + +def create_app(): + app = Flask(__name__) + app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://jfgiraud:lyon4@db/bolsa' + app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + app.config['SECRET_KEY'] = 'votre_cle_secrete_tres_securisee_jfgiraud04051963++fin' + + db.init_app(app) + + # Tentative de connexion et création des tables au démarrage du conteneur + with app.app_context(): + max_retries = 15 + delay = 2 + for i in range(max_retries): + try: + db.create_all() + print("Base de données connectée et tables initialisées avec succès.") + break + except OperationalError as e: + print(f"Tentative {i+1}/{max_retries} : MySQL n'est pas encore prêt. Nouvelle tentative dans {delay}s...") + time.sleep(delay) + else: + raise Exception("Impossible d'initialiser la base de données après plusieurs tentatives.") + + # Enregistrement des routes (Blueprint) + from app.routes import main + app.register_blueprint(main) + + return app + +# Expose 'app' pour que run.py puisse l'importer directement +app = create_app() \ No newline at end of file diff --git a/app/routes.py b/app/routes.py new file mode 100644 index 0000000..8538f78 --- /dev/null +++ b/app/routes.py @@ -0,0 +1,56 @@ +from flask import Blueprint, render_template, request, redirect, url_for, session, flash +from werkzeug.security import check_password_hash +from sqlalchemy import text +from app import db +from datetime import datetime + +main = Blueprint('main', __name__) + +@main.route('/', methods=['GET', 'POST']) +def index(): + if 'user_id' in session: + return redirect(url_for('main.menu')) + + if 'login_attempts' not in session: + session['login_attempts'] = 0 + + if request.method == 'POST': + user_login = request.form.get('login') + user_pass = request.form.get('pass') + + query = text("SELECT id, pass AS user_pass, nom, prenom FROM login WHERE login = :login AND actif = 1") + result = db.session.execute(query, {"login": user_login}).fetchone() + + if result and check_password_hash(result.user_pass, user_pass): + session.pop('login_attempts', None) + session['user_id'] = result.id + session['user_name'] = f"{result.prenom} {result.nom}" + return redirect(url_for('main.menu')) + else: + session['login_attempts'] += 1 + + if session['login_attempts'] >= 3: + session.pop('login_attempts', None) + return redirect('https://giraud-finance.com') + + essais_restants = 3 - session['login_attempts'] + flash(f"Identifiant ou mot de passe incorrect. Il vous reste {essais_restants} essai(s).", "danger") + + return render_template('index.html') + +@main.route('/menu') +def menu(): + if 'user_id' not in session: + return redirect(url_for('main.index')) + return render_template('menu.html') + +@main.route('/logout') +def logout(): + session.clear() + return redirect(url_for('main.index')) + +@main.route('/gestion-utilisateurs') +def gestion_utilisateurs(): + if 'user_id' not in session: + return redirect(url_for('main.index')) + return render_template('gestion_utilisateurs.html') \ No newline at end of file diff --git a/app/static/images/GFBlancLogo.png b/app/static/images/GFBlancLogo.png new file mode 100644 index 0000000..beebbdc Binary files /dev/null and b/app/static/images/GFBlancLogo.png differ diff --git a/app/static/images/favicon.png b/app/static/images/favicon.png new file mode 100644 index 0000000..59af163 Binary files /dev/null and b/app/static/images/favicon.png differ diff --git a/app/templates/base.html b/app/templates/base.html new file mode 100644 index 0000000..7e694b1 --- /dev/null +++ b/app/templates/base.html @@ -0,0 +1,204 @@ + + + + + + {% block title %}Bolsa App{% endblock %} + + + + + + + + + +
+ +
+ + Logo Giraud Finance + +
+ + +
+

Bolsa

+
+ + +
+
+ Paris : --:--:-- + +
+
+ New-York : --:--:-- + +
+ {% if session.get('user_id') %} + + {% endif %} +
+
+ + +
+ {% block content %}{% endblock %} +
+ + + + + + {% if session.get('user_id') %} + + {% endif %} + + + + + diff --git a/app/templates/gestion_utilisateurs.html b/app/templates/gestion_utilisateurs.html new file mode 100644 index 0000000..66e2180 --- /dev/null +++ b/app/templates/gestion_utilisateurs.html @@ -0,0 +1,45 @@ +{% extends "base.html" %} {% block title %}Gestion des utilisateurs - Bolsa{% +endblock %} {% block header_title %}Gestion utilisateurs{% endblock %} {% block +content %} +
+ +
+ +
+
+

+ Administration des utilisateurs +

+

+ Gérez les accès et les comptes autorisés de l'application. +

+
+ + Retour au menu + +
+ + +
+

+ Module de gestion des utilisateurs en cours de chargement... +

+
+ + +
+ Positions en bourse : Accès sécurisé. Veillez à fermer votre session après + chaque utilisation. +
+
+
+{% endblock %} diff --git a/app/templates/index.html b/app/templates/index.html new file mode 100644 index 0000000..711ff33 --- /dev/null +++ b/app/templates/index.html @@ -0,0 +1,58 @@ +{% extends "base.html" %} {% block title %}Connexion - Bolsa{% endblock %} {% +block content %} +
+
+

+ Connexion +

+ + + {% with messages = get_flashed_messages() %} {% if messages %} +
+ {% for message in messages %} {{ message }} {% endfor %} +
+ {% endif %} {% endwith %} + +
+
+ + +
+ +
+ + +
+ + +
+
+
+{% endblock %} diff --git a/app/templates/menu.html b/app/templates/menu.html new file mode 100644 index 0000000..0c134ee --- /dev/null +++ b/app/templates/menu.html @@ -0,0 +1,73 @@ +{% extends "base.html" %} {% block title %}Menu - Bolsa{% endblock %} {% block +header_title %}Tableau de bord{% endblock %} {% block content %} +
+ +
+ +

Espace de gestion

+

+ Accédez aux fonctions clés de l'application financière. +

+ + + + + +
+ Positions en bourse : Accès sécurisé. Veillez à fermer votre session après + chaque utilisation. +
+
+
+{% endblock %} diff --git a/create_user.py b/create_user.py new file mode 100644 index 0000000..bbe0360 --- /dev/null +++ b/create_user.py @@ -0,0 +1,31 @@ +from werkzeug.security import generate_password_hash +from sqlalchemy import text +from app import app, db + +with app.app_context(): + # Définition des informations de l'utilisateur + login_name = "jfgiraud" + nom = "GIRAUD" + prenom = "Jean-François" + mail = "jfgiraud@giraud-finance.com" + # Génération du hash sécurisé pour le mot de passe "lyon4" + hashed_password = generate_password_hash("lyon4") + + # Requête d'insertion + query = text(""" + INSERT INTO login (login, nom, prenom, mail, pass, actif) + VALUES (:login, :nom, :prenom, :mail, :pass, 1) + ON DUPLICATE KEY UPDATE + pass = :pass, nom = :nom, prenom = :prenom, mail = :mail, actif = 1 + """) + + db.session.execute(query, { + "login": login_name, + "nom": nom, + "prenom": prenom, + "mail": mail, + "pass": hashed_password + }) + db.session.commit() + + print(f"Utilisateur '{login_name}' créé/mis à jour avec succès avec un mot de passe haché !") \ No newline at end of file diff --git a/create_user.txt b/create_user.txt new file mode 100644 index 0000000..01e0502 --- /dev/null +++ b/create_user.txt @@ -0,0 +1,5 @@ +Pour exécuter ce script directement à l'intérieur du conteneur web utiliser la commande suivante dans votre terminal : + +docker compose exec web python create_user.py + +Une fois cette commande passée, vous pourrez vous connecter sur votre page d'accueil avec le login jfgiraud et le mot de passe lyon4. \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e0e7d51 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,51 @@ +services: + db: + image: mysql:8.0 + environment: + MYSQL_ROOT_PASSWORD: sysadm-1963 + MYSQL_DATABASE: bolsa + MYSQL_USER: jfgiraud + MYSQL_PASSWORD: lyon4 + volumes: + - db_data:/var/lib/mysql + healthcheck: + test: + [ + "CMD", + "mysqladmin", + "ping", + "-h", + "127.0.0.1", + "-u", + "root", + "-psysadm-1963", + ] + interval: 3s + timeout: 3s + retries: 10 + + web: + build: . + command: python run.py + volumes: + - .:/app + ports: + - "5000:5000" + environment: + - DATABASE_URL=mysql+pymysql://jfgiraud:lyon4@db/bolsa + depends_on: + db: + condition: service_healthy + + phpmyadmin: + image: phpmyadmin/phpmyadmin + ports: + - "8080:80" + environment: + PMA_HOST: db + MYSQL_ROOT_PASSWORD: sysadm-1963 + depends_on: + - db + +volumes: + db_data: diff --git a/run.py b/run.py new file mode 100644 index 0000000..7592ef5 --- /dev/null +++ b/run.py @@ -0,0 +1,4 @@ +from app import app + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=5000, debug=True) \ No newline at end of file