Gouzi Mohaled commited on
Commit
8446485
·
1 Parent(s): 4366a2e

Add application file

Browse files
Files changed (3) hide show
  1. Dockerfile +23 -0
  2. app.py +1570 -0
  3. requirements.txt +16 -0
Dockerfile ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Utiliser une image Python officielle comme base
2
+ FROM python:3.10-slim
3
+
4
+ # Définir le répertoire de travail dans le conteneur
5
+ WORKDIR /app
6
+
7
+ # Copier le fichier des dépendances dans le conteneur
8
+ COPY requirements.txt .
9
+
10
+ # Mettre à jour pip
11
+ RUN pip install --upgrade pip
12
+
13
+ # Installer les dépendances
14
+ RUN pip install --no-cache-dir -r requirements.txt
15
+
16
+ # Copier le reste de l'application dans le conteneur
17
+ COPY . .
18
+
19
+ # Exposer le port Streamlit (par défaut 8501)
20
+ EXPOSE 7860
21
+
22
+ # Définir la commande pour exécuter l'application Streamlit
23
+ CMD ["streamlit", "run", "app.py", "--server.port=7860", "--server.address=0.0.0.0"]
app.py ADDED
@@ -0,0 +1,1570 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import pytz
3
+ import json
4
+ import uuid
5
+ import base64
6
+ import streamlit as st
7
+ from supabase import create_client, Client
8
+ import requests
9
+ import bcrypt
10
+ import jwt
11
+ from dotenv import load_dotenv
12
+ import functools
13
+ from datetime import datetime, timedelta
14
+ from datetime import datetime
15
+ from io import BytesIO
16
+ from PIL import Image
17
+ import time
18
+ import traceback
19
+ from functools import lru_cache
20
+ import extra_streamlit_components as stx
21
+ from contextlib import contextmanager
22
+ from typing import Dict, List, Optional, Tuple, Any
23
+ from typing import TypedDict, Union
24
+ from utils.password_reset import initiate_password_reset
25
+ from utils.notification_handler import notify_security_alert #import du module de notifications (fonction alerte d'une nouvelle connexio)
26
+ 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)
27
+ from utils.email_utils import send_email
28
+ import secrets # Concerne l'email de vérification lors de l'inscription d'un nouvels utilisateur
29
+ 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)
30
+ from utils.logging_utils import log_to_file # système de logging défini sur un fichier séparé pour une meilleure lisibilité.
31
+ from utils.theme_utils import (
32
+ save_theme_preference,
33
+ load_theme_preference,
34
+ apply_theme,
35
+ inject_custom_css,
36
+ add_sidebar_logo,
37
+ apply_chat_styles,
38
+ inject_custom_toggle_css,
39
+ create_custom_footer,
40
+ theme_switcher,
41
+ remove_streamlit_style,
42
+ force_streamlit_style_update,
43
+ hide_pages
44
+ )
45
+ import asyncio
46
+ from utils.async_flowise import AsyncFlowiseClient
47
+ from utils.cache_manager import CacheManager
48
+ from admin_page import render_admin_dashboard
49
+ import tracemalloc
50
+ tracemalloc.start()
51
+ from utils.auth_cache import AuthenticationCache # Importation de la classe d'authentification
52
+ auth_cache = AuthenticationCache() # Créez une instance globale de AuthenticationCache pour utiliser ses focntions d'authentification et de cache au sein de l'application
53
+ from utils.ip_utils import get_user_ip
54
+
55
+
56
+
57
+
58
+
59
+
60
+
61
+
62
+
63
+ st.set_page_config(
64
+ page_title="Colibri - Votre assistant intelligent", # ou un titre approprié
65
+ page_icon=":bar_chart:",
66
+ layout="wide",
67
+ )
68
+ # Initialisez le cache manager globalement
69
+ cache_manager = CacheManager()
70
+ # Charger les variables d'environnement
71
+ load_dotenv()
72
+
73
+ SUPABASE_URL = os.getenv("SUPABASE_URL")
74
+ SUPABASE_KEY = os.getenv("SUPABASE_KEY")
75
+ JWT_SECRET = os.getenv("JWT_SECRET")
76
+ FLOWISE_API_URL_SANTE = os.getenv("FLOWISE_API_URL_SANTE")
77
+ FLOWISE_API_URL_CAR = os.getenv("FLOWISE_API_URL_CAR")
78
+ FLOWISE_API_URL_BTP = os.getenv("FLOWISE_API_URL_BTP")
79
+ FLOWISE_API_URL_RH = os.getenv("FLOWISE_API_URL_RH")
80
+ FLOWISE_API_URL_PILOTAGE = os.getenv("FLOWISE_API_URL_PILOTAGE")
81
+
82
+ # Configuration du logging
83
+
84
+ supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
85
+
86
+ # ===============================================================================
87
+ # 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
88
+
89
+ def start_activity_tracking(user_id: int):
90
+ """Démarre une nouvelle session de tracking d'activité"""
91
+ try:
92
+ # Vérifier s'il existe déjà une session active
93
+ active_tracking = supabase.table("user_activity_tracking").select("*").eq(
94
+ "user_id", user_id
95
+ ).is_("session_end", None).execute()
96
+
97
+ if not active_tracking.data:
98
+ # Créer une nouvelle session de tracking
99
+ supabase.table("user_activity_tracking").insert({
100
+ "user_id": user_id,
101
+ "session_start": datetime.now(pytz.UTC).isoformat(),
102
+ "date_tracked": datetime.now(pytz.UTC).date().isoformat()
103
+ }).execute()
104
+
105
+ except Exception as e:
106
+ log_to_file(f"Erreur lors du démarrage du tracking : {str(e)}", level=logging.ERROR)
107
+
108
+ def end_activity_tracking(user_id: int):
109
+ """Termine la session de tracking active"""
110
+ try:
111
+ # Récupérer la session active
112
+ active_tracking = supabase.table("user_activity_tracking").select("*").eq(
113
+ "user_id", user_id
114
+ ).is_("session_end", None).execute()
115
+
116
+ if active_tracking.data:
117
+ tracking = active_tracking.data[0]
118
+ session_start = datetime.fromisoformat(tracking['session_start'].replace('Z', '+00:00'))
119
+ session_end = datetime.now(pytz.UTC)
120
+ duration = int((session_end - session_start).total_seconds() / 60)
121
+
122
+ # Mettre à jour la session
123
+ supabase.table("user_activity_tracking").update({
124
+ "session_end": session_end.isoformat(),
125
+ "duration_minutes": duration
126
+ }).eq("id", tracking['id']).execute()
127
+
128
+ except Exception as e:
129
+ log_to_file(f"Erreur lors de la fin du tracking : {str(e)}", level=logging.ERROR)
130
+
131
+ def calculate_usage_time(user_id: int) -> int:
132
+ """Calcule le temps total d'utilisation en minutes pour la journée"""
133
+ try:
134
+ today = datetime.now(pytz.UTC).date()
135
+
136
+ # Récupérer toutes les sessions terminées de la journée
137
+ completed_sessions = supabase.table("user_activity_tracking").select(
138
+ "duration_minutes"
139
+ ).eq("user_id", user_id).eq(
140
+ "date_tracked", today.isoformat()
141
+ ).not_.is_("session_end", None).execute()
142
+
143
+ # Calculer le temps des sessions terminées
144
+ total_minutes = sum(
145
+ session.get('duration_minutes', 0)
146
+ for session in completed_sessions.data
147
+ if session.get('duration_minutes') is not None
148
+ )
149
+
150
+ # Ajouter le temps de la session active si elle existe
151
+ active_session = supabase.table("user_activity_tracking").select("*").eq(
152
+ "user_id", user_id
153
+ ).is_("session_end", None).execute()
154
+
155
+ if active_session.data:
156
+ session_start = datetime.fromisoformat(
157
+ active_session.data[0]['session_start'].replace('Z', '+00:00')
158
+ )
159
+ current_time = datetime.now(pytz.UTC)
160
+ active_duration = int((current_time - session_start).total_seconds() / 60)
161
+ total_minutes += active_duration
162
+
163
+ return total_minutes
164
+
165
+ except Exception as e:
166
+ log_to_file(f"Erreur lors du calcul du temps d'utilisation : {str(e)}", level=logging.ERROR)
167
+ return 0
168
+
169
+ def generate_user_summary(user_id: int):
170
+ """Génère et envoie un résumé quotidien à l'utilisateur"""
171
+ try:
172
+ today = datetime.now(pytz.UTC).date()
173
+
174
+ # Compter les conversations et messages
175
+ conversations = get_conversation_count(user_id)
176
+ messages = get_messages_count(user_id)
177
+ usage_time = calculate_usage_time(user_id) # Utilise la nouvelle méthode
178
+
179
+ summary_data = {
180
+ "conversations": conversations,
181
+ "questions": messages,
182
+ "usage_time": usage_time
183
+ }
184
+
185
+ # Envoyer le résumé
186
+ send_daily_summary(user_id, summary_data)
187
+
188
+ except Exception as e:
189
+ log_to_file(f"Erreur lors de la génération du résumé : {str(e)}", level=logging.ERROR)
190
+
191
+ def check_and_send_daily_summary():
192
+ """Vérifie et envoie le résumé quotidien si nécessaire"""
193
+ try:
194
+ if 'user' in st.session_state and 'last_summary_sent' not in st.session_state:
195
+ user_id = st.session_state['user']['id']
196
+
197
+ # Récupérer les préférences de notification
198
+ prefs = supabase.table("notification_preferences").select("*").eq("user_id", user_id).execute()
199
+
200
+ if prefs.data and prefs.data[0].get('daily_summary'):
201
+ # Générer et envoyer le résumé
202
+ summary_data = {
203
+ "conversations": get_conversation_count(user_id),
204
+ "questions": get_messages_count(user_id),
205
+ "usage_time": calculate_usage_time(user_id)
206
+ }
207
+
208
+ if send_daily_summary(user_id, summary_data):
209
+ # Marquer comme envoyé pour aujourd'hui
210
+ st.session_state['last_summary_sent'] = datetime.now(pytz.UTC).date()
211
+ log_to_file(f"Résumé quotidien envoyé avec succès pour l'utilisateur {user_id}", level=logging.INFO)
212
+ else:
213
+ log_to_file(f"Échec de l'envoi du résumé quotidien pour l'utilisateur {user_id}", level=logging.ERROR)
214
+
215
+ except Exception as e:
216
+ log_to_file(f"Erreur lors de la vérification du résumé quotidien : {str(e)}", level=logging.ERROR)
217
+
218
+ def get_conversation_count(user_id: int) -> int:
219
+ """Compte le nombre de conversations de la journée"""
220
+ try:
221
+ today = datetime.now(pytz.UTC).date()
222
+ result = supabase.table("chat_sessions").select(
223
+ "count", count="exact"
224
+ ).eq("user_id", user_id).gte(
225
+ "created_at", today.isoformat()
226
+ ).execute()
227
+ return result.count if hasattr(result, 'count') else 0
228
+ except Exception as e:
229
+ log_to_file(f"Erreur lors du comptage des conversations : {str(e)}", level=logging.ERROR)
230
+ return 0
231
+
232
+ def get_messages_count(user_id: int) -> int:
233
+ """Compte le nombre de messages de la journée"""
234
+ try:
235
+ today = datetime.now(pytz.UTC).date()
236
+
237
+ # Récupérer les sessions actives de l'utilisateur aujourd'hui
238
+ sessions = supabase.table("chat_sessions").select("session_id").eq(
239
+ "user_id", user_id
240
+ ).gte("created_at", today.isoformat()).execute()
241
+
242
+ sessions = supabase.table("chat_sessions").select("session_id").eq(
243
+ "user_id", user_id
244
+ ).gte("created_at", today.isoformat()).execute()
245
+
246
+ if not sessions.data:
247
+ return 0
248
+
249
+ # Récupérer les IDs des sessions
250
+ session_ids = [session['session_id'] for session in sessions.data]
251
+
252
+ # Compter les messages dans ces sessions
253
+ result = supabase.table("messages").select(
254
+ "count", count="exact"
255
+ ).in_("session_id", session_ids).eq(
256
+ "role", "user" # Pour ne compter que les messages de l'utilisateur
257
+ ).execute()
258
+
259
+ return result.count if hasattr(result, 'count') else 0
260
+ except Exception as e:
261
+ log_to_file(f"Erreur lors du comptage des messages : {str(e)}", level=logging.ERROR)
262
+ return 0
263
+
264
+
265
+ # 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
266
+ # ===============================================================================
267
+
268
+
269
+
270
+
271
+ # ===============================================================================
272
+ # Début de la section: Gestion des du cach et des erreurs
273
+
274
+
275
+ #implémenter une gestion des erreurs et des exceptions plus robuste
276
+ def global_exception_handler(func):
277
+ def wrapper(*args, **kwargs):
278
+ try:
279
+ return func(*args, **kwargs)
280
+ except Exception as e:
281
+ error_message = f"Une erreur s'est produite : {str(e)}"
282
+ st.error(error_message)
283
+ log_to_file(f"Erreur non gérée : {error_message}\n{traceback.format_exc()}", level=logging.ERROR)
284
+ # Vous pouvez ajouter ici une logique pour notifier les administrateurs
285
+ return wrapper
286
+
287
+
288
+ @global_exception_handler #décorateur @global_exception_handler pour la fonction plus haut def global_exeption_hundler (func)
289
+ def handle_error(error_message):
290
+ st.error(f"Une erreur s'est produite : {error_message}")
291
+ log_to_file(f"Erreur : {error_message}", level=logging.ERROR)
292
+
293
+ # Structure pour stocker le cache
294
+ cache_store = {}
295
+ @global_exception_handler
296
+ def timed_cache(expires_after=timedelta(minutes=30)):
297
+ def decorator(func):
298
+ @functools.wraps(func)
299
+ def wrapper(*args, **kwargs):
300
+ # Créer une clé unique pour cette requête
301
+ cache_key = f"{func.__name__}:{str(args)}:{str(kwargs)}"
302
+
303
+ # Vérifier si nous avons une réponse en cache et si elle est encore valide
304
+ if cache_key in cache_store:
305
+ result, timestamp = cache_store[cache_key]
306
+ if datetime.now() - timestamp < expires_after:
307
+ st.info("Réponse récupérée du cache")
308
+ return result
309
+
310
+ # Si pas de cache ou cache expiré, exécuter la fonction
311
+ result = func(*args, **kwargs)
312
+
313
+ # Stocker le résultat dans le cache
314
+ cache_store[cache_key] = (result, datetime.now())
315
+
316
+ return result
317
+ return wrapper
318
+ return decorator
319
+
320
+
321
+ @lru_cache(maxsize=100)
322
+ def expensive_operation(param):
323
+ """
324
+ Fonction simulant une opération coûteuse.
325
+ """
326
+ # Simuler une opération coûteuse
327
+ time.sleep(2)
328
+ return param * 2
329
+
330
+
331
+ # Fin de la section: Gestion des du cach et des erreurs
332
+ # ===============================================================================
333
+
334
+
335
+
336
+
337
+
338
+
339
+
340
+
341
+ # ================================================================================
342
+ # Début de la section: Authentification (login, logout)
343
+
344
+
345
+ # Regroupe les fonctions par rôle spécifique dans le processus d'authentification.
346
+
347
+ # Suit un ordre logique :
348
+ # - Interface utilisateur
349
+ # - Gestion des données d'authentification
350
+ # - Vérification des informations d'authentification
351
+ # - Initialisation de la session utilisateur
352
+
353
+ # Facilite la compréhension du flux d'authentification.
354
+
355
+ @timed_cache()
356
+ async def initialize_app():
357
+ """Initialise l'application et gère le cache Redis"""
358
+ try:
359
+ log_to_file("Démarrage de l'application - Vérification du cache", level=logging.INFO)
360
+
361
+ if not await auth_cache.is_cache_initialized():
362
+ # Premier chargement du cache
363
+ log_to_file("Cache vide - Chargement initial", level=logging.INFO)
364
+ success = await auth_cache.load_all_users_to_cache()
365
+ if success:
366
+ await auth_cache.update_cache_version()
367
+ # Charger aussi les sessions
368
+ await auth_cache.load_user_sessions_to_cache()
369
+ log_to_file("Chargement initial du cache réussi", level=logging.INFO)
370
+ else:
371
+ log_to_file("Échec du chargement initial du cache", level=logging.ERROR)
372
+ elif await auth_cache.is_cache_outdated():
373
+ # Mise à jour du cache existant
374
+ log_to_file("Cache existant - Synchronisation des modifications", level=logging.INFO)
375
+ await auth_cache.sync_cache_with_db()
376
+ await auth_cache.load_user_sessions_to_cache() # Recharger les sessions
377
+ else:
378
+ log_to_file("Cache à jour - Aucune action nécessaire", level=logging.INFO)
379
+
380
+ # Debug optionnel
381
+ await auth_cache.debug_redis_content()
382
+
383
+ except Exception as e:
384
+ log_to_file(f"Erreur lors de l'initialisation : {str(e)}", level=logging.ERROR)
385
+
386
+
387
+
388
+
389
+ @global_exception_handler
390
+ def authentication_page():
391
+ hide_pages() # cacher les pages non désirée de la sidebar dans la page d'authentification
392
+ st.title("Bienvenue sur votre application RAG")
393
+ menu = ["Se connecter", "S'inscrire"]
394
+ choice = st.sidebar.selectbox("Menu", menu)
395
+
396
+ if choice == "Se connecter":
397
+ with st.form("login_form"):
398
+ st.subheader("Connexion")
399
+ email = st.text_input("Email")
400
+ password = st.text_input("Mot de passe", type='password')
401
+
402
+ # Créer deux colonnes pour les boutons
403
+ col1, col2 = st.columns([1, 1])
404
+
405
+ with col1:
406
+ if st.form_submit_button("Connexion"):
407
+ success, result = login_user(email, password)
408
+ if success:
409
+ st.success(result)
410
+ st.rerun()
411
+ else:
412
+ st.error(result)
413
+
414
+ with col2:
415
+ if st.form_submit_button("Mot de passe oublié ?"):
416
+ st.session_state['show_reset'] = True
417
+
418
+ # Formulaire de réinitialisation du mot de passe
419
+ if st.session_state.get('show_reset', False):
420
+ with st.form("reset_form", clear_on_submit=True):
421
+ st.subheader("Réinitialisation du mot de passe")
422
+ reset_email = st.text_input("Entrez votre email")
423
+ if st.form_submit_button("Envoyer les instructions"):
424
+ if reset_email:
425
+ with st.spinner("Envoi en cours..."):
426
+ success, msg = initiate_password_reset(reset_email)
427
+ if success:
428
+ st.success(msg)
429
+ st.session_state['show_reset'] = False
430
+ else:
431
+ st.error(msg)
432
+ else:
433
+ st.error("Veuillez entrer votre email")
434
+
435
+ elif choice == "S'inscrire":
436
+ with st.form("signup_form"):
437
+ st.subheader("Inscription")
438
+ col1, col2 = st.columns(2)
439
+
440
+ with col1:
441
+ email = st.text_input("Email")
442
+ password = st.text_input("Mot de passe", type='password')
443
+ password_confirm = st.text_input("Confirmez le mot de passe", type='password')
444
+
445
+ with col2:
446
+ nom = st.text_input("Nom")
447
+ prenom = st.text_input("Prénom")
448
+ profession = st.text_input("Profession")
449
+ entreprise = st.text_input("Entreprise")
450
+
451
+ # Option pour créer un admin (visible uniquement pour les admins connectés)
452
+ create_as_admin = False
453
+ # Vérification sécurisée du rôle administrateur
454
+ is_admin = (
455
+ 'user' in st.session_state
456
+ and isinstance(st.session_state.get('user'), dict)
457
+ and st.session_state['user'].get('role') == 'admin'
458
+ )
459
+
460
+ if is_admin:
461
+ create_as_admin = st.checkbox("Créer en tant qu'administrateur")
462
+
463
+ submit_button = st.form_submit_button("Inscription")
464
+
465
+ if submit_button:
466
+ if password != password_confirm:
467
+ st.error("Les mots de passe ne correspondent pas.")
468
+ else:
469
+ professional_info = {
470
+ "profession": profession,
471
+ "entreprise": entreprise
472
+ }
473
+ # Définir le rôle en fonction du choix
474
+ role = 'admin' if create_as_admin and is_admin else 'user'
475
+ success, result = register_user(
476
+ email=email,
477
+ password=password,
478
+ nom=nom,
479
+ prenom=prenom,
480
+ professional_info=professional_info,
481
+ role=role
482
+ )
483
+ if success:
484
+ st.success(result)
485
+ else:
486
+ st.error(result)
487
+
488
+ def check_access_validity(ip_address: str, email: str) -> Tuple[bool, str]:
489
+ """
490
+ Vérifie à la fois les restrictions d'IP et de domaine email.
491
+
492
+ Args:
493
+ ip_address (str): L'adresse IP à vérifier
494
+ email (str): L'adresse email à vérifier
495
+
496
+ Returns:
497
+ Tuple[bool, str]: Un tuple contenant:
498
+ - bool: True si l'accès est autorisé, False sinon
499
+ - str: Message d'erreur si l'accès est refusé, chaîne vide si autorisé
500
+ """
501
+ try:
502
+ # Récupérer toutes les restrictions actives en une seule requête
503
+ restrictions = supabase.table("access_restrictions").select("*").eq("is_active", True).execute()
504
+
505
+ # Si pas de restrictions actives, autoriser l'accès
506
+ if not restrictions.data:
507
+ return True, ""
508
+
509
+ restrictions_data = restrictions.data[0]
510
+
511
+ # Vérifier les restrictions d'IP
512
+ allowed_ip_ranges = restrictions_data.get('allowed_ip_ranges', [])
513
+ if allowed_ip_ranges:
514
+ # Extraire l'adresse IP du dictionnaire
515
+ user_ip = ip_address.get('ip') if isinstance(ip_address, dict) else ip_address
516
+ if not any(user_ip == allowed_ip for allowed_ip in allowed_ip_ranges):
517
+ return False, "Votre adresse IP n'est pas autorisée à accéder à cette application."
518
+
519
+ # Vérifier les restrictions de domaine email
520
+ allowed_domains = restrictions_data.get('allowed_email_domains', [])
521
+ if allowed_domains:
522
+ email_domain = email.split('@')[1].lower()
523
+ if not any(domain.lower() == email_domain for domain in allowed_domains):
524
+ return False, "Votre domaine d'adresse email n'est pas autorisé à accéder à cette application."
525
+
526
+ # Si toutes les vérifications sont passées
527
+ return True, ""
528
+
529
+ except Exception as e:
530
+ error_msg = f"Erreur lors de la vérification des restrictions : {str(e)}"
531
+ log_to_file(error_msg, level=logging.ERROR)
532
+ return False, error_msg
533
+
534
+
535
+ @global_exception_handler #décorateur @global_exception_handler pour la fonction plus haut def global_exeption_hundler (func)
536
+ 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]:
537
+ """
538
+ Enregistre un nouvel utilisateur avec vérification d'email et d'IP.
539
+ Si c'est le premier utilisateur, il sera automatiquement défini comme administrateur.
540
+ Si c'est un utilisateur existant, il sera automatiquement défini comme utilisateur normal.
541
+ """
542
+ try:
543
+ ip_address = get_user_ip()
544
+ log_to_file(f"(def register_user)-Adresse IP de l'utilisateur : {ip_address}", level=logging.INFO) # Afficher l'adresse IP pour le debug
545
+
546
+ # Vérifier les restrictions d'IP et d'email en une seule fois
547
+ is_valid, error_message = check_access_validity(ip_address, email)
548
+ if not is_valid:
549
+ return False, error_message
550
+
551
+
552
+ # Vérifier si c'est le premier utilisateur (sera admin par défaut)
553
+ users_count = supabase.table("users").select("count", count="exact").execute()
554
+ is_first_user = users_count.count == 0 if hasattr(users_count, 'count') else True
555
+
556
+ # Vérifier si l'email existe déjà
557
+ existing_user = supabase.table("users").select("email").eq("email", email).execute()
558
+ if existing_user.data:
559
+ return False, "Cet email est déjà utilisé."
560
+
561
+ # Hasher le mot de passe
562
+ hashed_password = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
563
+
564
+ # Déterminer le rôle
565
+ if is_first_user:
566
+ assigned_role = 'admin' # Premier utilisateur est toujours admin
567
+ else:
568
+ assigned_role = role if role in ['admin', 'user'] else 'user'
569
+
570
+ # Préparer les données utilisateur
571
+ user_data = {
572
+ "email": email,
573
+ "password": hashed_password,
574
+ "nom": nom,
575
+ "prenom": prenom,
576
+ "role": assigned_role,
577
+ "is_active": False, # L'utilisateur sera activé après vérification
578
+ "created_at": datetime.now(pytz.UTC).isoformat(),
579
+ "professional_info": professional_info or {}
580
+ }
581
+
582
+ # Insérer l'utilisateur
583
+ data = supabase.table("users").insert(user_data).execute()
584
+
585
+ if data.data:
586
+ user_id = data.data[0]['id']
587
+
588
+ # Initialiser les permissions par défaut pour les assistants
589
+ default_permissions = [
590
+ {"user_id": user_id, "assistant_type": "insuranceSANTE", "is_authorized": False},
591
+ {"user_id": user_id, "assistant_type": "insuranceCAR", "is_authorized": False},
592
+ {"user_id": user_id, "assistant_type": "insuranceBTP", "is_authorized": False},
593
+ {"user_id": user_id, "assistant_type": "Pilotage", "is_authorized": False},
594
+ {"user_id": user_id, "assistant_type": "RH", "is_authorized": False}
595
+
596
+
597
+ ]
598
+
599
+ # Si c'est le premier utilisateur (admin), autoriser tous les assistants
600
+ if is_first_user:
601
+ for perm in default_permissions:
602
+ perm["is_authorized"] = True
603
+
604
+ # Insérer les permissions
605
+ supabase.table("user_assistant_permissions").insert(default_permissions).execute()
606
+
607
+ # Initialiser les préférences de notification
608
+ default_notifications = {
609
+ "user_id": user_id,
610
+ "email_notifications": True,
611
+ "security_alerts": True,
612
+ "assistant_updates": True,
613
+ "daily_summary": True,
614
+ "created_at": datetime.now(pytz.UTC).isoformat(),
615
+ "updated_at": datetime.now(pytz.UTC).isoformat()
616
+ }
617
+
618
+ # Insérer les préférences de notification
619
+ supabase.table("notification_preferences").insert(default_notifications).execute()
620
+
621
+ # Créer un token de vérification
622
+ verification_token = secrets.token_urlsafe(32)
623
+ expires_at = datetime.now(pytz.UTC) + timedelta(days=7)
624
+
625
+ # Enregistrer le token de vérification
626
+ supabase.table("email_verification").insert({
627
+ "user_id": user_id,
628
+ "email": email,
629
+ "verification_token": verification_token,
630
+ "expires_at": expires_at.isoformat()
631
+ }).execute()
632
+
633
+ # Envoyer l'email de vérification
634
+ send_verification_email(email, verification_token)
635
+
636
+ log_to_file(f"Nouvel utilisateur enregistré: {email} avec le rôle: {assigned_role}", level=logging.INFO)
637
+ if is_first_user:
638
+ 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."
639
+ return True, "Inscription réussie ! Veuillez vérifier votre email pour activer votre compte."
640
+
641
+ return False, "Erreur lors de l'enregistrement."
642
+
643
+ except Exception as e:
644
+ log_to_file(f"Erreur lors de l'inscription : {str(e)}", level=logging.ERROR)
645
+ return False, f"Erreur lors de l'inscription : {str(e)}"
646
+
647
+
648
+ async def login_user_async(email: str, password: str) -> tuple[bool, str]:
649
+ """
650
+ Connecte un utilisateur de manière asynchrone avec vérification des restrictions.
651
+ Les utilisateurs ayant le rôle "admin" sont exemptés des restrictions.
652
+
653
+ Args:
654
+ email (str): L'adresse email de l'utilisateur
655
+ password (str): Le mot de passe de l'utilisateur
656
+
657
+ Returns:
658
+ Tuple[bool, str]: Un tuple contenant:
659
+ - bool: True si la connexion est réussie, False sinon
660
+ - str: Message d'erreur si la connexion a échoué, chaîne vide si réussie
661
+ """
662
+ try:
663
+ ip_address = get_user_ip()
664
+
665
+ # Récupérer l'utilisateur
666
+ cached_user = await auth_cache.get_user_from_cache(email)
667
+ if cached_user:
668
+ # Utilisateur trouvé dans le cache
669
+ log_to_file(f"Utilisateur trouvé dans le cache: {email}", level=logging.INFO)
670
+ user = cached_user
671
+ else:
672
+ # Si pas dans le cache, requête Supabase
673
+ user_query = supabase.table("users").select("*").eq("email", email).execute()
674
+ if not user_query.data:
675
+ return False, "Utilisateur non trouvé."
676
+
677
+ user = user_query.data[0]
678
+ # Mettre en cache pour les prochaines fois
679
+ await auth_cache.set_user_in_cache(user)
680
+ log_to_file(f"Utilisateur mis en cache: {email}", level=logging.INFO)
681
+
682
+ # Vérifier le rôle de l'utilisateur
683
+ is_admin = user['role'] == 'admin'
684
+
685
+ # Si l'utilisateur n'est pas admin, vérifier les restrictions d'IP et de domaine email
686
+ if not is_admin:
687
+ is_valid, error_message = check_access_validity(ip_address, email)
688
+ if not is_valid:
689
+ return False, error_message
690
+
691
+ # Vérifier si le compte est actif
692
+ if not user.get('is_active', True):
693
+ return False, "Ce compte a été désactivé. Contactez l'administrateur."
694
+
695
+ # Vérifier le mot de passe
696
+ if not bcrypt.checkpw(password.encode(), user['password'].encode()):
697
+ return False, "Mot de passe incorrect."
698
+
699
+ # Mettre à jour la dernière connexion
700
+ current_time = datetime.now(pytz.UTC).isoformat()
701
+ supabase.table("users").update({
702
+ "last_login": current_time
703
+ }).eq("id", user['id']).execute()
704
+
705
+ # Mettre à jour le cache avec la nouvelle date de connexion
706
+ user['last_login'] = current_time
707
+ await auth_cache.set_user_in_cache(user)
708
+
709
+ # Générer le token JWT
710
+ expires_at = datetime.now(pytz.UTC) + timedelta(days=1)
711
+ token = jwt.encode({
712
+ "id": user['id'],
713
+ "email": email,
714
+ "nom": user['nom'],
715
+ "prenom": user['prenom'],
716
+ "role": user['role'],
717
+ "exp": expires_at.timestamp()
718
+ }, JWT_SECRET, algorithm="HS256")
719
+
720
+ # Gérer la session
721
+ session_data = {
722
+ "token": token,
723
+ "expires_at": expires_at.isoformat()
724
+ }
725
+
726
+ existing_session = supabase.table("user_sessions").select("*").eq("user_id", user['id']).execute()
727
+ if existing_session.data:
728
+ supabase.table("user_sessions").update(session_data).eq("user_id", user['id']).execute()
729
+ else:
730
+ session_data["user_id"] = user['id']
731
+ supabase.table("user_sessions").insert(session_data).execute()
732
+
733
+ # Configurer la session state
734
+ st.session_state['user'] = jwt.decode(token, JWT_SECRET, algorithms=["HS256"])
735
+ st.session_state['user_theme'] = load_theme_preference()
736
+ st.session_state['authentication_status'] = True
737
+ st.session_state['user_id'] = user['id']
738
+
739
+ # Envoyer la notification de connexion
740
+ notify_security_alert(user['id'],
741
+ f"Nouvelle connexion détectée le {datetime.now(pytz.UTC).strftime('%Y-%m-%d à %H:%M')} UTC"
742
+ )
743
+
744
+ log_to_file(f"Connexion réussie pour l'utilisateur: {email}")
745
+ return True, "Connexion réussie"
746
+
747
+ except Exception as e:
748
+ log_to_file(f"Erreur lors de la connexion : {str(e)}", logging.ERROR)
749
+ return False, f"Erreur lors de la connexion : {str(e)}"
750
+
751
+
752
+ def login_user(email: str, password: str) -> tuple[bool, str]:
753
+ """Version synchrone de login_user pour Streamlit"""
754
+ import asyncio
755
+ try:
756
+ loop = asyncio.new_event_loop()
757
+ asyncio.set_event_loop(loop)
758
+ return loop.run_until_complete(login_user_async(email, password))
759
+ except Exception as e:
760
+ log_to_file(f"Erreur lors de la connexion synchrone : {str(e)}", logging.ERROR)
761
+ return False, f"Erreur lors de la connexion : {str(e)}"
762
+
763
+
764
+ @global_exception_handler
765
+ async def logout():
766
+ try:
767
+ if 'user_id' in st.session_state:
768
+ user_id = st.session_state['user_id']
769
+ email = st.session_state.get('user', {}).get('email')
770
+
771
+ # Notification de déconnexion
772
+ notify_security_alert(user_id,
773
+ f"Déconnexion effectuée le {datetime.now(pytz.UTC).strftime('%Y-%m-%d à %H:%M')} UTC"
774
+ )
775
+
776
+ # Supprimer du cache
777
+ if email:
778
+ await cache_manager.delete(f"user_email:{email}")
779
+
780
+ # Supprimer la session utilisateur
781
+ supabase.table("user_sessions").delete().eq("user_id", user_id).execute()
782
+
783
+ # Désactiver les sessions de chat
784
+ supabase.table("chat_sessions").update({
785
+ "is_active": False
786
+ }).eq("user_id", user_id).eq("is_active", True).execute()
787
+
788
+ except Exception as e:
789
+ log_to_file(f"Erreur lors de la déconnexion : {str(e)}")
790
+ finally:
791
+ # Nettoyer la session state
792
+ keys_to_clear = [
793
+ 'authentication_status',
794
+ 'user',
795
+ 'user_id',
796
+ 'user_theme',
797
+ 'chat_history',
798
+ 'current_chat_session'
799
+ ]
800
+ for key in keys_to_clear:
801
+ if key in st.session_state:
802
+ del st.session_state[key]
803
+ st.rerun()
804
+
805
+ def sync_logout():
806
+ """Wrapper synchrone pour logout"""
807
+ import asyncio
808
+ asyncio.run(logout())
809
+
810
+
811
+
812
+
813
+ @global_exception_handler
814
+ def check_authentication():
815
+ """Vérifie la validité de l'authentification de l'utilisateur"""
816
+ try:
817
+ # Si l'utilisateur n'est pas dans la session, vérifier dans la base de données
818
+ if 'user_id' not in st.session_state:
819
+ # Récupérer toutes les sessions non expirées
820
+ current_time = datetime.now(pytz.UTC)
821
+ sessions = supabase.table("user_sessions").select("*").gt("expires_at", current_time.isoformat()).execute()
822
+
823
+ if sessions.data:
824
+ session = sessions.data[0] # Prendre la session la plus récente
825
+ try:
826
+ # Vérifier si le token est valide
827
+ user = jwt.decode(session['token'], JWT_SECRET, algorithms=["HS256"])
828
+ # Si le token est valide, restaurer la session
829
+ st.session_state['user'] = user
830
+ st.session_state['user_id'] = session['user_id']
831
+ st.session_state['authentication_status'] = True
832
+ st.session_state['user_theme'] = load_theme_preference()
833
+
834
+ # Rafraîchir le token si nécessaire
835
+ expires_at = datetime.fromisoformat(session['expires_at'].replace('Z', '+00:00'))
836
+ if expires_at - current_time < timedelta(hours=1):
837
+ refresh_token()
838
+ return True
839
+ except jwt.InvalidTokenError:
840
+ pass
841
+
842
+ return False
843
+
844
+ # Si l'utilisateur est dans la session, vérifier si le token est toujours valide
845
+ user_id = st.session_state['user_id']
846
+ session = supabase.table("user_sessions").select("*").eq("user_id", user_id).order('created_at', desc=True).limit(1).execute()
847
+
848
+ if session.data:
849
+ session = session.data[0]
850
+ try:
851
+ expires_at = datetime.fromisoformat(session['expires_at'].replace('Z', '+00:00'))
852
+ if expires_at > datetime.now(pytz.UTC):
853
+ # Vérifier si le token est valide
854
+ jwt.decode(session['token'], JWT_SECRET, algorithms=["HS256"])
855
+
856
+ # Rafraîchir le token si nécessaire
857
+ if expires_at - datetime.now(pytz.UTC) < timedelta(hours=1):
858
+ refresh_token()
859
+ return True
860
+ except jwt.InvalidTokenError:
861
+ pass
862
+
863
+ # Si on arrive ici, la session n'est plus valide
864
+ sync_logout()
865
+ return False
866
+
867
+ except Exception as e:
868
+ print(f"Erreur lors de la vérification de l'authentification : {str(e)}")
869
+ return False
870
+
871
+
872
+ def send_verification_email(email: str, token: str) -> bool:
873
+ """Envoie l'email de vérification"""
874
+ try:
875
+ subject = "Vérification de votre compte"
876
+ # Utiliser directement localhost:8501 sans http://
877
+ base_url = "localhost:8501"
878
+
879
+ # Construire l'URL avec le format correct pour Streamlit
880
+ verification_url = f"http://{base_url}/Verify_email?token={token}"
881
+ # Note: "Verify_email" correspond au nom de la page dans Streamlit (le nom du fichier sans .py)
882
+
883
+ html_content = f"""
884
+ <html>
885
+ <body>
886
+ <h2>Vérification de votre compte</h2>
887
+ <p>Merci de vous être inscrit. Pour finaliser votre inscription, veuillez cliquer sur le lien ci-dessous :</p>
888
+ <p><a href="{verification_url}">Vérifier mon compte</a></p>
889
+ <p>Ce lien est valable pendant 7 jours.</p>
890
+ <p>Si vous n'avez pas créé de compte, veuillez ignorer cet email.</p>
891
+ <p>Si le lien ne fonctionne pas, copiez et collez cette URL dans votre navigateur :</p>
892
+ <p>{verification_url}</p>
893
+ </body>
894
+ </html>
895
+ """
896
+ log_to_file(f"URL de vérification générée : {verification_url}", level=logging.INFO)
897
+ return send_email(email, subject, html_content)
898
+ except Exception as e:
899
+ log_to_file(f"Erreur lors de l'envoi de l'email de vérification : {str(e)}", level=logging.ERROR)
900
+ return False
901
+
902
+
903
+
904
+
905
+
906
+ def initialize_missing_notification_preferences():
907
+ """Initialise les préférences de notification pour les utilisateurs qui n'en ont pas"""
908
+ try:
909
+ # Récupérer tous les utilisateurs
910
+ users = supabase.table("users").select("id").execute()
911
+
912
+ # Récupérer les utilisateurs qui ont déjà des préférences
913
+ existing_prefs = supabase.table("notification_preferences").select("user_id").execute()
914
+ existing_user_ids = [pref['user_id'] for pref in existing_prefs.data]
915
+
916
+ # Pour chaque utilisateur sans préférences
917
+ for user in users.data:
918
+ if user['id'] not in existing_user_ids:
919
+ # Créer les préférences par défaut
920
+ default_notifications = {
921
+ "user_id": user['id'],
922
+ "email_notifications": True,
923
+ "security_alerts": True,
924
+ "assistant_updates": True,
925
+ "daily_summary": True,
926
+ "created_at": datetime.now(pytz.UTC).isoformat(),
927
+ "updated_at": datetime.now(pytz.UTC).isoformat()
928
+ }
929
+
930
+ # Insérer les préférences
931
+ supabase.table("notification_preferences").insert(default_notifications).execute()
932
+ log_to_file(f"Préférences de notification initialisées pour l'utilisateur {user['id']}", level=logging.INFO)
933
+
934
+ except Exception as e:
935
+ log_to_file(f"Erreur lors de l'initialisation des préférences : {str(e)}", level=logging.ERROR)
936
+
937
+
938
+ # Fin de la section: Authentification (login, sync_logout)
939
+ # ===============================================================================
940
+
941
+
942
+
943
+
944
+ # ================================================================================
945
+ # Début de la section: Chat et Interface utilisateur
946
+
947
+
948
+ # fonction pour initialiser le choix de l'assistant
949
+ @global_exception_handler #décorateur @global_exception_handler pour la fonction plus haut def global_exeption_hundler (func)
950
+ def init_session_state():
951
+ if 'authentication_status' not in st.session_state:
952
+ st.session_state['authentication_status'] = None
953
+ if 'user' not in st.session_state:
954
+ st.session_state['user'] = None
955
+ if 'chat_history' not in st.session_state:
956
+ st.session_state['chat_history'] = []
957
+ # Assurez-vous que current_assistant a toujours une valeur par défaut
958
+ if 'current_assistant' not in st.session_state:
959
+ st.session_state['current_assistant'] = 'insuranceSANTE' # Valeur par défaut
960
+ if 'user_theme' not in st.session_state:
961
+ st.session_state['user_theme'] = load_theme_preference()
962
+ if 'sidebar_bg' not in st.session_state:
963
+ st.session_state.sidebar_bg = None
964
+ if 'sidebar_bg_color' not in st.session_state:
965
+ st.session_state.sidebar_bg_color = "#f0f2f6" # Couleur par défaut
966
+
967
+
968
+ @global_exception_handler #décorateur @global_exception_handler pour la fonction plus haut def global_exeption_hundler (func)
969
+ async def test_flowise_connection():
970
+ """Test asynchrone des connexions Flowise"""
971
+ client = AsyncFlowiseClient()
972
+ try:
973
+ results = await client.check_health()
974
+
975
+ for assistant_type, is_healthy in results.items():
976
+ if is_healthy:
977
+ st.success(f"Connexion réussie à l'assistant {assistant_type}!")
978
+ else:
979
+ st.error(f"La connexion a échoué pour l'assistant {assistant_type}")
980
+
981
+ return all(results.values())
982
+ except Exception as e:
983
+ st.error(f"Erreur lors du test des connexions : {str(e)}")
984
+ log_to_file(f"Erreur lors du test des connexions : {str(e)}", level=logging.ERROR)
985
+ return False
986
+ finally:
987
+ await client.close()
988
+
989
+
990
+ @timed_cache(expires_after=timedelta(minutes=30))
991
+ @global_exception_handler #décorateur @global_exception_handler pour la fonction plus haut def global_exeption_hundler (func)
992
+ async def query_flowise(question, assistant_type, max_retries=3, timeout=60):
993
+ """Version asynchrone optimisée de query_flowise avec rate limiting"""
994
+ cancel_container = st.empty()
995
+ should_cancel = False
996
+ attempt_count = 0
997
+
998
+ def create_cancel_button():
999
+ nonlocal attempt_count
1000
+ col1, col2 = cancel_container.columns([3, 1])
1001
+ with col2:
1002
+ if st.button("Annuler", key=f"cancel_btn_{attempt_count}"):
1003
+ nonlocal should_cancel
1004
+ should_cancel = True
1005
+ log_to_file("Requête annulée par l'utilisateur")
1006
+ return True
1007
+ attempt_count += 1
1008
+ return False
1009
+
1010
+ client = AsyncFlowiseClient()
1011
+ status_container = None
1012
+
1013
+ try:
1014
+ user_id = str(st.session_state.get('user', {}).get('id', 'anonymous'))
1015
+
1016
+ # Obtenir le compte des requêtes une seule fois au début
1017
+ request_count = await client.rate_limiter.get_user_requests_count(user_id)
1018
+ log_to_file(f"User ID: {user_id} - Requêtes effectuées: {request_count}", level=logging.INFO)
1019
+
1020
+ for attempt in range(max_retries):
1021
+ if should_cancel:
1022
+ if status_container:
1023
+ status_container.empty()
1024
+ cancel_container.empty()
1025
+ return {"error": "Requête annulée par l'utilisateur"}
1026
+
1027
+ status_container = st.status(f"Tentative {attempt + 1}/{max_retries}...", expanded=True)
1028
+
1029
+ try:
1030
+ create_cancel_button()
1031
+ status_container.write(f"Envoi de la requête à l'assistant (timeout: {timeout}s)")
1032
+
1033
+ # Utiliser asyncio.wait_for pour gérer le timeout de manière plus efficace
1034
+ result = await asyncio.wait_for(
1035
+ client.query_assistant(
1036
+ question=question,
1037
+ assistant_type=assistant_type,
1038
+ user_id=user_id
1039
+ ),
1040
+ timeout=timeout
1041
+ )
1042
+
1043
+ if should_cancel:
1044
+ return {"error": "Requête annulée par l'utilisateur"}
1045
+
1046
+ if "error" in result:
1047
+ if "Trop de requêtes" in result["error"]:
1048
+ status_container.write("Limite de requêtes atteinte. Attente avant nouvelle tentative...")
1049
+ # Utiliser une attente exponentielle
1050
+ await asyncio.sleep(min(2 ** attempt, 10))
1051
+ continue
1052
+
1053
+ if attempt < max_retries - 1:
1054
+ status_container.write("Nouvelle tentative...")
1055
+ continue
1056
+
1057
+ return result
1058
+
1059
+ # Succès
1060
+ cancel_container.empty()
1061
+ status_container.update(label="Réponse reçue !", state="complete")
1062
+ return result
1063
+
1064
+ except asyncio.TimeoutError:
1065
+ log_to_file(f"Timeout lors de la tentative {attempt + 1}", level=logging.WARNING)
1066
+ if attempt < max_retries - 1:
1067
+ st.warning(f"Tentative {attempt + 1} a expiré, nouvelle tentative...")
1068
+ # Utiliser une attente exponentielle
1069
+ await asyncio.sleep(min(2 ** attempt, 10))
1070
+ continue
1071
+ return {"error": "Timeout de toutes les requêtes"}
1072
+
1073
+ except Exception as e:
1074
+ log_to_file(f"Erreur lors de la tentative {attempt + 1}: {str(e)}", level=logging.ERROR)
1075
+ if attempt < max_retries - 1:
1076
+ await asyncio.sleep(min(2 ** attempt, 10))
1077
+ continue
1078
+ return {"error": str(e)}
1079
+
1080
+ finally:
1081
+ if status_container and attempt < max_retries - 1:
1082
+ status_container.empty()
1083
+
1084
+ finally:
1085
+ await client.close()
1086
+ if status_container:
1087
+ status_container.empty()
1088
+ cancel_container.empty()
1089
+
1090
+
1091
+ @global_exception_handler
1092
+ def update_message_feedback(message_id, feedback):
1093
+ """Mettre à jour le feedback d'un message"""
1094
+ try:
1095
+ supabase.table("messages").update(
1096
+ {"feedback": feedback}
1097
+ ).eq("message_id", message_id).execute()
1098
+ return True
1099
+ except Exception as e:
1100
+ log_to_file(f"Erreur lors de la mise à jour du feedback : {str(e)}", level=logging.ERROR)
1101
+ return False
1102
+
1103
+
1104
+ # Fonction export_conversation pour retourner les données au lieu de créer un bouton de téléchargement
1105
+ @global_exception_handler
1106
+ def export_conversation():
1107
+ if st.session_state['chat_history']:
1108
+ conversation_data = {
1109
+ "user": st.session_state['user'],
1110
+ "timestamp": datetime.now().isoformat(),
1111
+ "messages": st.session_state['chat_history']
1112
+ }
1113
+ json_data = json.dumps(conversation_data, indent=2)
1114
+ st.download_button(
1115
+ label="Télécharger la conversation",
1116
+ data=json_data,
1117
+ file_name="conversation_export.json",
1118
+ mime="application/json"
1119
+ )
1120
+ else:
1121
+ st.warning("Aucune conversation à exporter.")
1122
+
1123
+
1124
+ @global_exception_handler
1125
+ def check_assistant_access(user_id: int, assistant_type: str) -> bool:
1126
+ """
1127
+ Vérifie si l'utilisateur a accès à un assistant spécifique.
1128
+ """
1129
+ try:
1130
+ # Si l'utilisateur est admin, il a accès à tout
1131
+ user = supabase.table("users").select("role").eq("id", user_id).execute()
1132
+ if user.data and user.data[0]['role'] == 'admin':
1133
+ return True
1134
+
1135
+ # Sinon, vérifier les permissions spécifiques
1136
+ response = supabase.table("user_assistant_permissions").select("is_authorized").eq(
1137
+ "user_id", user_id
1138
+ ).eq("assistant_type", assistant_type).execute()
1139
+
1140
+ if response.data:
1141
+ return response.data[0]['is_authorized']
1142
+ return False
1143
+ except Exception as e:
1144
+ log_to_file(f"Erreur lors de la vérification des accès : {str(e)}", level=logging.ERROR)
1145
+ return False
1146
+
1147
+
1148
+ @global_exception_handler #décorateur @global_exception_handler pour la fonction plus haut def global_exeption_hundler (func)
1149
+ def improved_ui():
1150
+ """
1151
+ Interface utilisateur principale avec vérification des autorisations.
1152
+ """
1153
+ init_session_state()
1154
+ remove_streamlit_style()
1155
+ hide_pages()
1156
+ current_theme = st.session_state.user_theme
1157
+ apply_theme(current_theme)
1158
+ inject_custom_css(current_theme)
1159
+ add_sidebar_logo(current_theme)
1160
+ force_streamlit_style_update()
1161
+ apply_chat_styles(current_theme)
1162
+ inject_custom_toggle_css(current_theme)
1163
+ save_theme_preference(current_theme)
1164
+ check_and_send_daily_summary()
1165
+ create_custom_footer()
1166
+ asyncio.run(test_flowise_connection())
1167
+
1168
+ if 'user_id' not in st.session_state:
1169
+ st.error("Session utilisateur non valide")
1170
+ return
1171
+
1172
+ if 'current_chat_session' not in st.session_state:
1173
+ session_id = sync_load_active_chat_session(st.session_state['user_id'])
1174
+ if not session_id:
1175
+ st.error("Impossible de charger la session de chat")
1176
+ return
1177
+
1178
+ if 'chat_history' not in st.session_state:
1179
+ st.session_state.chat_history = sync_load_chat_history(st.session_state['current_chat_session'])
1180
+
1181
+ with st.sidebar:
1182
+ st.markdown(f"### Bienvenue, {st.session_state['user']['prenom']} {st.session_state['user']['nom']}")
1183
+ user_id = st.session_state['user']['id']
1184
+
1185
+ # Section Admin - Placer ceci en premier, avant tout autre élément de la sidebar
1186
+ if 'role' in st.session_state['user'] and st.session_state['user']['role'] == 'admin':
1187
+ with st.container(): # Utiliser un container pour regrouper les éléments admin
1188
+ st.write("### 🔧 Administration")
1189
+ if st.button("Accéder au Dashboard Admin"):
1190
+ st.session_state['show_admin'] = True
1191
+ st.rerun()
1192
+ st.write("---") # Séparateur
1193
+
1194
+ # Liste complète des assistants avec leurs détails
1195
+ assistant_options = {
1196
+ 'insuranceSANTE': 'Colibri Vitalité ❣️',
1197
+ 'insuranceCAR': 'Colibri Carburant 🚘',
1198
+ 'insuranceBTP': 'Colibri Batisseur 🏠',
1199
+ 'Pilotage': 'Colibri Tatillon 👨🏻‍✈️',
1200
+ 'RH': 'Colibri Equipage 🪪',
1201
+ }
1202
+
1203
+ # Filtrer les assistants autorisés
1204
+ authorized_assistants = {
1205
+ key: value for key, value in assistant_options.items()
1206
+ if check_assistant_access(user_id, key)
1207
+ }
1208
+
1209
+ if not authorized_assistants:
1210
+ st.warning("Vous n'avez accès à aucun assistant pour le moment. Contactez un administrateur pour obtenir les autorisations nécessaires.")
1211
+ return
1212
+
1213
+ # Sélection de l'assistant
1214
+ selected_assistant = st.selectbox(
1215
+ "Choisissez votre assistant",
1216
+ options=list(authorized_assistants.keys()),
1217
+ format_func=lambda x: authorized_assistants[x],
1218
+ key='assistant_selector'
1219
+ )
1220
+
1221
+ if selected_assistant != st.session_state.get('current_assistant'):
1222
+ st.session_state.current_assistant = selected_assistant
1223
+ st.session_state.chat_history = []
1224
+ st.rerun()
1225
+
1226
+ # Menu dépliant pour les actions utilisateur
1227
+ with st.expander("Actions utilisateur", expanded=False):
1228
+ theme_switcher()
1229
+ # Bouton pour accéder au profil utilisateur
1230
+ if st.button("Mon Profil 👤"):
1231
+ st.switch_page("pages/profile.py")
1232
+
1233
+ if st.button("Vider le cache 🚮"):
1234
+ cache_store.clear()
1235
+ st.success("Cache vidé avec succès!")
1236
+
1237
+ if st.button("Effacer l'historique 📜"):
1238
+ st.session_state.chat_history = []
1239
+ st.rerun()
1240
+
1241
+ if st.button("Exporter la conversation 📤"):
1242
+ export_conversation()
1243
+
1244
+ if st.button("Déconnexion"):
1245
+ sync_logout()
1246
+
1247
+ # Affichage des permissions actuelles
1248
+ with st.expander("Vos accès", expanded=False):
1249
+ st.write("#### Assistants autorisés")
1250
+ for assistant_type, assistant_name in assistant_options.items():
1251
+ has_access = check_assistant_access(user_id, assistant_type)
1252
+ icon = "✅" if has_access else "❌"
1253
+ st.write(f"{icon} {assistant_name}")
1254
+
1255
+ # Charger l'historique des messages
1256
+ if 'chat_history' not in st.session_state:
1257
+ st.session_state.chat_history = load_chat_history(st.session_state['current_chat_session'])
1258
+
1259
+ # Zone de chat principale
1260
+ chat_container = st.container()
1261
+ with chat_container:
1262
+ for message in st.session_state.chat_history:
1263
+ with st.chat_message(message["role"]):
1264
+ st.markdown(message["content"])
1265
+ if message["role"] == "assistant":
1266
+ col1, spacer1, col2, spacer2, col4 = st.columns([1, 1, 1, 2, 16])
1267
+ with col1:
1268
+ if st.button("👍", key=f"like_{message['message_id']}"):
1269
+ if update_message_feedback(message['message_id'], 'positive'):
1270
+ message['feedback'] = 'positive'
1271
+ st.rerun()
1272
+ with col2:
1273
+ if st.button("👎", key=f"dislike_{message['message_id']}"):
1274
+ if update_message_feedback(message['message_id'], 'negative'):
1275
+ message['feedback'] = 'negative'
1276
+ st.rerun()
1277
+ with col4:
1278
+ if message.get('feedback'):
1279
+ st.write(f"Feedback: {'Positif' if message['feedback'] == 'positive' else 'Négatif'}")
1280
+
1281
+ # Zone de saisie
1282
+ user_input = st.chat_input("Posez votre question ici...")
1283
+ if user_input:
1284
+ # Sauvegarder et afficher le message utilisateur
1285
+ saved_message = sync_save_message(
1286
+ st.session_state['current_chat_session'],
1287
+ "user",
1288
+ user_input,
1289
+ st.session_state.current_assistant
1290
+ )
1291
+ if saved_message:
1292
+ st.session_state.chat_history.append({
1293
+ "role": "user",
1294
+ "content": user_input,
1295
+ "message_id": saved_message["message_id"],
1296
+ "assistant_type": st.session_state.current_assistant
1297
+ })
1298
+
1299
+ with st.spinner("L'assistant réfléchit..."):
1300
+ response = asyncio.run(query_flowise(
1301
+ question=user_input,
1302
+ assistant_type=st.session_state.current_assistant,
1303
+ max_retries=3,
1304
+ timeout=60
1305
+ ))
1306
+
1307
+ if 'error' in response:
1308
+ st.error(f"Erreur: {response['error']}")
1309
+ else:
1310
+ # Sauvegarder et afficher la réponse de l'assistant
1311
+ saved_message = sync_save_message(
1312
+ st.session_state['current_chat_session'],
1313
+ "assistant",
1314
+ response['answer'],
1315
+ st.session_state.current_assistant
1316
+ )
1317
+ if saved_message:
1318
+ st.session_state.chat_history.append({
1319
+ "role": "assistant",
1320
+ "content": response['answer'],
1321
+ "message_id": saved_message["message_id"],
1322
+ "assistant_type": st.session_state.current_assistant
1323
+ })
1324
+
1325
+ st.rerun()
1326
+
1327
+ # Aide contextuelle
1328
+ with st.expander("Aide"):
1329
+ st.markdown("""
1330
+ ### Comment utiliser cette application :
1331
+ 1. **Poser une question** : Tapez votre question dans la zone de texte en bas de l'écran et appuyez sur Entrée.
1332
+ 2. **Historique du chat** : Vos conversations précédentes apparaîtront au-dessus de la zone de texte.
1333
+ 3. **Feedback** : Vous pouvez donner un feedback positif (👍) ou négatif (👎) aux réponses de l'assistant.
1334
+ 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.
1335
+ 5. **Personnalisation** : Vous pouvez changer le thème de l'application dans la barre latérale.
1336
+
1337
+ Si vous avez d'autres questions, n'hésitez pas à demander à l'assistant !
1338
+ """)
1339
+
1340
+
1341
+ # Fin de la section: Chat et Interface utilisateur
1342
+ # ================================================================================
1343
+
1344
+
1345
+
1346
+
1347
+
1348
+
1349
+ # ===============================================================================
1350
+ # Début de la section: Gestion des sessions
1351
+
1352
+
1353
+ #générer des tokens uniques pour les sessions de chat dans Supabase
1354
+ @global_exception_handler
1355
+ def generate_chat_session_token():
1356
+ """Générer un nouveau token UUID pour la session de chat"""
1357
+ return str(uuid.uuid4())
1358
+
1359
+
1360
+ #Gère le rafraîchissement des tokens JWT dans Supabase, utilisée pour maintenir la session active
1361
+ @global_exception_handler
1362
+ def refresh_token():
1363
+ if 'user_id' not in st.session_state:
1364
+ return
1365
+
1366
+ user_id = st.session_state['user_id']
1367
+ try:
1368
+ new_expires_at = datetime.now(pytz.UTC) + timedelta(days=1)
1369
+ new_token = jwt.encode({
1370
+ "id": user_id,
1371
+ "exp": new_expires_at.timestamp()
1372
+ }, JWT_SECRET, algorithm="HS256")
1373
+
1374
+ supabase.table("user_sessions").update({
1375
+ "token": new_token,
1376
+ "expires_at": new_expires_at.isoformat()
1377
+ }).eq("user_id", user_id).execute()
1378
+
1379
+ st.session_state['user'] = jwt.decode(new_token, JWT_SECRET, algorithms=["HS256"])
1380
+ log_to_file("Token rafraîchi avec succès", level=logging.INFO)
1381
+ except Exception as e:
1382
+ log_to_file(f"Erreur lors du rafraîchissement du token : {str(e)}", level=logging.ERROR)
1383
+
1384
+
1385
+ @global_exception_handler
1386
+ async def create_chat_session(user_id: int) -> Optional[str]:
1387
+ """Crée une nouvelle session de chat"""
1388
+ try:
1389
+ # Désactiver les sessions existantes
1390
+ supabase.table("chat_sessions").update({
1391
+ "is_active": False
1392
+ }).eq("user_id", user_id).eq("is_active", True).execute()
1393
+
1394
+ # Créer une nouvelle session
1395
+ new_token = str(uuid.uuid4())
1396
+ current_time = datetime.now(pytz.UTC).isoformat()
1397
+
1398
+ session_data = {
1399
+ "user_id": user_id,
1400
+ "is_active": True,
1401
+ "session_token": new_token,
1402
+ "last_accessed": current_time
1403
+ }
1404
+
1405
+ response = supabase.table("chat_sessions").insert(session_data).execute()
1406
+
1407
+ if response.data:
1408
+ session_id = response.data[0]['session_id']
1409
+ session_data['session_id'] = session_id
1410
+
1411
+ # Mettre à jour le cache
1412
+ await auth_cache.update_session_and_history(session_data)
1413
+
1414
+ # Mettre à jour la session state
1415
+ st.session_state['current_chat_session'] = session_id
1416
+ st.session_state['chat_history'] = []
1417
+
1418
+ log_to_file(f"Nouvelle session créée : {session_id}", level=logging.INFO)
1419
+ return session_id
1420
+
1421
+ return None
1422
+ except Exception as e:
1423
+ log_to_file(f"Erreur lors de la création de la session : {str(e)}", level=logging.ERROR)
1424
+ return None
1425
+
1426
+
1427
+ @global_exception_handler
1428
+ async def get_active_chat_session(user_id: int) -> Optional[str]:
1429
+ """Récupère la session de chat active depuis le cache"""
1430
+ try:
1431
+ session = await auth_cache.get_active_session(user_id)
1432
+ if session:
1433
+ st.session_state['current_chat_session'] = session['session_id']
1434
+ return session['session_id']
1435
+ return await create_chat_session(user_id)
1436
+ except Exception as e:
1437
+ log_to_file(f"Erreur lors de la récupération de la session de chat : {str(e)}", level=logging.ERROR)
1438
+ return None
1439
+
1440
+
1441
+ @global_exception_handler
1442
+ async def save_message(session_id: str, role: str, content: str, assistant_type: Optional[str] = None) -> Optional[Dict]:
1443
+ """Sauvegarde un message et met à jour le cache"""
1444
+ try:
1445
+ data = {
1446
+ "session_id": session_id,
1447
+ "role": role,
1448
+ "content": content,
1449
+ "assistant_type": assistant_type,
1450
+ "created_at": datetime.now(pytz.UTC).isoformat()
1451
+ }
1452
+
1453
+ response = supabase.table("messages").insert(data).execute()
1454
+
1455
+ if response.data:
1456
+ message_data = response.data[0]
1457
+
1458
+ # Récupérer la session pour la mise à jour du cache
1459
+ session = await auth_cache.get_active_session(st.session_state['user_id'])
1460
+ if session:
1461
+ await auth_cache.update_session_and_history(session, message_data)
1462
+
1463
+ return message_data
1464
+ return None
1465
+ except Exception as e:
1466
+ log_to_file(f"Erreur lors de la sauvegarde du message : {str(e)}", level=logging.ERROR)
1467
+ return None
1468
+
1469
+ def sync_save_message(session_id: str, role: str, content: str, assistant_type: Optional[str] = None) -> Optional[Dict]:
1470
+ """Version synchrone de save_message"""
1471
+ return asyncio.run(save_message(session_id, role, content, assistant_type))
1472
+
1473
+ async def load_chat_history(session_id: str) -> List[Dict]:
1474
+ """Charge l'historique des messages depuis le cache Redis"""
1475
+ try:
1476
+ history = await auth_cache.get_chat_history(session_id)
1477
+ log_to_file(f"Historique chargé depuis le cache pour la session {session_id}", level=logging.INFO)
1478
+ return history
1479
+ except Exception as e:
1480
+ log_to_file(f"Erreur lors du chargement de l'historique : {str(e)}", level=logging.ERROR)
1481
+ return []
1482
+
1483
+ def sync_load_chat_history(session_id: str) -> List[Dict]:
1484
+ """Version synchrone de load_chat_history"""
1485
+ return asyncio.run(load_chat_history(session_id))
1486
+
1487
+ def verify_session_integrity():
1488
+ """Vérifier l'intégrité des données de session"""
1489
+ required_keys = ['current_chat_session', 'chat_session_token', 'chat_history']
1490
+ missing_keys = [key for key in required_keys if key not in st.session_state]
1491
+
1492
+ if missing_keys:
1493
+ log_to_file(f"Données de session manquantes : {missing_keys}", level=logging.WARNING)
1494
+ if 'user_id' in st.session_state:
1495
+ load_active_chat_session(st.session_state['user_id'])
1496
+ return False
1497
+ return True
1498
+
1499
+
1500
+ async def load_active_chat_session(user_id: int) -> Optional[str]:
1501
+ """Charge la session de chat active depuis le cache"""
1502
+ try:
1503
+ session = await auth_cache.get_active_session(user_id)
1504
+
1505
+ if session:
1506
+ session_id = session['session_id']
1507
+ st.session_state['current_chat_session'] = session_id
1508
+
1509
+ # Charger l'historique
1510
+ messages = await load_chat_history(session_id)
1511
+ st.session_state['chat_history'] = messages
1512
+
1513
+ log_to_file(f"Session active chargée : {session_id}", level=logging.INFO)
1514
+ return session_id
1515
+
1516
+ return await create_chat_session(user_id)
1517
+
1518
+ except Exception as e:
1519
+ log_to_file(f"Erreur lors du chargement de la session : {str(e)}", level=logging.ERROR)
1520
+ return None
1521
+
1522
+ def sync_load_active_chat_session(user_id: int) -> Optional[str]:
1523
+ """Version synchrone de load_active_chat_session"""
1524
+ return asyncio.run(load_active_chat_session(user_id))
1525
+
1526
+ # Fin de la section: Gestion des sessions
1527
+ # ===============================================================================
1528
+
1529
+
1530
+
1531
+
1532
+
1533
+ # ===============================================================================
1534
+ # Début de la section principale (main)
1535
+
1536
+
1537
+ @global_exception_handler #décorateur @global_exception_handler pour la fonction plus haut def global_exeption_hundler (func)
1538
+ def main():
1539
+ asyncio.run(initialize_app())
1540
+ init_session_state()
1541
+ initialize_missing_notification_preferences() #initialiser les préférences de notifications manquantes
1542
+ hide_pages()
1543
+
1544
+ if 'authentication_status' not in st.session_state:
1545
+ st.session_state['authentication_status'] = None
1546
+
1547
+ if st.session_state['authentication_status'] is not True:
1548
+ authentication_page()
1549
+ else:
1550
+ is_session_valid = check_authentication()
1551
+
1552
+ if is_session_valid:
1553
+ apply_theme(st.session_state['user_theme'])
1554
+
1555
+ # Vérifier si nous devons afficher le dashboard admin
1556
+ if st.session_state.get('show_admin', False) and st.session_state['user']['role'] == 'admin':
1557
+ asyncio.run(render_admin_dashboard())
1558
+ else:
1559
+ improved_ui()
1560
+
1561
+ else:
1562
+ st.warning("Votre session a expiré. Veuillez vous reconnecter.")
1563
+ authentication_page()
1564
+
1565
+ # Point d'entrée de l'application
1566
+ if __name__ == "__main__":
1567
+ main()
1568
+
1569
+ # Fin de la section principale (main)
1570
+ # ===============================================================================
requirements.txt ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ streamlit # Pour la dernière version
2
+ supabase
3
+ requests
4
+ bcrypt
5
+ pyjwt
6
+ python-dotenv
7
+ Pillow # Pour l'utilisation de Image de PIL
8
+ streamlit-cookies-manager
9
+ extra-streamlit-components
10
+ pandas
11
+ aiohttp>=3.8.1 # implémenter les requêtes asynchrones avec asyncio
12
+ asyncio>=3.4.3 # implémenter les requêtes asynchrones avec asyncio
13
+ aiohttp-ratelimiter>=0.1.0 # pour implémenter le rate limiting
14
+ pybreaker>=0.6.0 # pour l'implémentation du circuit breaker
15
+ redis>=5.0.1 # implémentation d'un système de cache avec Redis afin d'optimiser les échanges avec la base de données
16
+ flowise # Client Python officiel pour Flowise