Spaces:
Sleeping
Sleeping
import os | |
import pytz | |
import json | |
import uuid | |
import base64 | |
import streamlit as st | |
from supabase import create_client, Client | |
import requests | |
import bcrypt | |
import jwt | |
from dotenv import load_dotenv | |
import functools | |
from datetime import datetime, timedelta | |
from datetime import datetime | |
from io import BytesIO | |
from PIL import Image | |
import time | |
import traceback | |
from functools import lru_cache | |
import extra_streamlit_components as stx | |
from contextlib import contextmanager | |
from typing import Dict, List, Optional, Tuple, Any | |
from typing import TypedDict, Union | |
from utils.password_reset import initiate_password_reset | |
from utils.notification_handler import notify_security_alert #import du module de notifications (fonction alerte d'une nouvelle connexio) | |
from utils.notification_handler import send_daily_summary #import du module de notifications (fonction du résumé quotidien, voire la fonction def generate_user_summary(user_id: int) dans ce fichier) | |
from utils.email_utils import send_email | |
import secrets # Concerne l'email de vérification lors de l'inscription d'un nouvels utilisateur | |
import logging # gardez l'import de logging même si nous n'utilisons plus directement ses fonctions de log, car nous avons toujours besoin de ses constantes de niveau (level=logging.INFO/ERROR/WARNING) | |
from utils.logging_utils import log_to_file # système de logging défini sur un fichier séparé pour une meilleure lisibilité. | |
from utils.theme_utils import ( | |
save_theme_preference, | |
load_theme_preference, | |
apply_theme, | |
inject_custom_css, | |
add_sidebar_logo, | |
apply_chat_styles, | |
inject_custom_toggle_css, | |
create_custom_footer, | |
theme_switcher, | |
remove_streamlit_style, | |
force_streamlit_style_update, | |
hide_pages | |
) | |
import asyncio | |
from utils.async_flowise import AsyncFlowiseClient | |
from utils.cache_manager import CacheManager | |
from admin_page import render_admin_dashboard | |
import tracemalloc | |
tracemalloc.start() | |
from utils.auth_cache import AuthenticationCache # Importation de la classe d'authentification | |
auth_cache = AuthenticationCache() # Créez une instance globale de AuthenticationCache pour utiliser ses focntions d'authentification et de cache au sein de l'application | |
from utils.ip_utils import get_user_ip | |
st.set_page_config( | |
page_title="Colibri - Votre assistant intelligent", # ou un titre approprié | |
page_icon=":bar_chart:", | |
layout="wide", | |
) | |
# Initialisez le cache manager globalement | |
cache_manager = CacheManager() | |
# Charger les variables d'environnement | |
load_dotenv() | |
SUPABASE_URL = os.getenv("SUPABASE_URL") | |
SUPABASE_KEY = os.getenv("SUPABASE_KEY") | |
JWT_SECRET = os.getenv("JWT_SECRET") | |
FLOWISE_API_URL_SANTE = os.getenv("FLOWISE_API_URL_SANTE") | |
FLOWISE_API_URL_CAR = os.getenv("FLOWISE_API_URL_CAR") | |
FLOWISE_API_URL_BTP = os.getenv("FLOWISE_API_URL_BTP") | |
FLOWISE_API_URL_RH = os.getenv("FLOWISE_API_URL_RH") | |
FLOWISE_API_URL_PILOTAGE = os.getenv("FLOWISE_API_URL_PILOTAGE") | |
# Configuration du logging | |
supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY) | |
# =============================================================================== | |
# début de l'implémentation des focntions de génération du résumé quatidien pour envoyer une notification à l'utilisateur ayant activé cette notif | |
def start_activity_tracking(user_id: int): | |
"""Démarre une nouvelle session de tracking d'activité""" | |
try: | |
# Vérifier s'il existe déjà une session active | |
active_tracking = supabase.table("user_activity_tracking").select("*").eq( | |
"user_id", user_id | |
).is_("session_end", None).execute() | |
if not active_tracking.data: | |
# Créer une nouvelle session de tracking | |
supabase.table("user_activity_tracking").insert({ | |
"user_id": user_id, | |
"session_start": datetime.now(pytz.UTC).isoformat(), | |
"date_tracked": datetime.now(pytz.UTC).date().isoformat() | |
}).execute() | |
except Exception as e: | |
log_to_file(f"Erreur lors du démarrage du tracking : {str(e)}", level=logging.ERROR) | |
def end_activity_tracking(user_id: int): | |
"""Termine la session de tracking active""" | |
try: | |
# Récupérer la session active | |
active_tracking = supabase.table("user_activity_tracking").select("*").eq( | |
"user_id", user_id | |
).is_("session_end", None).execute() | |
if active_tracking.data: | |
tracking = active_tracking.data[0] | |
session_start = datetime.fromisoformat(tracking['session_start'].replace('Z', '+00:00')) | |
session_end = datetime.now(pytz.UTC) | |
duration = int((session_end - session_start).total_seconds() / 60) | |
# Mettre à jour la session | |
supabase.table("user_activity_tracking").update({ | |
"session_end": session_end.isoformat(), | |
"duration_minutes": duration | |
}).eq("id", tracking['id']).execute() | |
except Exception as e: | |
log_to_file(f"Erreur lors de la fin du tracking : {str(e)}", level=logging.ERROR) | |
def calculate_usage_time(user_id: int) -> int: | |
"""Calcule le temps total d'utilisation en minutes pour la journée""" | |
try: | |
today = datetime.now(pytz.UTC).date() | |
# Récupérer toutes les sessions terminées de la journée | |
completed_sessions = supabase.table("user_activity_tracking").select( | |
"duration_minutes" | |
).eq("user_id", user_id).eq( | |
"date_tracked", today.isoformat() | |
).not_.is_("session_end", None).execute() | |
# Calculer le temps des sessions terminées | |
total_minutes = sum( | |
session.get('duration_minutes', 0) | |
for session in completed_sessions.data | |
if session.get('duration_minutes') is not None | |
) | |
# Ajouter le temps de la session active si elle existe | |
active_session = supabase.table("user_activity_tracking").select("*").eq( | |
"user_id", user_id | |
).is_("session_end", None).execute() | |
if active_session.data: | |
session_start = datetime.fromisoformat( | |
active_session.data[0]['session_start'].replace('Z', '+00:00') | |
) | |
current_time = datetime.now(pytz.UTC) | |
active_duration = int((current_time - session_start).total_seconds() / 60) | |
total_minutes += active_duration | |
return total_minutes | |
except Exception as e: | |
log_to_file(f"Erreur lors du calcul du temps d'utilisation : {str(e)}", level=logging.ERROR) | |
return 0 | |
def generate_user_summary(user_id: int): | |
"""Génère et envoie un résumé quotidien à l'utilisateur""" | |
try: | |
today = datetime.now(pytz.UTC).date() | |
# Compter les conversations et messages | |
conversations = get_conversation_count(user_id) | |
messages = get_messages_count(user_id) | |
usage_time = calculate_usage_time(user_id) # Utilise la nouvelle méthode | |
summary_data = { | |
"conversations": conversations, | |
"questions": messages, | |
"usage_time": usage_time | |
} | |
# Envoyer le résumé | |
send_daily_summary(user_id, summary_data) | |
except Exception as e: | |
log_to_file(f"Erreur lors de la génération du résumé : {str(e)}", level=logging.ERROR) | |
def check_and_send_daily_summary(): | |
"""Vérifie et envoie le résumé quotidien si nécessaire""" | |
try: | |
if 'user' in st.session_state and 'last_summary_sent' not in st.session_state: | |
user_id = st.session_state['user']['id'] | |
# Récupérer les préférences de notification | |
prefs = supabase.table("notification_preferences").select("*").eq("user_id", user_id).execute() | |
if prefs.data and prefs.data[0].get('daily_summary'): | |
# Générer et envoyer le résumé | |
summary_data = { | |
"conversations": get_conversation_count(user_id), | |
"questions": get_messages_count(user_id), | |
"usage_time": calculate_usage_time(user_id) | |
} | |
if send_daily_summary(user_id, summary_data): | |
# Marquer comme envoyé pour aujourd'hui | |
st.session_state['last_summary_sent'] = datetime.now(pytz.UTC).date() | |
log_to_file(f"Résumé quotidien envoyé avec succès pour l'utilisateur {user_id}", level=logging.INFO) | |
else: | |
log_to_file(f"Échec de l'envoi du résumé quotidien pour l'utilisateur {user_id}", level=logging.ERROR) | |
except Exception as e: | |
log_to_file(f"Erreur lors de la vérification du résumé quotidien : {str(e)}", level=logging.ERROR) | |
def get_conversation_count(user_id: int) -> int: | |
"""Compte le nombre de conversations de la journée""" | |
try: | |
today = datetime.now(pytz.UTC).date() | |
result = supabase.table("chat_sessions").select( | |
"count", count="exact" | |
).eq("user_id", user_id).gte( | |
"created_at", today.isoformat() | |
).execute() | |
return result.count if hasattr(result, 'count') else 0 | |
except Exception as e: | |
log_to_file(f"Erreur lors du comptage des conversations : {str(e)}", level=logging.ERROR) | |
return 0 | |
def get_messages_count(user_id: int) -> int: | |
"""Compte le nombre de messages de la journée""" | |
try: | |
today = datetime.now(pytz.UTC).date() | |
# Récupérer les sessions actives de l'utilisateur aujourd'hui | |
sessions = supabase.table("chat_sessions").select("session_id").eq( | |
"user_id", user_id | |
).gte("created_at", today.isoformat()).execute() | |
sessions = supabase.table("chat_sessions").select("session_id").eq( | |
"user_id", user_id | |
).gte("created_at", today.isoformat()).execute() | |
if not sessions.data: | |
return 0 | |
# Récupérer les IDs des sessions | |
session_ids = [session['session_id'] for session in sessions.data] | |
# Compter les messages dans ces sessions | |
result = supabase.table("messages").select( | |
"count", count="exact" | |
).in_("session_id", session_ids).eq( | |
"role", "user" # Pour ne compter que les messages de l'utilisateur | |
).execute() | |
return result.count if hasattr(result, 'count') else 0 | |
except Exception as e: | |
log_to_file(f"Erreur lors du comptage des messages : {str(e)}", level=logging.ERROR) | |
return 0 | |
# Fin de l'implémentation des focntions de génération du résumé quatidien pour envoyer une notification à l'utilisateur ayant activé cette notif | |
# =============================================================================== | |
# =============================================================================== | |
# Début de la section: Gestion des du cach et des erreurs | |
#implémenter une gestion des erreurs et des exceptions plus robuste | |
def global_exception_handler(func): | |
def wrapper(*args, **kwargs): | |
try: | |
return func(*args, **kwargs) | |
except Exception as e: | |
error_message = f"Une erreur s'est produite : {str(e)}" | |
st.error(error_message) | |
log_to_file(f"Erreur non gérée : {error_message}\n{traceback.format_exc()}", level=logging.ERROR) | |
# Vous pouvez ajouter ici une logique pour notifier les administrateurs | |
return wrapper | |
#décorateur @global_exception_handler pour la fonction plus haut def global_exeption_hundler (func) | |
def handle_error(error_message): | |
st.error(f"Une erreur s'est produite : {error_message}") | |
log_to_file(f"Erreur : {error_message}", level=logging.ERROR) | |
# Structure pour stocker le cache | |
cache_store = {} | |
def timed_cache(expires_after=timedelta(minutes=30)): | |
def decorator(func): | |
def wrapper(*args, **kwargs): | |
# Créer une clé unique pour cette requête | |
cache_key = f"{func.__name__}:{str(args)}:{str(kwargs)}" | |
# Vérifier si nous avons une réponse en cache et si elle est encore valide | |
if cache_key in cache_store: | |
result, timestamp = cache_store[cache_key] | |
if datetime.now() - timestamp < expires_after: | |
st.info("Réponse récupérée du cache") | |
return result | |
# Si pas de cache ou cache expiré, exécuter la fonction | |
result = func(*args, **kwargs) | |
# Stocker le résultat dans le cache | |
cache_store[cache_key] = (result, datetime.now()) | |
return result | |
return wrapper | |
return decorator | |
def expensive_operation(param): | |
""" | |
Fonction simulant une opération coûteuse. | |
""" | |
# Simuler une opération coûteuse | |
time.sleep(2) | |
return param * 2 | |
# Fin de la section: Gestion des du cach et des erreurs | |
# =============================================================================== | |
# ================================================================================ | |
# Début de la section: Authentification (login, logout) | |
# Regroupe les fonctions par rôle spécifique dans le processus d'authentification. | |
# Suit un ordre logique : | |
# - Interface utilisateur | |
# - Gestion des données d'authentification | |
# - Vérification des informations d'authentification | |
# - Initialisation de la session utilisateur | |
# Facilite la compréhension du flux d'authentification. | |
async def initialize_app(): | |
"""Initialise l'application et gère le cache Redis""" | |
try: | |
log_to_file("Démarrage de l'application - Vérification du cache", level=logging.INFO) | |
if not await auth_cache.is_cache_initialized(): | |
# Premier chargement du cache | |
log_to_file("Cache vide - Chargement initial", level=logging.INFO) | |
success = await auth_cache.load_all_users_to_cache() | |
if success: | |
await auth_cache.update_cache_version() | |
# Charger aussi les sessions | |
await auth_cache.load_user_sessions_to_cache() | |
log_to_file("Chargement initial du cache réussi", level=logging.INFO) | |
else: | |
log_to_file("Échec du chargement initial du cache", level=logging.ERROR) | |
elif await auth_cache.is_cache_outdated(): | |
# Mise à jour du cache existant | |
log_to_file("Cache existant - Synchronisation des modifications", level=logging.INFO) | |
await auth_cache.sync_cache_with_db() | |
await auth_cache.load_user_sessions_to_cache() # Recharger les sessions | |
else: | |
log_to_file("Cache à jour - Aucune action nécessaire", level=logging.INFO) | |
# Debug optionnel | |
await auth_cache.debug_redis_content() | |
except Exception as e: | |
log_to_file(f"Erreur lors de l'initialisation : {str(e)}", level=logging.ERROR) | |
def authentication_page(): | |
hide_pages() # cacher les pages non désirée de la sidebar dans la page d'authentification | |
st.title("Bienvenue sur votre application RAG") | |
menu = ["Se connecter", "S'inscrire"] | |
choice = st.sidebar.selectbox("Menu", menu) | |
if choice == "Se connecter": | |
with st.form("login_form"): | |
st.subheader("Connexion") | |
email = st.text_input("Email") | |
password = st.text_input("Mot de passe", type='password') | |
# Créer deux colonnes pour les boutons | |
col1, col2 = st.columns([1, 1]) | |
with col1: | |
if st.form_submit_button("Connexion"): | |
success, result = login_user(email, password) | |
if success: | |
st.success(result) | |
st.rerun() | |
else: | |
st.error(result) | |
with col2: | |
if st.form_submit_button("Mot de passe oublié ?"): | |
st.session_state['show_reset'] = True | |
# Formulaire de réinitialisation du mot de passe | |
if st.session_state.get('show_reset', False): | |
with st.form("reset_form", clear_on_submit=True): | |
st.subheader("Réinitialisation du mot de passe") | |
reset_email = st.text_input("Entrez votre email") | |
if st.form_submit_button("Envoyer les instructions"): | |
if reset_email: | |
with st.spinner("Envoi en cours..."): | |
success, msg = initiate_password_reset(reset_email) | |
if success: | |
st.success(msg) | |
st.session_state['show_reset'] = False | |
else: | |
st.error(msg) | |
else: | |
st.error("Veuillez entrer votre email") | |
elif choice == "S'inscrire": | |
with st.form("signup_form"): | |
st.subheader("Inscription") | |
col1, col2 = st.columns(2) | |
with col1: | |
email = st.text_input("Email") | |
password = st.text_input("Mot de passe", type='password') | |
password_confirm = st.text_input("Confirmez le mot de passe", type='password') | |
with col2: | |
nom = st.text_input("Nom") | |
prenom = st.text_input("Prénom") | |
profession = st.text_input("Profession") | |
entreprise = st.text_input("Entreprise") | |
# Option pour créer un admin (visible uniquement pour les admins connectés) | |
create_as_admin = False | |
# Vérification sécurisée du rôle administrateur | |
is_admin = ( | |
'user' in st.session_state | |
and isinstance(st.session_state.get('user'), dict) | |
and st.session_state['user'].get('role') == 'admin' | |
) | |
if is_admin: | |
create_as_admin = st.checkbox("Créer en tant qu'administrateur") | |
submit_button = st.form_submit_button("Inscription") | |
if submit_button: | |
if password != password_confirm: | |
st.error("Les mots de passe ne correspondent pas.") | |
else: | |
professional_info = { | |
"profession": profession, | |
"entreprise": entreprise | |
} | |
# Définir le rôle en fonction du choix | |
role = 'admin' if create_as_admin and is_admin else 'user' | |
success, result = register_user( | |
email=email, | |
password=password, | |
nom=nom, | |
prenom=prenom, | |
professional_info=professional_info, | |
role=role | |
) | |
if success: | |
st.success(result) | |
else: | |
st.error(result) | |
def check_access_validity(ip_address: str, email: str) -> Tuple[bool, str]: | |
""" | |
Vérifie à la fois les restrictions d'IP et de domaine email. | |
Args: | |
ip_address (str): L'adresse IP à vérifier | |
email (str): L'adresse email à vérifier | |
Returns: | |
Tuple[bool, str]: Un tuple contenant: | |
- bool: True si l'accès est autorisé, False sinon | |
- str: Message d'erreur si l'accès est refusé, chaîne vide si autorisé | |
""" | |
try: | |
# Récupérer toutes les restrictions actives en une seule requête | |
restrictions = supabase.table("access_restrictions").select("*").eq("is_active", True).execute() | |
# Si pas de restrictions actives, autoriser l'accès | |
if not restrictions.data: | |
return True, "" | |
restrictions_data = restrictions.data[0] | |
# Vérifier les restrictions d'IP | |
allowed_ip_ranges = restrictions_data.get('allowed_ip_ranges', []) | |
if allowed_ip_ranges: | |
# Extraire l'adresse IP du dictionnaire | |
user_ip = ip_address.get('ip') if isinstance(ip_address, dict) else ip_address | |
if not any(user_ip == allowed_ip for allowed_ip in allowed_ip_ranges): | |
return False, "Votre adresse IP n'est pas autorisée à accéder à cette application." | |
# Vérifier les restrictions de domaine email | |
allowed_domains = restrictions_data.get('allowed_email_domains', []) | |
if allowed_domains: | |
email_domain = email.split('@')[1].lower() | |
if not any(domain.lower() == email_domain for domain in allowed_domains): | |
return False, "Votre domaine d'adresse email n'est pas autorisé à accéder à cette application." | |
# Si toutes les vérifications sont passées | |
return True, "" | |
except Exception as e: | |
error_msg = f"Erreur lors de la vérification des restrictions : {str(e)}" | |
log_to_file(error_msg, level=logging.ERROR) | |
return False, error_msg | |
#décorateur @global_exception_handler pour la fonction plus haut def global_exeption_hundler (func) | |
def register_user(email: str, password: str, nom: str, prenom: str, professional_info: Optional[Dict[str, Any]] = None, role: Optional[str] = None) -> tuple[bool, str]: | |
""" | |
Enregistre un nouvel utilisateur avec vérification d'email et d'IP. | |
Si c'est le premier utilisateur, il sera automatiquement défini comme administrateur. | |
Si c'est un utilisateur existant, il sera automatiquement défini comme utilisateur normal. | |
""" | |
try: | |
ip_address = get_user_ip() | |
log_to_file(f"(def register_user)-Adresse IP de l'utilisateur : {ip_address}", level=logging.INFO) # Afficher l'adresse IP pour le debug | |
# Vérifier les restrictions d'IP et d'email en une seule fois | |
is_valid, error_message = check_access_validity(ip_address, email) | |
if not is_valid: | |
return False, error_message | |
# Vérifier si c'est le premier utilisateur (sera admin par défaut) | |
users_count = supabase.table("users").select("count", count="exact").execute() | |
is_first_user = users_count.count == 0 if hasattr(users_count, 'count') else True | |
# Vérifier si l'email existe déjà | |
existing_user = supabase.table("users").select("email").eq("email", email).execute() | |
if existing_user.data: | |
return False, "Cet email est déjà utilisé." | |
# Hasher le mot de passe | |
hashed_password = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode() | |
# Déterminer le rôle | |
if is_first_user: | |
assigned_role = 'admin' # Premier utilisateur est toujours admin | |
else: | |
assigned_role = role if role in ['admin', 'user'] else 'user' | |
# Préparer les données utilisateur | |
user_data = { | |
"email": email, | |
"password": hashed_password, | |
"nom": nom, | |
"prenom": prenom, | |
"role": assigned_role, | |
"is_active": False, # L'utilisateur sera activé après vérification | |
"created_at": datetime.now(pytz.UTC).isoformat(), | |
"professional_info": professional_info or {} | |
} | |
# Insérer l'utilisateur | |
data = supabase.table("users").insert(user_data).execute() | |
if data.data: | |
user_id = data.data[0]['id'] | |
# Initialiser les permissions par défaut pour les assistants | |
default_permissions = [ | |
{"user_id": user_id, "assistant_type": "insuranceSANTE", "is_authorized": False}, | |
{"user_id": user_id, "assistant_type": "insuranceCAR", "is_authorized": False}, | |
{"user_id": user_id, "assistant_type": "insuranceBTP", "is_authorized": False}, | |
{"user_id": user_id, "assistant_type": "Pilotage", "is_authorized": False}, | |
{"user_id": user_id, "assistant_type": "RH", "is_authorized": False} | |
] | |
# Si c'est le premier utilisateur (admin), autoriser tous les assistants | |
if is_first_user: | |
for perm in default_permissions: | |
perm["is_authorized"] = True | |
# Insérer les permissions | |
supabase.table("user_assistant_permissions").insert(default_permissions).execute() | |
# Initialiser les préférences de notification | |
default_notifications = { | |
"user_id": user_id, | |
"email_notifications": True, | |
"security_alerts": True, | |
"assistant_updates": True, | |
"daily_summary": True, | |
"created_at": datetime.now(pytz.UTC).isoformat(), | |
"updated_at": datetime.now(pytz.UTC).isoformat() | |
} | |
# Insérer les préférences de notification | |
supabase.table("notification_preferences").insert(default_notifications).execute() | |
# Créer un token de vérification | |
verification_token = secrets.token_urlsafe(32) | |
expires_at = datetime.now(pytz.UTC) + timedelta(days=7) | |
# Enregistrer le token de vérification | |
supabase.table("email_verification").insert({ | |
"user_id": user_id, | |
"email": email, | |
"verification_token": verification_token, | |
"expires_at": expires_at.isoformat() | |
}).execute() | |
# Envoyer l'email de vérification | |
send_verification_email(email, verification_token) | |
log_to_file(f"Nouvel utilisateur enregistré: {email} avec le rôle: {assigned_role}", level=logging.INFO) | |
if is_first_user: | |
return True, "Inscription réussie ! Vous êtes le premier utilisateur et avez été défini comme administrateur. Veuillez vérifier votre email pour activer votre compte." | |
return True, "Inscription réussie ! Veuillez vérifier votre email pour activer votre compte." | |
return False, "Erreur lors de l'enregistrement." | |
except Exception as e: | |
log_to_file(f"Erreur lors de l'inscription : {str(e)}", level=logging.ERROR) | |
return False, f"Erreur lors de l'inscription : {str(e)}" | |
async def login_user_async(email: str, password: str) -> tuple[bool, str]: | |
""" | |
Connecte un utilisateur de manière asynchrone avec vérification des restrictions. | |
Les utilisateurs ayant le rôle "admin" sont exemptés des restrictions. | |
Args: | |
email (str): L'adresse email de l'utilisateur | |
password (str): Le mot de passe de l'utilisateur | |
Returns: | |
Tuple[bool, str]: Un tuple contenant: | |
- bool: True si la connexion est réussie, False sinon | |
- str: Message d'erreur si la connexion a échoué, chaîne vide si réussie | |
""" | |
try: | |
ip_address = get_user_ip() | |
# Récupérer l'utilisateur | |
cached_user = await auth_cache.get_user_from_cache(email) | |
if cached_user: | |
# Utilisateur trouvé dans le cache | |
log_to_file(f"Utilisateur trouvé dans le cache: {email}", level=logging.INFO) | |
user = cached_user | |
else: | |
# Si pas dans le cache, requête Supabase | |
user_query = supabase.table("users").select("*").eq("email", email).execute() | |
if not user_query.data: | |
return False, "Utilisateur non trouvé." | |
user = user_query.data[0] | |
# Mettre en cache pour les prochaines fois | |
await auth_cache.set_user_in_cache(user) | |
log_to_file(f"Utilisateur mis en cache: {email}", level=logging.INFO) | |
# Vérifier le rôle de l'utilisateur | |
is_admin = user['role'] == 'admin' | |
# Si l'utilisateur n'est pas admin, vérifier les restrictions d'IP et de domaine email | |
if not is_admin: | |
is_valid, error_message = check_access_validity(ip_address, email) | |
if not is_valid: | |
return False, error_message | |
# Vérifier si le compte est actif | |
if not user.get('is_active', True): | |
return False, "Ce compte a été désactivé. Contactez l'administrateur." | |
# Vérifier le mot de passe | |
if not bcrypt.checkpw(password.encode(), user['password'].encode()): | |
return False, "Mot de passe incorrect." | |
# Mettre à jour la dernière connexion | |
current_time = datetime.now(pytz.UTC).isoformat() | |
supabase.table("users").update({ | |
"last_login": current_time | |
}).eq("id", user['id']).execute() | |
# Mettre à jour le cache avec la nouvelle date de connexion | |
user['last_login'] = current_time | |
await auth_cache.set_user_in_cache(user) | |
# Générer le token JWT | |
expires_at = datetime.now(pytz.UTC) + timedelta(days=1) | |
token = jwt.encode({ | |
"id": user['id'], | |
"email": email, | |
"nom": user['nom'], | |
"prenom": user['prenom'], | |
"role": user['role'], | |
"exp": expires_at.timestamp() | |
}, JWT_SECRET, algorithm="HS256") | |
# Gérer la session | |
session_data = { | |
"token": token, | |
"expires_at": expires_at.isoformat() | |
} | |
existing_session = supabase.table("user_sessions").select("*").eq("user_id", user['id']).execute() | |
if existing_session.data: | |
supabase.table("user_sessions").update(session_data).eq("user_id", user['id']).execute() | |
else: | |
session_data["user_id"] = user['id'] | |
supabase.table("user_sessions").insert(session_data).execute() | |
# Configurer la session state | |
st.session_state['user'] = jwt.decode(token, JWT_SECRET, algorithms=["HS256"]) | |
st.session_state['user_theme'] = load_theme_preference() | |
st.session_state['authentication_status'] = True | |
st.session_state['user_id'] = user['id'] | |
# Envoyer la notification de connexion | |
notify_security_alert(user['id'], | |
f"Nouvelle connexion détectée le {datetime.now(pytz.UTC).strftime('%Y-%m-%d à %H:%M')} UTC" | |
) | |
log_to_file(f"Connexion réussie pour l'utilisateur: {email}") | |
return True, "Connexion réussie" | |
except Exception as e: | |
log_to_file(f"Erreur lors de la connexion : {str(e)}", logging.ERROR) | |
return False, f"Erreur lors de la connexion : {str(e)}" | |
def login_user(email: str, password: str) -> tuple[bool, str]: | |
"""Version synchrone de login_user pour Streamlit""" | |
import asyncio | |
try: | |
loop = asyncio.new_event_loop() | |
asyncio.set_event_loop(loop) | |
return loop.run_until_complete(login_user_async(email, password)) | |
except Exception as e: | |
log_to_file(f"Erreur lors de la connexion synchrone : {str(e)}", logging.ERROR) | |
return False, f"Erreur lors de la connexion : {str(e)}" | |
async def logout(): | |
try: | |
if 'user_id' in st.session_state: | |
user_id = st.session_state['user_id'] | |
email = st.session_state.get('user', {}).get('email') | |
# Notification de déconnexion | |
notify_security_alert(user_id, | |
f"Déconnexion effectuée le {datetime.now(pytz.UTC).strftime('%Y-%m-%d à %H:%M')} UTC" | |
) | |
# Supprimer du cache | |
if email: | |
await cache_manager.delete(f"user_email:{email}") | |
# Supprimer la session utilisateur | |
supabase.table("user_sessions").delete().eq("user_id", user_id).execute() | |
# Désactiver les sessions de chat | |
supabase.table("chat_sessions").update({ | |
"is_active": False | |
}).eq("user_id", user_id).eq("is_active", True).execute() | |
except Exception as e: | |
log_to_file(f"Erreur lors de la déconnexion : {str(e)}") | |
finally: | |
# Nettoyer la session state | |
keys_to_clear = [ | |
'authentication_status', | |
'user', | |
'user_id', | |
'user_theme', | |
'chat_history', | |
'current_chat_session' | |
] | |
for key in keys_to_clear: | |
if key in st.session_state: | |
del st.session_state[key] | |
st.rerun() | |
def sync_logout(): | |
"""Wrapper synchrone pour logout""" | |
import asyncio | |
asyncio.run(logout()) | |
def check_authentication(): | |
"""Vérifie la validité de l'authentification de l'utilisateur""" | |
try: | |
# Si l'utilisateur n'est pas dans la session, vérifier dans la base de données | |
if 'user_id' not in st.session_state: | |
# Récupérer toutes les sessions non expirées | |
current_time = datetime.now(pytz.UTC) | |
sessions = supabase.table("user_sessions").select("*").gt("expires_at", current_time.isoformat()).execute() | |
if sessions.data: | |
session = sessions.data[0] # Prendre la session la plus récente | |
try: | |
# Vérifier si le token est valide | |
user = jwt.decode(session['token'], JWT_SECRET, algorithms=["HS256"]) | |
# Si le token est valide, restaurer la session | |
st.session_state['user'] = user | |
st.session_state['user_id'] = session['user_id'] | |
st.session_state['authentication_status'] = True | |
st.session_state['user_theme'] = load_theme_preference() | |
# Rafraîchir le token si nécessaire | |
expires_at = datetime.fromisoformat(session['expires_at'].replace('Z', '+00:00')) | |
if expires_at - current_time < timedelta(hours=1): | |
refresh_token() | |
return True | |
except jwt.InvalidTokenError: | |
pass | |
return False | |
# Si l'utilisateur est dans la session, vérifier si le token est toujours valide | |
user_id = st.session_state['user_id'] | |
session = supabase.table("user_sessions").select("*").eq("user_id", user_id).order('created_at', desc=True).limit(1).execute() | |
if session.data: | |
session = session.data[0] | |
try: | |
expires_at = datetime.fromisoformat(session['expires_at'].replace('Z', '+00:00')) | |
if expires_at > datetime.now(pytz.UTC): | |
# Vérifier si le token est valide | |
jwt.decode(session['token'], JWT_SECRET, algorithms=["HS256"]) | |
# Rafraîchir le token si nécessaire | |
if expires_at - datetime.now(pytz.UTC) < timedelta(hours=1): | |
refresh_token() | |
return True | |
except jwt.InvalidTokenError: | |
pass | |
# Si on arrive ici, la session n'est plus valide | |
sync_logout() | |
return False | |
except Exception as e: | |
print(f"Erreur lors de la vérification de l'authentification : {str(e)}") | |
return False | |
def send_verification_email(email: str, token: str) -> bool: | |
"""Envoie l'email de vérification""" | |
try: | |
subject = "Vérification de votre compte" | |
# Utiliser directement localhost:8501 sans http:// | |
base_url = base_url = os.getenv('APP_URL', '').rstrip('/') | |
if "localhost" in base_url: | |
protocol = "http://" | |
else: | |
protocol = "https://" | |
# Construire l'URL avec le format correct pour Streamlit | |
if base_url.startswith(protocol): | |
verification_url = f"{base_url}/Verify_email?token={token}" | |
else: | |
verification_url = f"{protocol}{base_url}/Verify_email?token={token}" | |
# Note: "Verify_email" correspond au nom de la page dans Streamlit (le nom du fichier sans .py) | |
html_content = f""" | |
<html> | |
<body> | |
<h2>Vérification de votre compte</h2> | |
<p>Merci de vous être inscrit. Pour finaliser votre inscription, veuillez cliquer sur le lien ci-dessous :</p> | |
<p><a href="{verification_url}">Vérifier mon compte</a></p> | |
<p>Ce lien est valable pendant 7 jours.</p> | |
<p>Si vous n'avez pas créé de compte, veuillez ignorer cet email.</p> | |
<p>Si le lien ne fonctionne pas, copiez et collez cette URL dans votre navigateur :</p> | |
<p>{verification_url}</p> | |
</body> | |
</html> | |
""" | |
log_to_file(f"URL de vérification générée : {verification_url}", level=logging.INFO) | |
return send_email(email, subject, html_content) | |
except Exception as e: | |
log_to_file(f"Erreur lors de l'envoi de l'email de vérification : {str(e)}", level=logging.ERROR) | |
return False | |
def initialize_missing_notification_preferences(): | |
"""Initialise les préférences de notification pour les utilisateurs qui n'en ont pas""" | |
try: | |
# Récupérer tous les utilisateurs | |
users = supabase.table("users").select("id").execute() | |
# Récupérer les utilisateurs qui ont déjà des préférences | |
existing_prefs = supabase.table("notification_preferences").select("user_id").execute() | |
existing_user_ids = [pref['user_id'] for pref in existing_prefs.data] | |
# Pour chaque utilisateur sans préférences | |
for user in users.data: | |
if user['id'] not in existing_user_ids: | |
# Créer les préférences par défaut | |
default_notifications = { | |
"user_id": user['id'], | |
"email_notifications": True, | |
"security_alerts": True, | |
"assistant_updates": True, | |
"daily_summary": True, | |
"created_at": datetime.now(pytz.UTC).isoformat(), | |
"updated_at": datetime.now(pytz.UTC).isoformat() | |
} | |
# Insérer les préférences | |
supabase.table("notification_preferences").insert(default_notifications).execute() | |
log_to_file(f"Préférences de notification initialisées pour l'utilisateur {user['id']}", level=logging.INFO) | |
except Exception as e: | |
log_to_file(f"Erreur lors de l'initialisation des préférences : {str(e)}", level=logging.ERROR) | |
# Fin de la section: Authentification (login, sync_logout) | |
# =============================================================================== | |
# ================================================================================ | |
# Début de la section: Chat et Interface utilisateur | |
# fonction pour initialiser le choix de l'assistant | |
#décorateur @global_exception_handler pour la fonction plus haut def global_exeption_hundler (func) | |
def init_session_state(): | |
if 'authentication_status' not in st.session_state: | |
st.session_state['authentication_status'] = None | |
if 'user' not in st.session_state: | |
st.session_state['user'] = None | |
if 'chat_history' not in st.session_state: | |
st.session_state['chat_history'] = [] | |
# Assurez-vous que current_assistant a toujours une valeur par défaut | |
if 'current_assistant' not in st.session_state: | |
st.session_state['current_assistant'] = 'insuranceSANTE' # Valeur par défaut | |
if 'user_theme' not in st.session_state: | |
st.session_state['user_theme'] = load_theme_preference() | |
if 'sidebar_bg' not in st.session_state: | |
st.session_state.sidebar_bg = None | |
if 'sidebar_bg_color' not in st.session_state: | |
st.session_state.sidebar_bg_color = "#f0f2f6" # Couleur par défaut | |
#décorateur @global_exception_handler pour la fonction plus haut def global_exeption_hundler (func) | |
async def test_flowise_connection(): | |
"""Test asynchrone des connexions Flowise""" | |
client = AsyncFlowiseClient() | |
try: | |
results = await client.check_health() | |
for assistant_type, is_healthy in results.items(): | |
if is_healthy: | |
st.success(f"Connexion réussie à l'assistant {assistant_type}!") | |
else: | |
st.error(f"La connexion a échoué pour l'assistant {assistant_type}") | |
return all(results.values()) | |
except Exception as e: | |
st.error(f"Erreur lors du test des connexions : {str(e)}") | |
log_to_file(f"Erreur lors du test des connexions : {str(e)}", level=logging.ERROR) | |
return False | |
finally: | |
await client.close() | |
#décorateur @global_exception_handler pour la fonction plus haut def global_exeption_hundler (func) | |
async def query_flowise(question, assistant_type, max_retries=3, timeout=60): | |
"""Version asynchrone optimisée de query_flowise avec rate limiting""" | |
cancel_container = st.empty() | |
should_cancel = False | |
attempt_count = 0 | |
def create_cancel_button(): | |
nonlocal attempt_count | |
col1, col2 = cancel_container.columns([3, 1]) | |
with col2: | |
if st.button("Annuler", key=f"cancel_btn_{attempt_count}"): | |
nonlocal should_cancel | |
should_cancel = True | |
log_to_file("Requête annulée par l'utilisateur") | |
return True | |
attempt_count += 1 | |
return False | |
client = AsyncFlowiseClient() | |
status_container = None | |
try: | |
user_id = str(st.session_state.get('user', {}).get('id', 'anonymous')) | |
# Obtenir le compte des requêtes une seule fois au début | |
request_count = await client.rate_limiter.get_user_requests_count(user_id) | |
log_to_file(f"User ID: {user_id} - Requêtes effectuées: {request_count}", level=logging.INFO) | |
for attempt in range(max_retries): | |
if should_cancel: | |
if status_container: | |
status_container.empty() | |
cancel_container.empty() | |
return {"error": "Requête annulée par l'utilisateur"} | |
status_container = st.status(f"Tentative {attempt + 1}/{max_retries}...", expanded=True) | |
try: | |
create_cancel_button() | |
status_container.write(f"Envoi de la requête à l'assistant (timeout: {timeout}s)") | |
# Utiliser asyncio.wait_for pour gérer le timeout de manière plus efficace | |
result = await asyncio.wait_for( | |
client.query_assistant( | |
question=question, | |
assistant_type=assistant_type, | |
user_id=user_id | |
), | |
timeout=timeout | |
) | |
if should_cancel: | |
return {"error": "Requête annulée par l'utilisateur"} | |
if "error" in result: | |
if "Trop de requêtes" in result["error"]: | |
status_container.write("Limite de requêtes atteinte. Attente avant nouvelle tentative...") | |
# Utiliser une attente exponentielle | |
await asyncio.sleep(min(2 ** attempt, 10)) | |
continue | |
if attempt < max_retries - 1: | |
status_container.write("Nouvelle tentative...") | |
continue | |
return result | |
# Succès | |
cancel_container.empty() | |
status_container.update(label="Réponse reçue !", state="complete") | |
return result | |
except asyncio.TimeoutError: | |
log_to_file(f"Timeout lors de la tentative {attempt + 1}", level=logging.WARNING) | |
if attempt < max_retries - 1: | |
st.warning(f"Tentative {attempt + 1} a expiré, nouvelle tentative...") | |
# Utiliser une attente exponentielle | |
await asyncio.sleep(min(2 ** attempt, 10)) | |
continue | |
return {"error": "Timeout de toutes les requêtes"} | |
except Exception as e: | |
log_to_file(f"Erreur lors de la tentative {attempt + 1}: {str(e)}", level=logging.ERROR) | |
if attempt < max_retries - 1: | |
await asyncio.sleep(min(2 ** attempt, 10)) | |
continue | |
return {"error": str(e)} | |
finally: | |
if status_container and attempt < max_retries - 1: | |
status_container.empty() | |
finally: | |
await client.close() | |
if status_container: | |
status_container.empty() | |
cancel_container.empty() | |
def update_message_feedback(message_id, feedback): | |
"""Mettre à jour le feedback d'un message""" | |
try: | |
supabase.table("messages").update( | |
{"feedback": feedback} | |
).eq("message_id", message_id).execute() | |
return True | |
except Exception as e: | |
log_to_file(f"Erreur lors de la mise à jour du feedback : {str(e)}", level=logging.ERROR) | |
return False | |
# Fonction export_conversation pour retourner les données au lieu de créer un bouton de téléchargement | |
def export_conversation(): | |
if st.session_state['chat_history']: | |
conversation_data = { | |
"user": st.session_state['user'], | |
"timestamp": datetime.now().isoformat(), | |
"messages": st.session_state['chat_history'] | |
} | |
json_data = json.dumps(conversation_data, indent=2) | |
st.download_button( | |
label="Télécharger la conversation", | |
data=json_data, | |
file_name="conversation_export.json", | |
mime="application/json" | |
) | |
else: | |
st.warning("Aucune conversation à exporter.") | |
def check_assistant_access(user_id: int, assistant_type: str) -> bool: | |
""" | |
Vérifie si l'utilisateur a accès à un assistant spécifique. | |
""" | |
try: | |
# Si l'utilisateur est admin, il a accès à tout | |
user = supabase.table("users").select("role").eq("id", user_id).execute() | |
if user.data and user.data[0]['role'] == 'admin': | |
return True | |
# Sinon, vérifier les permissions spécifiques | |
response = supabase.table("user_assistant_permissions").select("is_authorized").eq( | |
"user_id", user_id | |
).eq("assistant_type", assistant_type).execute() | |
if response.data: | |
return response.data[0]['is_authorized'] | |
return False | |
except Exception as e: | |
log_to_file(f"Erreur lors de la vérification des accès : {str(e)}", level=logging.ERROR) | |
return False | |
#décorateur @global_exception_handler pour la fonction plus haut def global_exeption_hundler (func) | |
def improved_ui(): | |
""" | |
Interface utilisateur principale avec vérification des autorisations. | |
""" | |
init_session_state() | |
remove_streamlit_style() | |
hide_pages() | |
current_theme = st.session_state.user_theme | |
apply_theme(current_theme) | |
inject_custom_css(current_theme) | |
add_sidebar_logo(current_theme) | |
force_streamlit_style_update() | |
apply_chat_styles(current_theme) | |
inject_custom_toggle_css(current_theme) | |
save_theme_preference(current_theme) | |
check_and_send_daily_summary() | |
create_custom_footer() | |
asyncio.run(test_flowise_connection()) | |
if 'user_id' not in st.session_state: | |
st.error("Session utilisateur non valide") | |
return | |
if 'current_chat_session' not in st.session_state: | |
session_id = sync_load_active_chat_session(st.session_state['user_id']) | |
if not session_id: | |
st.error("Impossible de charger la session de chat") | |
return | |
if 'chat_history' not in st.session_state: | |
st.session_state.chat_history = sync_load_chat_history(st.session_state['current_chat_session']) | |
with st.sidebar: | |
st.markdown(f"### Bienvenue, {st.session_state['user']['prenom']} {st.session_state['user']['nom']}") | |
user_id = st.session_state['user']['id'] | |
# Section Admin - Placer ceci en premier, avant tout autre élément de la sidebar | |
if 'role' in st.session_state['user'] and st.session_state['user']['role'] == 'admin': | |
with st.container(): # Utiliser un container pour regrouper les éléments admin | |
st.write("### 🔧 Administration") | |
if st.button("Accéder au Dashboard Admin"): | |
st.session_state['show_admin'] = True | |
st.rerun() | |
st.write("---") # Séparateur | |
# Liste complète des assistants avec leurs détails | |
assistant_options = { | |
'insuranceSANTE': 'Colibri Vitalité ❣️', | |
'insuranceCAR': 'Colibri Carburant 🚘', | |
'insuranceBTP': 'Colibri Batisseur 🏠', | |
'Pilotage': 'Colibri Tatillon 👨🏻✈️', | |
'RH': 'Colibri Equipage 🪪', | |
} | |
# Filtrer les assistants autorisés | |
authorized_assistants = { | |
key: value for key, value in assistant_options.items() | |
if check_assistant_access(user_id, key) | |
} | |
if not authorized_assistants: | |
st.warning("Vous n'avez accès à aucun assistant pour le moment. Contactez un administrateur pour obtenir les autorisations nécessaires.") | |
return | |
# Sélection de l'assistant | |
selected_assistant = st.selectbox( | |
"Choisissez votre assistant", | |
options=list(authorized_assistants.keys()), | |
format_func=lambda x: authorized_assistants[x], | |
key='assistant_selector' | |
) | |
if selected_assistant != st.session_state.get('current_assistant'): | |
st.session_state.current_assistant = selected_assistant | |
st.session_state.chat_history = [] | |
st.rerun() | |
# Menu dépliant pour les actions utilisateur | |
with st.expander("Actions utilisateur", expanded=False): | |
theme_switcher() | |
# Bouton pour accéder au profil utilisateur | |
if st.button("Mon Profil 👤"): | |
st.switch_page("pages/profile.py") | |
if st.button("Vider le cache 🚮"): | |
cache_store.clear() | |
st.success("Cache vidé avec succès!") | |
if st.button("Effacer l'historique 📜"): | |
st.session_state.chat_history = [] | |
st.rerun() | |
if st.button("Exporter la conversation 📤"): | |
export_conversation() | |
if st.button("Déconnexion"): | |
sync_logout() | |
# Affichage des permissions actuelles | |
with st.expander("Vos accès", expanded=False): | |
st.write("#### Assistants autorisés") | |
for assistant_type, assistant_name in assistant_options.items(): | |
has_access = check_assistant_access(user_id, assistant_type) | |
icon = "✅" if has_access else "❌" | |
st.write(f"{icon} {assistant_name}") | |
# Charger l'historique des messages | |
if 'chat_history' not in st.session_state: | |
st.session_state.chat_history = load_chat_history(st.session_state['current_chat_session']) | |
# Zone de chat principale | |
chat_container = st.container() | |
with chat_container: | |
for message in st.session_state.chat_history: | |
with st.chat_message(message["role"]): | |
st.markdown(message["content"]) | |
if message["role"] == "assistant": | |
col1, spacer1, col2, spacer2, col4 = st.columns([1, 1, 1, 2, 16]) | |
with col1: | |
if st.button("👍", key=f"like_{message['message_id']}"): | |
if update_message_feedback(message['message_id'], 'positive'): | |
message['feedback'] = 'positive' | |
st.rerun() | |
with col2: | |
if st.button("👎", key=f"dislike_{message['message_id']}"): | |
if update_message_feedback(message['message_id'], 'negative'): | |
message['feedback'] = 'negative' | |
st.rerun() | |
with col4: | |
if message.get('feedback'): | |
st.write(f"Feedback: {'Positif' if message['feedback'] == 'positive' else 'Négatif'}") | |
# Zone de saisie | |
user_input = st.chat_input("Posez votre question ici...") | |
if user_input: | |
# Sauvegarder et afficher le message utilisateur | |
saved_message = sync_save_message( | |
st.session_state['current_chat_session'], | |
"user", | |
user_input, | |
st.session_state.current_assistant | |
) | |
if saved_message: | |
st.session_state.chat_history.append({ | |
"role": "user", | |
"content": user_input, | |
"message_id": saved_message["message_id"], | |
"assistant_type": st.session_state.current_assistant | |
}) | |
with st.spinner("L'assistant réfléchit..."): | |
response = asyncio.run(query_flowise( | |
question=user_input, | |
assistant_type=st.session_state.current_assistant, | |
max_retries=3, | |
timeout=60 | |
)) | |
if 'error' in response: | |
st.error(f"Erreur: {response['error']}") | |
else: | |
# Sauvegarder et afficher la réponse de l'assistant | |
saved_message = sync_save_message( | |
st.session_state['current_chat_session'], | |
"assistant", | |
response['answer'], | |
st.session_state.current_assistant | |
) | |
if saved_message: | |
st.session_state.chat_history.append({ | |
"role": "assistant", | |
"content": response['answer'], | |
"message_id": saved_message["message_id"], | |
"assistant_type": st.session_state.current_assistant | |
}) | |
st.rerun() | |
# Aide contextuelle | |
with st.expander("Aide"): | |
st.markdown(""" | |
### Comment utiliser cette application : | |
1. **Poser une question** : Tapez votre question dans la zone de texte en bas de l'écran et appuyez sur Entrée. | |
2. **Historique du chat** : Vos conversations précédentes apparaîtront au-dessus de la zone de texte. | |
3. **Feedback** : Vous pouvez donner un feedback positif (👍) ou négatif (👎) aux réponses de l'assistant. | |
4. **Actions utilisateur** : Utilisez le menu dépliant dans la barre latérale pour diverses actions comme la déconnexion ou l'exportation de la conversation. | |
5. **Personnalisation** : Vous pouvez changer le thème de l'application dans la barre latérale. | |
Si vous avez d'autres questions, n'hésitez pas à demander à l'assistant ! | |
""") | |
# Fin de la section: Chat et Interface utilisateur | |
# ================================================================================ | |
# =============================================================================== | |
# Début de la section: Gestion des sessions | |
#générer des tokens uniques pour les sessions de chat dans Supabase | |
def generate_chat_session_token(): | |
"""Générer un nouveau token UUID pour la session de chat""" | |
return str(uuid.uuid4()) | |
#Gère le rafraîchissement des tokens JWT dans Supabase, utilisée pour maintenir la session active | |
def refresh_token(): | |
if 'user_id' not in st.session_state: | |
return | |
user_id = st.session_state['user_id'] | |
try: | |
new_expires_at = datetime.now(pytz.UTC) + timedelta(days=1) | |
new_token = jwt.encode({ | |
"id": user_id, | |
"exp": new_expires_at.timestamp() | |
}, JWT_SECRET, algorithm="HS256") | |
supabase.table("user_sessions").update({ | |
"token": new_token, | |
"expires_at": new_expires_at.isoformat() | |
}).eq("user_id", user_id).execute() | |
st.session_state['user'] = jwt.decode(new_token, JWT_SECRET, algorithms=["HS256"]) | |
log_to_file("Token rafraîchi avec succès", level=logging.INFO) | |
except Exception as e: | |
log_to_file(f"Erreur lors du rafraîchissement du token : {str(e)}", level=logging.ERROR) | |
async def create_chat_session(user_id: int) -> Optional[str]: | |
"""Crée une nouvelle session de chat""" | |
try: | |
# Désactiver les sessions existantes | |
supabase.table("chat_sessions").update({ | |
"is_active": False | |
}).eq("user_id", user_id).eq("is_active", True).execute() | |
# Créer une nouvelle session | |
new_token = str(uuid.uuid4()) | |
current_time = datetime.now(pytz.UTC).isoformat() | |
session_data = { | |
"user_id": user_id, | |
"is_active": True, | |
"session_token": new_token, | |
"last_accessed": current_time | |
} | |
response = supabase.table("chat_sessions").insert(session_data).execute() | |
if response.data: | |
session_id = response.data[0]['session_id'] | |
session_data['session_id'] = session_id | |
# Mettre à jour le cache | |
await auth_cache.update_session_and_history(session_data) | |
# Mettre à jour la session state | |
st.session_state['current_chat_session'] = session_id | |
st.session_state['chat_history'] = [] | |
log_to_file(f"Nouvelle session créée : {session_id}", level=logging.INFO) | |
return session_id | |
return None | |
except Exception as e: | |
log_to_file(f"Erreur lors de la création de la session : {str(e)}", level=logging.ERROR) | |
return None | |
async def get_active_chat_session(user_id: int) -> Optional[str]: | |
"""Récupère la session de chat active depuis le cache""" | |
try: | |
session = await auth_cache.get_active_session(user_id) | |
if session: | |
st.session_state['current_chat_session'] = session['session_id'] | |
return session['session_id'] | |
return await create_chat_session(user_id) | |
except Exception as e: | |
log_to_file(f"Erreur lors de la récupération de la session de chat : {str(e)}", level=logging.ERROR) | |
return None | |
async def save_message(session_id: str, role: str, content: str, assistant_type: Optional[str] = None) -> Optional[Dict]: | |
"""Sauvegarde un message et met à jour le cache""" | |
try: | |
data = { | |
"session_id": session_id, | |
"role": role, | |
"content": content, | |
"assistant_type": assistant_type, | |
"created_at": datetime.now(pytz.UTC).isoformat() | |
} | |
response = supabase.table("messages").insert(data).execute() | |
if response.data: | |
message_data = response.data[0] | |
# Récupérer la session pour la mise à jour du cache | |
session = await auth_cache.get_active_session(st.session_state['user_id']) | |
if session: | |
await auth_cache.update_session_and_history(session, message_data) | |
return message_data | |
return None | |
except Exception as e: | |
log_to_file(f"Erreur lors de la sauvegarde du message : {str(e)}", level=logging.ERROR) | |
return None | |
def sync_save_message(session_id: str, role: str, content: str, assistant_type: Optional[str] = None) -> Optional[Dict]: | |
"""Version synchrone de save_message""" | |
return asyncio.run(save_message(session_id, role, content, assistant_type)) | |
async def load_chat_history(session_id: str) -> List[Dict]: | |
"""Charge l'historique des messages depuis le cache Redis""" | |
try: | |
history = await auth_cache.get_chat_history(session_id) | |
log_to_file(f"Historique chargé depuis le cache pour la session {session_id}", level=logging.INFO) | |
return history | |
except Exception as e: | |
log_to_file(f"Erreur lors du chargement de l'historique : {str(e)}", level=logging.ERROR) | |
return [] | |
def sync_load_chat_history(session_id: str) -> List[Dict]: | |
"""Version synchrone de load_chat_history""" | |
return asyncio.run(load_chat_history(session_id)) | |
def verify_session_integrity(): | |
"""Vérifier l'intégrité des données de session""" | |
required_keys = ['current_chat_session', 'chat_session_token', 'chat_history'] | |
missing_keys = [key for key in required_keys if key not in st.session_state] | |
if missing_keys: | |
log_to_file(f"Données de session manquantes : {missing_keys}", level=logging.WARNING) | |
if 'user_id' in st.session_state: | |
load_active_chat_session(st.session_state['user_id']) | |
return False | |
return True | |
async def load_active_chat_session(user_id: int) -> Optional[str]: | |
"""Charge la session de chat active depuis le cache""" | |
try: | |
session = await auth_cache.get_active_session(user_id) | |
if session: | |
session_id = session['session_id'] | |
st.session_state['current_chat_session'] = session_id | |
# Charger l'historique | |
messages = await load_chat_history(session_id) | |
st.session_state['chat_history'] = messages | |
log_to_file(f"Session active chargée : {session_id}", level=logging.INFO) | |
return session_id | |
return await create_chat_session(user_id) | |
except Exception as e: | |
log_to_file(f"Erreur lors du chargement de la session : {str(e)}", level=logging.ERROR) | |
return None | |
def sync_load_active_chat_session(user_id: int) -> Optional[str]: | |
"""Version synchrone de load_active_chat_session""" | |
return asyncio.run(load_active_chat_session(user_id)) | |
# Fin de la section: Gestion des sessions | |
# =============================================================================== | |
# =============================================================================== | |
# Début de la section principale (main) | |
#décorateur @global_exception_handler pour la fonction plus haut def global_exeption_hundler (func) | |
def main(): | |
asyncio.run(initialize_app()) | |
init_session_state() | |
initialize_missing_notification_preferences() #initialiser les préférences de notifications manquantes | |
hide_pages() | |
if 'authentication_status' not in st.session_state: | |
st.session_state['authentication_status'] = None | |
if st.session_state['authentication_status'] is not True: | |
authentication_page() | |
else: | |
is_session_valid = check_authentication() | |
if is_session_valid: | |
apply_theme(st.session_state['user_theme']) | |
# Vérifier si nous devons afficher le dashboard admin | |
if st.session_state.get('show_admin', False) and st.session_state['user']['role'] == 'admin': | |
asyncio.run(render_admin_dashboard()) | |
else: | |
improved_ui() | |
else: | |
st.warning("Votre session a expiré. Veuillez vous reconnecter.") | |
authentication_page() | |
# Point d'entrée de l'application | |
if __name__ == "__main__": | |
main() | |
# Fin de la section principale (main) | |
# =============================================================================== | |