Initial commit - Bolsa app avec Tailwind et authentification
This commit is contained in:
+20
@@ -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/
|
||||||
@@ -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"]
|
||||||
@@ -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()
|
||||||
@@ -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')
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 630 B |
@@ -0,0 +1,204 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="fr" class="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>{% block title %}Bolsa App{% endblock %}</title>
|
||||||
|
|
||||||
|
<!-- Favicon -->
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
href="{{ url_for('static', filename='images/favicon.png') }}"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Tailwind CSS CDN -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
||||||
|
</head>
|
||||||
|
<body
|
||||||
|
class="bg-gray-950 text-gray-100 min-h-screen flex flex-col justify-between"
|
||||||
|
>
|
||||||
|
<!-- Header Global -->
|
||||||
|
<header
|
||||||
|
class="bg-gray-900 border-b border-gray-800 px-6 py-4 flex items-center justify-between"
|
||||||
|
>
|
||||||
|
<!-- Gauche : Logo (taille augmentée d'environ 1.5x, ex: h-10 -> h-16) -->
|
||||||
|
<div class="flex items-center">
|
||||||
|
<a
|
||||||
|
href="{{ url_for('main.menu') if session.get('user_id') else url_for('main.index') }}"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="{{ url_for('static', filename='images/GFBlancLogo.png') }}"
|
||||||
|
alt="Logo Giraud Finance"
|
||||||
|
class="h-16 w-auto"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Centre : Titre (agrandi avec text-4xl et police plus grasse) -->
|
||||||
|
<div class="absolute left-1/2 transform -translate-x-1/2">
|
||||||
|
<h1 class="text-4xl font-extrabold tracking-wider text-white">Bolsa</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Droite : Horloges Paris & New-York + Lien de déconnexion -->
|
||||||
|
<div class="text-xs font-mono text-right space-y-1">
|
||||||
|
<div id="clock-paris" class="flex items-center justify-end space-x-2">
|
||||||
|
<span>Paris : <span class="time">--:--:--</span></span>
|
||||||
|
<span
|
||||||
|
id="status-paris"
|
||||||
|
class="inline-block w-2.5 h-2.5 rounded-full bg-red-500"
|
||||||
|
title="Fermée"
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
<div id="clock-ny" class="flex items-center justify-end space-x-2">
|
||||||
|
<span>New-York : <span class="time">--:--:--</span></span>
|
||||||
|
<span
|
||||||
|
id="status-ny"
|
||||||
|
class="inline-block w-2.5 h-2.5 rounded-full bg-red-500"
|
||||||
|
title="Fermée"
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
{% if session.get('user_id') %}
|
||||||
|
<div class="pt-1.5 flex justify-end">
|
||||||
|
<a
|
||||||
|
href="{{ url_for('main.logout') }}"
|
||||||
|
class="no-underline px-2.5 py-1 bg-red-950/60 hover:bg-red-900/80 border border-red-900/60 text-red-300 rounded text-[11px] font-sans transition duration-200"
|
||||||
|
>
|
||||||
|
Se déconnecter
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Container dynamique -->
|
||||||
|
<main class="flex-grow container mx-auto px-4 py-8">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Footer Global -->
|
||||||
|
<footer
|
||||||
|
class="bg-gray-900 border-t border-gray-800 px-6 py-4 flex items-center justify-between text-xs text-gray-400"
|
||||||
|
>
|
||||||
|
<!-- Espace vide à gauche pour équilibrer le flexbox -->
|
||||||
|
<div class="w-1/3"></div>
|
||||||
|
|
||||||
|
<!-- Centre : Copyright -->
|
||||||
|
<div class="w-1/3 text-center">
|
||||||
|
<p>copyright © Jean-François GIRAUD - Giraud Finance - 2026</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Droite : Nom et IP de l'utilisateur si connecté -->
|
||||||
|
<div class="w-1/3 text-right font-mono">
|
||||||
|
{% if session.get('user_id') %}
|
||||||
|
<div class="text-gray-200 font-semibold">
|
||||||
|
{{ session.get('user_name') }}
|
||||||
|
</div>
|
||||||
|
<div id="external-ip" class="text-gray-400 text-[11px]">
|
||||||
|
{{ session.get('user_ip', 'IP inconnue') }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<!-- Script pour récupérer l'IP externe en JS -->
|
||||||
|
{% if session.get('user_id') %}
|
||||||
|
<script>
|
||||||
|
fetch("https://api.ipify.org?format=json")
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => {
|
||||||
|
document.getElementById("external-ip").textContent = data.ip;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
document.getElementById("external-ip").textContent =
|
||||||
|
"IP non disponible";
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Script pour l'heure et l'état des bourses -->
|
||||||
|
<script>
|
||||||
|
function updateClocks() {
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
// Options pour Paris (Europe/Paris)
|
||||||
|
const optionsParis = {
|
||||||
|
timeZone: "Europe/Paris",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
hour12: false,
|
||||||
|
};
|
||||||
|
const timeStringParis = new Intl.DateTimeFormat(
|
||||||
|
"fr-FR",
|
||||||
|
optionsParis,
|
||||||
|
).format(now);
|
||||||
|
document.querySelector("#clock-paris .time").textContent =
|
||||||
|
timeStringParis;
|
||||||
|
|
||||||
|
// Options pour New York (America/New_York)
|
||||||
|
const optionsNY = {
|
||||||
|
timeZone: "America/New_York",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
hour12: false,
|
||||||
|
};
|
||||||
|
const timeStringNY = new Intl.DateTimeFormat("fr-FR", optionsNY).format(
|
||||||
|
now,
|
||||||
|
);
|
||||||
|
document.querySelector("#clock-ny .time").textContent = timeStringNY;
|
||||||
|
|
||||||
|
// Vérification ouverture bourses (Jours ouvrés Lundi-Vendredi)
|
||||||
|
const dayParis = new Intl.DateTimeFormat("fr-FR", {
|
||||||
|
timeZone: "Europe/Paris",
|
||||||
|
weekday: "short",
|
||||||
|
}).format(now);
|
||||||
|
const hourNumParis = parseFloat(
|
||||||
|
new Intl.DateTimeFormat("fr-FR", {
|
||||||
|
timeZone: "Europe/Paris",
|
||||||
|
hour: "numeric",
|
||||||
|
minute: "numeric",
|
||||||
|
hour12: false,
|
||||||
|
})
|
||||||
|
.format(now)
|
||||||
|
.replace(":", "."),
|
||||||
|
);
|
||||||
|
|
||||||
|
const dayNY = new Intl.DateTimeFormat("fr-FR", {
|
||||||
|
timeZone: "America/New_York",
|
||||||
|
weekday: "short",
|
||||||
|
}).format(now);
|
||||||
|
const hourNumNY = parseFloat(
|
||||||
|
new Intl.DateTimeFormat("fr-FR", {
|
||||||
|
timeZone: "America/New_York",
|
||||||
|
hour: "numeric",
|
||||||
|
minute: "numeric",
|
||||||
|
hour12: false,
|
||||||
|
})
|
||||||
|
.format(now)
|
||||||
|
.replace(":", "."),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Paris ouvert du Lundi au Vendredi de 09:00 à 17:30
|
||||||
|
const isParisOpen =
|
||||||
|
!["sam.", "dim.", "sam", "dim"].includes(dayParis.toLowerCase()) &&
|
||||||
|
hourNumParis >= 9.0 &&
|
||||||
|
hourNumParis <= 17.3;
|
||||||
|
const dotParis = document.getElementById("status-paris");
|
||||||
|
dotParis.className = `inline-block w-2.5 h-2.5 rounded-full ${isParisOpen ? "bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.6)]" : "bg-red-500"}`;
|
||||||
|
|
||||||
|
// New York ouvert du Lundi au Vendredi de 09:30 à 16:00
|
||||||
|
const isNYOpen =
|
||||||
|
!["sam.", "dim.", "sam", "dim"].includes(dayNY.toLowerCase()) &&
|
||||||
|
hourNumNY >= 9.3 &&
|
||||||
|
hourNumNY <= 16.0;
|
||||||
|
const dotNY = document.getElementById("status-ny");
|
||||||
|
dotNY.className = `inline-block w-2.5 h-2.5 rounded-full ${isNYOpen ? "bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.6)]" : "bg-red-500"}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
setInterval(updateClocks, 1000);
|
||||||
|
updateClocks();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
{% extends "base.html" %} {% block title %}Gestion des utilisateurs - Bolsa{%
|
||||||
|
endblock %} {% block header_title %}Gestion utilisateurs{% endblock %} {% block
|
||||||
|
content %}
|
||||||
|
<div class="flex flex-col items-center justify-center py-6">
|
||||||
|
<!-- Box centrée principale -->
|
||||||
|
<div
|
||||||
|
class="bg-gray-900 border border-gray-800 p-8 rounded-2xl shadow-2xl w-full max-w-4xl"
|
||||||
|
>
|
||||||
|
<!-- En-tête de section -->
|
||||||
|
<div
|
||||||
|
class="flex justify-between items-center mb-6 border-b border-gray-800 pb-4"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-extrabold text-white">
|
||||||
|
Administration des utilisateurs
|
||||||
|
</h2>
|
||||||
|
<p class="text-gray-400 text-sm">
|
||||||
|
Gérez les accès et les comptes autorisés de l'application.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href="{{ url_for('main.menu') }}"
|
||||||
|
class="px-4 py-2 bg-gray-800 hover:bg-gray-700 text-gray-200 text-sm font-medium rounded-lg transition duration-200"
|
||||||
|
>
|
||||||
|
Retour au menu
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Contenu de la page -->
|
||||||
|
<div class="text-gray-300 py-8 text-center">
|
||||||
|
<p class="text-gray-400 italic">
|
||||||
|
Module de gestion des utilisateurs en cours de chargement...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pied de page interne -->
|
||||||
|
<div
|
||||||
|
class="border-t border-gray-800 pt-4 text-xs text-gray-400 italic text-center"
|
||||||
|
>
|
||||||
|
Positions en bourse : Accès sécurisé. Veillez à fermer votre session après
|
||||||
|
chaque utilisation.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
{% extends "base.html" %} {% block title %}Connexion - Bolsa{% endblock %} {%
|
||||||
|
block content %}
|
||||||
|
<div class="flex justify-center items-center h-[65vh]">
|
||||||
|
<div
|
||||||
|
class="bg-gray-900 border border-gray-800 p-8 rounded-xl shadow-2xl w-full max-w-md"
|
||||||
|
>
|
||||||
|
<h2 class="text-2xl font-semibold text-center mb-6 text-white">
|
||||||
|
Connexion
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<!-- Messages flash d'erreur -->
|
||||||
|
{% with messages = get_flashed_messages() %} {% if messages %}
|
||||||
|
<div
|
||||||
|
class="mb-4 p-3 bg-red-950 border border-red-800 text-red-300 text-sm rounded text-center"
|
||||||
|
>
|
||||||
|
{% for message in messages %} {{ message }} {% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %} {% endwith %}
|
||||||
|
|
||||||
|
<form method="POST" action="{{ url_for('main.index') }}" class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label for="login" class="block text-sm font-medium text-gray-300 mb-1"
|
||||||
|
>Login :</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="login"
|
||||||
|
name="login"
|
||||||
|
required
|
||||||
|
autocomplete="username"
|
||||||
|
class="w-full px-4 py-2 bg-gray-950 border border-gray-700 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-blue-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="pass" class="block text-sm font-medium text-gray-300 mb-1"
|
||||||
|
>Mot de passe :</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="pass"
|
||||||
|
name="pass"
|
||||||
|
required
|
||||||
|
autocomplete="current-password"
|
||||||
|
class="w-full px-4 py-2 bg-gray-950 border border-gray-700 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-blue-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="w-full mt-2 py-2.5 px-4 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition duration-200 cursor-pointer"
|
||||||
|
>
|
||||||
|
Valider
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
{% extends "base.html" %} {% block title %}Menu - Bolsa{% endblock %} {% block
|
||||||
|
header_title %}Tableau de bord{% endblock %} {% block content %}
|
||||||
|
<div class="flex flex-col items-center justify-center py-6">
|
||||||
|
<!-- Box centrée principale -->
|
||||||
|
<div
|
||||||
|
class="bg-gray-900 border border-gray-800 p-8 rounded-2xl shadow-2xl w-full max-w-4xl text-center"
|
||||||
|
>
|
||||||
|
<!-- Titre et sous-titre -->
|
||||||
|
<h2 class="text-3xl font-extrabold text-white mb-2">Espace de gestion</h2>
|
||||||
|
<p class="text-gray-400 text-sm mb-8">
|
||||||
|
Accédez aux fonctions clés de l'application financière.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Grille des 8 gros boutons -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-5 mb-8">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="py-6 px-6 bg-gray-950 hover:bg-blue-600/30 border-2 border-gray-800 hover:border-blue-500 rounded-xl text-white text-lg font-bold tracking-wide transition-all duration-200 flex items-center justify-center shadow-lg hover:scale-[1.01]"
|
||||||
|
>
|
||||||
|
1. Actions
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="py-6 px-6 bg-gray-950 hover:bg-blue-600/30 border-2 border-gray-800 hover:border-blue-500 rounded-xl text-white text-lg font-bold tracking-wide transition-all duration-200 flex items-center justify-center shadow-lg hover:scale-[1.01]"
|
||||||
|
>
|
||||||
|
2. Ordres
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="py-6 px-6 bg-gray-950 hover:bg-blue-600/30 border-2 border-gray-800 hover:border-blue-500 rounded-xl text-white text-lg font-bold tracking-wide transition-all duration-200 flex items-center justify-center shadow-lg hover:scale-[1.01]"
|
||||||
|
>
|
||||||
|
3. Comptes
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="py-6 px-6 bg-gray-950 hover:bg-blue-600/30 border-2 border-gray-800 hover:border-blue-500 rounded-xl text-white text-lg font-bold tracking-wide transition-all duration-200 flex items-center justify-center shadow-lg hover:scale-[1.01]"
|
||||||
|
>
|
||||||
|
4. Données perso
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="{{ url_for('main.gestion_utilisateurs') }}"
|
||||||
|
class="py-6 px-6 bg-gray-950 hover:bg-blue-600/30 border-2 border-gray-800 hover:border-blue-500 rounded-xl text-white text-lg font-bold tracking-wide transition-all duration-200 flex items-center justify-center shadow-lg hover:scale-[1.01]"
|
||||||
|
>
|
||||||
|
5. Gestion utilisateurs
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="py-6 px-6 bg-gray-950 hover:bg-blue-600/30 border-2 border-gray-800 hover:border-blue-500 rounded-xl text-white text-lg font-bold tracking-wide transition-all duration-200 flex items-center justify-center shadow-lg hover:scale-[1.01]"
|
||||||
|
>
|
||||||
|
6. Stat historique
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="py-6 px-6 bg-gray-950 hover:bg-blue-600/30 border-2 border-gray-800 hover:border-blue-500 rounded-xl text-white text-lg font-bold tracking-wide transition-all duration-200 flex items-center justify-center shadow-lg hover:scale-[1.01]"
|
||||||
|
>
|
||||||
|
7. Import/Export Actions CSV
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="py-6 px-6 bg-gray-950 hover:bg-blue-600/30 border-2 border-gray-800 hover:border-blue-500 rounded-xl text-white text-lg font-bold tracking-wide transition-all duration-200 flex items-center justify-center shadow-lg hover:scale-[1.01]"
|
||||||
|
>
|
||||||
|
8. Import historique CSV
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pied de formulaire interne -->
|
||||||
|
<div class="border-t border-gray-800 pt-4 text-xs text-gray-400 italic">
|
||||||
|
Positions en bourse : Accès sécurisé. Veillez à fermer votre session après
|
||||||
|
chaque utilisation.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -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é !")
|
||||||
@@ -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.
|
||||||
@@ -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:
|
||||||
Reference in New Issue
Block a user