From e784c85e360403dc8028b2a0a662b056e4ea42c3 Mon Sep 17 00:00:00 2001 From: Michel Date: Thu, 21 Aug 2025 09:31:40 +0200 Subject: [PATCH] =?UTF-8?q?Am=C3=A9lioration=20app=20Gestion=20users?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/creer_user.py | 365 +++++++++++++++++++++++++++++----------------- 1 file changed, 234 insertions(+), 131 deletions(-) diff --git a/app/creer_user.py b/app/creer_user.py index 375105b..3dd08a0 100644 --- a/app/creer_user.py +++ b/app/creer_user.py @@ -1,4 +1,4 @@ -# app_users.py — création + modification de champs (sans sidebar, .env only) +# app_users.py — création + modification de champs (wide + onglets + grille) import os import re from datetime import date, datetime @@ -9,7 +9,20 @@ from mysql.connector import errorcode import streamlit as st from dotenv import load_dotenv +# ----------------------- +# Auth minimale +# ----------------------- def require_login(): + st.markdown( + """ + + """, + unsafe_allow_html=True + ) load_dotenv() admin_user = os.getenv("ADMIN_USER", "admin") admin_hash = os.getenv("ADMIN_PASS_HASH") @@ -22,21 +35,36 @@ def require_login(): st.session_state.auth_ok = False if not st.session_state.auth_ok: - st.title("🔐 Accès restreint") - u = st.text_input("Utilisateur") - p = st.text_input("Mot de passe", type="password") - if st.button("Se connecter"): - if u == admin_user and bcrypt.checkpw(p.encode(), admin_hash.encode()): - st.session_state.auth_ok = True - st.rerun() # ✅ nouvelle fonction - else: - st.error("Identifiants invalides") + st.markdown("

🔐 Accès restreint

", unsafe_allow_html=True) + + # conteneur centré + login_left, login_center, login_right = st.columns([1, 2, 1]) + with login_center: + st.markdown( + """ +
+ """, + unsafe_allow_html=True + ) + u = st.text_input("Utilisateur", key="login_user") + p = st.text_input("Mot de passe", type="password", key="login_pass") + + if st.button("Se connecter", use_container_width=True): + if u == admin_user and bcrypt.checkpw(p.encode(), admin_hash.encode()): + st.session_state.auth_ok = True + st.rerun() + else: + st.error("Identifiants invalides") + + st.markdown("
", unsafe_allow_html=True) + st.stop() require_login() -# ====================== -# Connexion MySQL via .env -# ====================== + +# ----------------------- +# Connexion MySQL +# ----------------------- @st.cache_resource def get_connection(): load_dotenv() @@ -56,9 +84,9 @@ def get_connection(): host=host, port=port, user=user, password=pwd, database=db, autocommit=True ) -# ====================== +# ----------------------- # Utilitaires -# ====================== +# ----------------------- EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$") def normalize_phone(phone: str | None) -> str | None: @@ -69,7 +97,6 @@ def normalize_phone(phone: str | None) -> str | None: def to_sql_date(d: date | str) -> str: if isinstance(d, date): return d.strftime("%Y-%m-%d") - # accepte JJ/MM/AAAA for fmt in ("%Y-%m-%d", "%d/%m/%Y"): try: return datetime.strptime(d, fmt).strftime("%Y-%m-%d") @@ -156,7 +183,6 @@ def update_field(cnx, username: str, field: str, value): raise ValueError("Champ non autorisé.") sql_field = allowed[field] - # validations if field == "email": if not EMAIL_RE.match(str(value)): raise ValueError("Email invalide.") @@ -180,14 +206,27 @@ def update_password(cnx, username: str, new_password: str): ) return pwd_hash +# ----------------------- +# UI +# ----------------------- +st.set_page_config(page_title="Acces.Utilisateurs", page_icon="👤", layout="wide") -# ====================== -# UI Streamlit -# ====================== -st.set_page_config(page_title="Acces.Utilisateurs", page_icon="👤", layout="centered") -st.title("Acces.Utilisateurs") +# CSS doux +st.markdown( + """ + + """, + unsafe_allow_html=True +) -# Connexion +st.title("👤 Acces.Utilisateurs") + +# Connexion DB (badge) try: cnx = get_connection() st.caption("Connexion MySQL via `.env` : **OK** ✅") @@ -195,113 +234,185 @@ except Exception as e: st.error(f"Connexion MySQL impossible : {e}") st.stop() -# --- Création --- -with st.form("create_user_form", clear_on_submit=False): - st.subheader("Nouveau compte") +# Bouton Sortie / Exit +hdr_l, hdr_r = st.columns([1, 0.18]) +with hdr_r: + if st.button("🔒 SORTIE / EXIT", use_container_width=True, help="Se déconnecter et verrouiller l'accès"): + st.session_state.auth_ok = False + st.rerun() - col1, col2 = st.columns(2) - username = col1.text_input("NomUtilisateur", placeholder="ex: cjaquier") - full_name = col2.text_input("Nom_complet", placeholder="Clément JAQUIER") +# ----------------------- +# Onglets +# ----------------------- +tab_list, tab_create, tab_edit, tab_security = st.tabs( + ["📋 Liste", "🆕 Créer", "✏️ Modifier", "🔐 Sécurité"] +) - col3, col4 = st.columns(2) - site = col3.text_input("Site", placeholder="Roissy", value="Roissy") - email = col4.text_input("email", placeholder="prenom.nom@domaine.com") - - col5, col6 = st.columns(2) - phone = col5.text_input("Téléphone", placeholder="06 12 12 35 32") - expires = col6.date_input("DateExpiration", value=date.today()) - - col7, col8 = st.columns(2) - password = col7.text_input("Mot de passe (saisie)", type="password") - confirm = col8.text_input("Confirmer mot de passe", type="password") - - store_plain = st.checkbox("Remplir aussi MotDePasse en clair (déconseillé)", value=False) - submitted = st.form_submit_button("Créer l’utilisateur", use_container_width=True) - - if submitted: - if not username or not full_name or not site or not email or not password: - st.error("Merci de remplir tous les champs obligatoires.") - elif password != confirm: - st.error("Les mots de passe ne correspondent pas.") - else: - try: - pwd_hash = insert_user( - cnx, - username=username.strip(), - full_name=full_name.strip(), - site=site.strip(), - password=password, - expires=expires, - phone=phone.strip() if phone else None, - email=email.strip(), - store_plain=store_plain, - ) - st.success("Utilisateur créé avec succès ✅") - with st.expander("Voir le hash généré (MotDePasseHash)"): - st.code(pwd_hash) - except mysql.connector.Error as db_err: - if db_err.errno == errorcode.ER_ACCESS_DENIED_ERROR: - st.error("Identifiants MySQL invalides.") - else: - st.error(f"Erreur MySQL : {db_err}") - except Exception as e: - st.error(f"Erreur : {e}") - -st.divider() - -# --- Modification d'un champ --- -st.subheader("Modifier un utilisateur existant") - -# chargement liste -try: - users = list_users(cnx, limit=1000) - usernames = [u["NomUtilisateur"] for u in users] -except Exception as e: - users = [] - usernames = [] - st.warning(f"Impossible de charger la liste des utilisateurs : {e}") - -colA, colB = st.columns([2, 3]) -with colA: - sel_user = st.selectbox("Utilisateur", usernames, placeholder="Choisir un utilisateur") -with colB: - if sel_user: - details = get_user(cnx, sel_user) - if details: - st.caption(f"Actuel — Nom: **{details['Nom_complet']}**, Site: **{details['Site']}**, " - f"Expiration: **{details['DateExpiration']}**, Tél: **{details['Telephone']}**, " - f"Email: **{details['email']}**") - -col1, col2 = st.columns(2) -with col1: - field = st.selectbox("Champ à modifier", ["Nom_complet", "Site", "Telephone", "DateExpiration", "email"]) -with col2: - # widget adapté au champ - if field == "DateExpiration": - new_value = st.date_input("Nouvelle valeur (date)", value=date.today(), key="new_date") - elif field == "Telephone": - new_value = st.text_input("Nouvelle valeur (téléphone)", key="new_tel") - else: - new_value = st.text_input("Nouvelle valeur", key="new_text") - -btn_update = st.button("Mettre à jour le champ", type="primary", use_container_width=True, disabled=not sel_user) - -if btn_update and sel_user: +# --- Onglet Liste --- +with tab_list: + st.subheader("Utilisateurs récents") try: - update_field(cnx, sel_user, field, new_value) - st.success(f"✅ {field} mis à jour pour {sel_user}") - except mysql.connector.Error as db_err: - st.error(f"Erreur MySQL : {db_err}") - except Exception as e: - st.error(f"Erreur : {e}") + import pandas as pd + rows = list_users(cnx, limit=1000) + df = pd.DataFrame(rows) -# --- Réinitialisation mot de passe (optionnel) --- -with st.expander("🔐 Réinitialiser le mot de passe (bcrypt)"): - user_pw = st.selectbox("Utilisateur", usernames, key="pw_user", placeholder="Choisir un utilisateur") + if not df.empty: + df["DateExpiration"] = pd.to_datetime(df["DateExpiration"], errors="coerce").dt.date + + from datetime import date as _date + today = _date.today() + expired_mask = df["DateExpiration"] <= today + + col_a, col_b = st.columns(2) + col_a.metric("Total utilisateurs", len(df)) + col_b.metric("Comptes périmés", int(expired_mask.sum())) + + def style_expired(row): + if row["DateExpiration"] <= today: + return ["color: white; background-color: #d32f2f;"] * len(row) + return [""] * len(row) + styled = df.style.apply(style_expired, axis=1) + + visible_rows = len(df) + row_px = 36 + header_px = 48 + padding_px = 24 + height = min(1000, header_px + row_px * visible_rows + padding_px) + + st.dataframe( + styled, + use_container_width=True, + hide_index=True, + height=height, + ) + else: + st.info("Aucun utilisateur à afficher.") + except Exception as e: + st.warning(f"Impossible de lister les utilisateurs : {e}") + +# --- Onglet Créer --- +with tab_create: + with st.form("create_user_form", clear_on_submit=False): + st.subheader("Nouveau compte") + + c1, c2, c3 = st.columns([1.2, 1.5, 1]) + username = c1.text_input("NomUtilisateur", placeholder="ex: cjaquier") + full_name = c2.text_input("Nom_complet", placeholder="Clément JAQUIER") + site = c3.text_input("Site", placeholder="Roissy", value="Roissy") + + c4, c5, c6 = st.columns([1.4, 1, 1]) + email = c4.text_input("email", placeholder="prenom.nom@domaine.com") + phone = c5.text_input("Téléphone", placeholder="06 12 12 35 32") + expires = c6.date_input("DateExpiration", value=date.today()) + + c7, c8 = st.columns(2) + password = c7.text_input("Mot de passe (saisie)", type="password") + confirm = c8.text_input("Confirmer mot de passe", type="password") + + store_plain = st.checkbox("Remplir aussi MotDePasse en clair (déconseillé)", value=False) + submitted = st.form_submit_button("Créer l’utilisateur", use_container_width=True) + + if submitted: + if not username or not full_name or not site or not email or not password: + st.error("Merci de remplir tous les champs obligatoires.") + elif password != confirm: + st.error("Les mots de passe ne correspondent pas.") + else: + try: + pwd_hash = insert_user( + cnx, + username=username.strip(), + full_name=full_name.strip(), + site=site.strip(), + password=password, + expires=expires, + phone=phone.strip() if phone else None, + email=email.strip(), + store_plain=store_plain, + ) + st.success("Utilisateur créé avec succès ✅") + with st.expander("Voir le hash généré (MotDePasseHash)"): + st.code(pwd_hash) + except mysql.connector.Error as db_err: + if db_err.errno == errorcode.ER_ACCESS_DENIED_ERROR: + st.error("Identifiants MySQL invalides.") + else: + st.error(f"Erreur MySQL : {db_err}") + except Exception as e: + st.error(f"Erreur : {e}") + +# --- Onglet Modifier --- +with tab_edit: + st.subheader("Modifier un utilisateur existant") + + try: + users = list_users(cnx, limit=1000) + usernames = [u["NomUtilisateur"] for u in users] + except Exception as e: + users = [] + usernames = [] + st.warning(f"Impossible de charger la liste des utilisateurs : {e}") + + top_left, top_right = st.columns([1.2, 2]) + + with top_left: + sel_user = st.selectbox("Utilisateur", usernames, placeholder="Choisir un utilisateur") + + field = st.selectbox( + "Champ à modifier", + ["Nom_complet", "Site", "Telephone", "DateExpiration", "email"] + ) + + if field == "DateExpiration": + new_value = st.date_input("Nouvelle valeur (date)", value=date.today(), key="new_date") + elif field == "Telephone": + new_value = st.text_input("Nouvelle valeur (téléphone)", key="new_tel") + else: + new_value = st.text_input("Nouvelle valeur", key="new_text") + + update_btn = st.button( + "Mettre à jour le champ", + type="primary", + use_container_width=True, + disabled=not sel_user, + ) + + with top_right: + if sel_user: + details = get_user(cnx, sel_user) + if details: + c1, c2, c3, c4, c5 = st.columns(5, gap="small") + c1.markdown(f"
Nom
{details['Nom_complet'] or ''}
", unsafe_allow_html=True) + c2.markdown(f"
Site
{details['Site'] or ''}
", unsafe_allow_html=True) + c3.markdown(f"
Expire
{details['DateExpiration'] or ''}
", unsafe_allow_html=True) + c4.markdown(f"
Tél
{details['Telephone'] or ''}
", unsafe_allow_html=True) + c5.markdown(f"
Email
{details['email'] or ''}
", unsafe_allow_html=True) + + if update_btn and sel_user: + try: + update_field(cnx, sel_user, field, new_value) + st.success(f"✅ {field} mis à jour pour {sel_user}") + except mysql.connector.Error as db_err: + st.error(f"Erreur MySQL : {db_err}") + except Exception as e: + st.error(f"Erreur : {e}") + +# --- Onglet Sécurité --- +with tab_security: + st.subheader("Réinitialiser le mot de passe (bcrypt)") + try: + if 'user_cache_for_pw' not in st.session_state: + st.session_state.user_cache_for_pw = [u["NomUtilisateur"] for u in list_users(cnx, limit=1000)] + pw_user_list = st.session_state.user_cache_for_pw + except Exception: + pw_user_list = [] + + user_pw = st.selectbox("Utilisateur", pw_user_list, key="pw_user", placeholder="Choisir un utilisateur") colp1, colp2 = st.columns(2) new_pw = colp1.text_input("Nouveau mot de passe", type="password") new_pw2 = colp2.text_input("Confirmer", type="password") - if st.button("Mettre à jour le mot de passe", disabled=not user_pw): + + if st.button("Mettre à jour le mot de passe", disabled=not user_pw, use_container_width=True): if not new_pw: st.error("Mot de passe vide.") elif new_pw != new_pw2: @@ -314,11 +425,3 @@ with st.expander("🔐 Réinitialiser le mot de passe (bcrypt)"): st.code(h) except Exception as e: st.error(f"Erreur : {e}") - -st.divider() -st.subheader("Utilisateurs récents") -try: - rows = list_users(cnx, limit=200) - st.dataframe(rows, use_container_width=True, hide_index=True) -except Exception as e: - st.warning(f"Impossible de lister les utilisateurs : {e}")