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

Ajoute des fichiers et sous-dossiers supplémentaires

Browse files
.dockerignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ __pycache__
2
+ *.pyc
3
+ .env
4
+ venv/
5
+ .git/
.streamlit/config.toml ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ [client]
2
+ showSidebarNavigation = false
.vscode/extensions.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "recommendations": [
3
+ "kodu-ai.claude-dev-experimental"
4
+ ]
5
+ }
.vscode/settings.json ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {
2
+ "arena.codePrivacySettings": "Private",
3
+ "arena.maxOutputLines": 50
4
+ }
admin_page.py ADDED
@@ -0,0 +1,875 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ from typing import Dict, List, Optional, Tuple, Any, Callable, Awaitable
3
+ import pandas as pd
4
+ from datetime import datetime
5
+ import pytz
6
+ import re
7
+ import json
8
+ import asyncio
9
+ import os
10
+ from dotenv import load_dotenv
11
+ from utils.admin import (
12
+ get_all_users,
13
+ toggle_user_status,
14
+ change_user_role,
15
+ get_user_stats,
16
+ update_user_info,
17
+ get_user_permissions,
18
+ update_user_permissions,
19
+ supabase
20
+ )
21
+ from utils.theme_utils import apply_tooltip_and_button_styles
22
+ from utils.async_flowise import AsyncFlowiseClient
23
+
24
+ class FlowiseStreamingClient:
25
+ def __init__(self):
26
+ """
27
+ Initialise le client de streaming pour les assistants Flowise
28
+ """
29
+ self.flowise_client = AsyncFlowiseClient()
30
+ self.assistant_types = list(self.flowise_client.api_urls.keys())
31
+
32
+ async def get_service_health(self) -> dict:
33
+ """
34
+ Vérifie la santé des services Flowise
35
+ Returns:
36
+ dict: Un dictionnaire avec le statut de chaque assistant
37
+ """
38
+ return await self.flowise_client.check_health()
39
+
40
+ async def stream_assistant_response(
41
+ self,
42
+ question: str,
43
+ assistant_type: str
44
+ ) -> dict:
45
+ """
46
+ Génère une réponse en streaming d'un assistant Flowise
47
+ Args:
48
+ question (str): La question à poser
49
+ assistant_type (str): Le type d'assistant à utiliser
50
+ Returns:
51
+ dict: Réponse finale de l'assistant
52
+ """
53
+ # Vérifier si l'assistant type est valide
54
+ if assistant_type not in self.assistant_types:
55
+ return {"error": f"Type d'assistant non valide : {assistant_type}"}
56
+ full_response = ""
57
+ error = None
58
+ try:
59
+ response_stream = self.flowise_client.query_assistant_stream(question, assistant_type)
60
+ async for chunk in response_stream:
61
+ try:
62
+ chunk_data = json.loads(chunk)
63
+ if 'error' in chunk_data:
64
+ error = chunk_data['error']
65
+ break
66
+ if 'data' in chunk_data:
67
+ # Extraire la partie de la réponse qui est nouvelle
68
+ new_part = chunk_data['data'][len(full_response):]
69
+ full_response = chunk_data['data']
70
+ # Ajouter le nouveau morceau de réponse à l'historique du chat
71
+ add_message("assistant", new_part)
72
+ st.rerun() # Mettre à jour l'interface utilisateur
73
+ except json.JSONDecodeError:
74
+ error = "Erreur de décodage du chunk de réponse"
75
+ break
76
+ except Exception as e:
77
+ error = str(e)
78
+ # Fermer le client si nécessaire
79
+ await self.flowise_client.close()
80
+ # Retourner la réponse complète ou l'erreur
81
+ if error:
82
+ return {"error": error}
83
+ return {"answer": full_response}
84
+
85
+ def get_available_assistants(self) -> List[str]:
86
+ """
87
+ Récupère la liste des assistants disponibles
88
+ Returns:
89
+ List[str]: Liste des types d'assistants
90
+ """
91
+ return self.assistant_types
92
+
93
+ # Fonction d'exemple pour afficher le streaming dans la console
94
+ async def console_stream_callback(chunk: str):
95
+ """
96
+ Callback par défaut pour afficher le streaming dans la console
97
+ Args:
98
+ chunk (str): Le nouveau morceau de réponse
99
+ """
100
+ print(chunk, end='', flush=True)
101
+
102
+ def render_user_permissions(user_id: int):
103
+ """
104
+ Affiche et gère les permissions d'accès aux assistants pour un utilisateur.
105
+ """
106
+ current_permissions = get_user_permissions(user_id)
107
+ col1, col2 = st.columns(2)
108
+
109
+ with col1:
110
+ st.write("##### Colibri Vitalité ❣️")
111
+ is_sante_authorized = current_permissions.get('insuranceSANTE', False)
112
+ new_sante_state = st.toggle(
113
+ "Autoriser l'accès",
114
+ value=is_sante_authorized,
115
+ key=f"perm_{user_id}_sante"
116
+ )
117
+ if new_sante_state != is_sante_authorized:
118
+ success, msg = update_user_permissions(user_id, 'insuranceSANTE', new_sante_state)
119
+ if success:
120
+ st.success("✅ Accès mis à jour")
121
+ else:
122
+ st.error(f"❌ Erreur: {msg}")
123
+
124
+ st.write("##### Colibri Carburant 🚘")
125
+ is_car_authorized = current_permissions.get('insuranceCAR', False)
126
+ new_car_state = st.toggle(
127
+ "Autoriser l'accès",
128
+ value=is_car_authorized,
129
+ key=f"perm_{user_id}_car"
130
+ )
131
+ if new_car_state != is_car_authorized:
132
+ success, msg = update_user_permissions(user_id, 'insuranceCAR', new_car_state)
133
+ if success:
134
+ st.success("✅ Accès mis à jour")
135
+ else:
136
+ st.error(f"❌ Erreur: {msg}")
137
+
138
+ st.write("##### Colibri Batisseur 🏠")
139
+ is_btp_authorized = current_permissions.get('insuranceBTP', False)
140
+ new_btp_state = st.toggle(
141
+ "Autoriser l'accès",
142
+ value=is_btp_authorized,
143
+ key=f"perm_{user_id}_btp"
144
+ )
145
+ if new_btp_state != is_btp_authorized:
146
+ success, msg = update_user_permissions(user_id, 'insuranceBTP', new_btp_state)
147
+ if success:
148
+ st.success("✅ Accès mis à jour")
149
+ else:
150
+ st.error(f"❌ Erreur: {msg}")
151
+
152
+ with col2:
153
+ st.write("##### Colibri Equipage 🪪")
154
+ is_RH_authorized = current_permissions.get('RH', False)
155
+ new_RH_state = st.toggle(
156
+ "Autoriser l'accès",
157
+ value=is_RH_authorized,
158
+ key=f"perm_{user_id}_RH"
159
+ )
160
+ if new_RH_state != is_RH_authorized:
161
+ success, msg = update_user_permissions(user_id, 'RH', new_RH_state)
162
+ if success:
163
+ st.success("✅ Accès mis à jour")
164
+ else:
165
+ st.error(f"❌ Erreur: {msg}")
166
+
167
+ st.write("##### Colibri Tatillon 👨🏻‍✈️")
168
+ is_Pilotage_authorized = current_permissions.get('Pilotage', False)
169
+ new_Pilotage_state = st.toggle(
170
+ "Autoriser l'accès",
171
+ value=is_Pilotage_authorized,
172
+ help="Cet assistant permet de consulter et modifier les données de la base de données. Il est réservé aux administrateurs.",
173
+ key=f"perm_{user_id}_Pilotage"
174
+ )
175
+ if new_Pilotage_state != is_Pilotage_authorized:
176
+ success, msg = update_user_permissions(user_id, 'Pilotage', new_Pilotage_state)
177
+ if success:
178
+ st.success("✅ Accès mis à jour")
179
+ else:
180
+ st.error(f"❌ Erreur: {msg}")
181
+
182
+ def init_chat_history():
183
+ """Initialise l'historique du chat s'il n'existe pas"""
184
+ if "chat_history" not in st.session_state:
185
+ st.session_state.chat_history = []
186
+ if "is_processing" not in st.session_state:
187
+ st.session_state.is_processing = False
188
+ if "message_feedback" not in st.session_state:
189
+ st.session_state.message_feedback = {}
190
+
191
+ def clean_message_content(content: str) -> str:
192
+ """Nettoie le contenu du message des métadonnées JSON"""
193
+ try:
194
+ # Si le contenu est au format JSON
195
+ if content.strip().startswith('{'):
196
+ data = json.loads(content)
197
+ # Si c'est un message utilisateur, extraire la requête
198
+ if 'next_inputs' in data and 'query' in data['next_inputs']:
199
+ return data['next_inputs']['query']
200
+ # Si c'est une réponse de l'assistant
201
+ if 'data' in data:
202
+ return data['data']
203
+ return content
204
+ except:
205
+ return content
206
+
207
+ def add_message(role: str, content: str):
208
+ """Ajoute un message à l'historique du chat"""
209
+ message_id = str(len(st.session_state.chat_history))
210
+ cleaned_content = clean_message_content(content)
211
+ st.session_state.chat_history.append({
212
+ "role": role,
213
+ "content": cleaned_content,
214
+ "message_id": message_id
215
+ })
216
+
217
+ def update_message_feedback(message_id: str, feedback: str):
218
+ """Met à jour le feedback d'un message"""
219
+ st.session_state.message_feedback[message_id] = feedback
220
+
221
+ def display_chat():
222
+ """Affiche l'historique du chat avec un style cohérent"""
223
+ for message in st.session_state.chat_history:
224
+ with st.chat_message(message["role"]):
225
+ st.markdown(message["content"])
226
+
227
+ if message["role"] == "assistant":
228
+ message_id = message.get("message_id", "")
229
+ col1, col2, col3 = st.columns([1, 1, 10])
230
+
231
+ with col1:
232
+ if st.button("👍", key=f"like_{message_id}"):
233
+ update_message_feedback(message_id, "positive")
234
+ st.rerun()
235
+
236
+ with col2:
237
+ if st.button("👎", key=f"dislike_{message_id}"):
238
+ update_message_feedback(message_id, "negative")
239
+ st.rerun()
240
+
241
+ with col3:
242
+ if message_id in st.session_state.message_feedback:
243
+ feedback = st.session_state.message_feedback[message_id]
244
+ st.write(f"Feedback: {'Positif' if feedback == 'positive' else 'Négatif'}")
245
+
246
+ async def render_admin_dashboard():
247
+ """Affiche le tableau de bord administrateur"""
248
+ if 'user' not in st.session_state or st.session_state['user'].get('role') != 'admin':
249
+ st.error("Accès non autorisé. Cette page est réservée aux administrateurs.")
250
+ return
251
+
252
+ current_theme = st.session_state.get('user_theme', 'Clair')
253
+ apply_tooltip_and_button_styles(current_theme)
254
+
255
+ if st.button("← Retour à l'interface principale"):
256
+ st.session_state['show_admin'] = False
257
+ st.rerun()
258
+
259
+ st.title("Dashboard Administrateur")
260
+
261
+ # Création des onglets avec le nouvel onglet de streaming
262
+ tab_users, tab_restrictions, tab_chatbot, tab_chatreact, testfooter, = st.tabs([
263
+ "👥 Gestion Utilisateurs",
264
+ "🔒 Restrictions d'accès",
265
+ "💬 Chatbot",
266
+ "📡 Chat React",
267
+ " 🧪 Test Footer"
268
+ ])
269
+
270
+ # Onglet Gestion Utilisateurs
271
+ with tab_users:
272
+ stats = get_user_stats()
273
+ col1, col2, col3 = st.columns(3)
274
+ with col1:
275
+ st.metric("Total Utilisateurs", stats["total_users"])
276
+ with col2:
277
+ st.metric("Utilisateurs Actifs", stats["active_users"])
278
+ with col3:
279
+ st.metric("Administrateurs", stats["admin_users"])
280
+
281
+ st.subheader("Gestion des Utilisateurs")
282
+ search_term = st.text_input("Rechercher un utilisateur par email ou nom:", "")
283
+ users = get_all_users()
284
+ if users:
285
+ df = pd.DataFrame(users)
286
+ if 'created_at' in df.columns:
287
+ df['created_at'] = pd.to_datetime(df['created_at']).dt.strftime('%Y-%m-%d %H:%M')
288
+ if 'last_login' in df.columns:
289
+ df['last_login'] = pd.to_datetime(df['last_login']).dt.strftime('%Y-%m-%d %H:%M')
290
+ display_columns = ['id', 'email', 'nom', 'prenom', 'role', 'is_active', 'created_at', 'last_login']
291
+
292
+ if search_term:
293
+ df = df[
294
+ df['email'].str.contains(search_term, case=False) |
295
+ df['nom'].str.contains(search_term, case=False) |
296
+ df['prenom'].str.contains(search_term, case=False)
297
+ ]
298
+
299
+ st.dataframe(df[display_columns], use_container_width=True)
300
+
301
+ selected_user_email = st.selectbox(
302
+ "Sélectionner un utilisateur",
303
+ options=df['email'].tolist()
304
+ )
305
+
306
+ if selected_user_email:
307
+ selected_user = df[df['email'] == selected_user_email].iloc[0]
308
+ user_id = selected_user['id']
309
+
310
+ col1, col2 = st.columns(2)
311
+
312
+ with col1:
313
+ with st.expander("Paramètres du compte", expanded=True):
314
+ new_status = st.toggle(
315
+ "Compte actif",
316
+ value=selected_user['is_active'],
317
+ key=f"toggle_{user_id}"
318
+ )
319
+ if new_status != selected_user['is_active']:
320
+ success, msg = toggle_user_status(user_id, new_status)
321
+ if success:
322
+ st.success(msg)
323
+ else:
324
+ st.error(msg)
325
+
326
+ new_role = st.selectbox(
327
+ "Rôle utilisateur",
328
+ options=['user', 'admin'],
329
+ index=0 if selected_user['role'] == 'user' else 1,
330
+ key=f"role_{user_id}"
331
+ )
332
+ if new_role != selected_user['role']:
333
+ success, msg = change_user_role(user_id, new_role)
334
+ if success:
335
+ st.success(msg)
336
+ else:
337
+ st.error(msg)
338
+
339
+ with st.expander("Informations détaillées", expanded=False):
340
+ col3, col4 = st.columns(2)
341
+ with col3:
342
+ new_nom = st.text_input(
343
+ "Nom",
344
+ value=selected_user['nom'],
345
+ key=f"nom_{user_id}"
346
+ )
347
+ new_email = st.text_input(
348
+ "Email",
349
+ value=selected_user['email'],
350
+ key=f"email_{user_id}"
351
+ )
352
+ st.text_input(
353
+ "Date de création",
354
+ value=selected_user['created_at'],
355
+ disabled=True
356
+ )
357
+ with col4:
358
+ new_prenom = st.text_input(
359
+ "Prénom",
360
+ value=selected_user['prenom'],
361
+ key=f"prenom_{user_id}"
362
+ )
363
+ st.text_input(
364
+ "Rôle",
365
+ value=selected_user['role'],
366
+ disabled=True
367
+ )
368
+ st.text_input(
369
+ "Dernière connexion",
370
+ value=selected_user.get('last_login', 'Jamais'),
371
+ disabled=True
372
+ )
373
+
374
+ st.write("##### Informations professionnelles")
375
+ professional_info = selected_user.get('professional_info', {})
376
+ if isinstance(professional_info, str):
377
+ try:
378
+ professional_info = json.loads(professional_info)
379
+ except:
380
+ professional_info = {}
381
+
382
+ col5, col6 = st.columns(2)
383
+ with col5:
384
+ new_profession = st.text_input(
385
+ "Profession",
386
+ value=professional_info.get('profession', ''),
387
+ key=f"prof_{user_id}"
388
+ )
389
+ with col6:
390
+ new_entreprise = st.text_input(
391
+ "Entreprise",
392
+ value=professional_info.get('entreprise', ''),
393
+ key=f"entr_{user_id}"
394
+ )
395
+
396
+ if st.button("Mettre à jour les informations", key=f"update_{user_id}"):
397
+ if not re.match(r"[^@]+@[^@]+\.[^@]+", new_email):
398
+ st.error("Format d'email invalide")
399
+ return
400
+
401
+ if new_email != selected_user['email']:
402
+ existing_email = supabase.table("users").select("id").eq("email", new_email).neq("id", user_id).execute()
403
+ if existing_email.data:
404
+ st.error("Cet email est déjà utilisé par un autre utilisateur")
405
+ return
406
+
407
+ updated_info = {
408
+ "nom": new_nom,
409
+ "prenom": new_prenom,
410
+ "email": new_email,
411
+ "professional_info": {
412
+ "profession": new_profession,
413
+ "entreprise": new_entreprise
414
+ }
415
+ }
416
+
417
+ success, msg = update_user_info(user_id, updated_info)
418
+ if success:
419
+ st.success("Informations mises à jour avec succès")
420
+ st.rerun()
421
+ else:
422
+ st.error(f"Erreur lors de la mise à jour : {msg}")
423
+
424
+ with col2:
425
+ with st.expander("Gestion des accès aux assistants", expanded=False):
426
+ render_user_permissions(user_id)
427
+
428
+ else:
429
+ st.warning("Aucun utilisateur trouvé.")
430
+
431
+ # Onglet Restrictions d'accès
432
+ with tab_restrictions:
433
+ st.header("Restrictions d'accès")
434
+
435
+ current_restrictions = supabase.table("access_restrictions").select("*").eq("is_active", True).execute()
436
+ restrictions = current_restrictions.data[0] if current_restrictions.data else {}
437
+
438
+ with st.form("access_restrictions_form"):
439
+ st.subheader("🌐 Domaines email autorisés")
440
+ st.info("Définissez les domaines email autorisés pour l'inscription. Un domaine par ligne.")
441
+ domains_text = st.text_area(
442
+ "Domaines autorisés",
443
+ value="\n".join(restrictions.get('allowed_email_domains', [])),
444
+ help="Exemple: entreprise.com \nautreentreprise.fr",
445
+ placeholder="Entrez les domaines (un par ligne)"
446
+ )
447
+
448
+ st.subheader("🌐 Adresses IP autorisées")
449
+ st.info("Définissez les adresses IP autorisées pour l'inscription. Une adresse par ligne.")
450
+ ip_ranges_text = st.text_area(
451
+ "Adresses IP autorisées",
452
+ value="\n".join(restrictions.get('allowed_ip_ranges', [])),
453
+ help="Exemple: 192.168.1.1 \n10.0.0.1",
454
+ placeholder="Entrez les adresses IP (une par ligne)"
455
+ )
456
+
457
+ col1, col2, spacer1, col4 = st.columns([4,4,6,3])
458
+ with col1:
459
+ is_email_active = st.toggle(
460
+ "Activer les restrictions d'email",
461
+ value=restrictions.get('is_email_active', False),
462
+ help="Activer/Désactiver les restrictions d'accès par email"
463
+ )
464
+
465
+ with col2:
466
+ is_ip_active = st.toggle(
467
+ "Activer les restrictions d'IP",
468
+ value=restrictions.get('is_ip_active', False),
469
+ help="Activer/Désactiver les restrictions d'accès par IP"
470
+ )
471
+
472
+ with col4:
473
+ st.markdown("###")
474
+ submit_button = st.form_submit_button("💾 Enregistrer les restrictions")
475
+
476
+ if submit_button:
477
+ try:
478
+ domains = [
479
+ d.strip().lower()
480
+ for d in domains_text.split('\n')
481
+ if d.strip() and '.' in d.strip()
482
+ ]
483
+
484
+ ip_ranges = [
485
+ ip.strip()
486
+ for ip in ip_ranges_text.split('\n')
487
+ if ip.strip()
488
+ ]
489
+
490
+ update_data = {
491
+ "allowed_email_domains": domains,
492
+ "allowed_ip_ranges": ip_ranges,
493
+ "is_email_active": is_email_active,
494
+ "is_ip_active": is_ip_active,
495
+ "updated_at": datetime.now(pytz.UTC).isoformat()
496
+ }
497
+
498
+ if current_restrictions.data:
499
+ supabase.table("access_restrictions").update(
500
+ update_data
501
+ ).eq("id", current_restrictions.data[0]['id']).execute()
502
+ else:
503
+ supabase.table("access_restrictions").insert(update_data).execute()
504
+
505
+ st.success("✅ Restrictions mises à jour avec succès!")
506
+
507
+ if is_email_active and domains:
508
+ st.info(f"Domaines autorisés : {', '.join(domains)}")
509
+ elif is_email_active and not domains:
510
+ st.warning("⚠️ Aucun domaine spécifié - l'inscription sera bloquée pour tous les domaines")
511
+ else:
512
+ st.info("ℹ️ Les restrictions d'email sont désactivées - tous les domaines sont autorisés")
513
+
514
+ if is_ip_active and ip_ranges:
515
+ st.info(f"Adresses IP autorisées : {', '.join(ip_ranges)}")
516
+ elif is_ip_active and not ip_ranges:
517
+ st.warning("⚠️ Aucune adresse IP spécifiée - l'inscription sera bloquée pour toutes les adresses IP")
518
+ else:
519
+ st.info("ℹ️ Les restrictions d'IP sont désactivées - toutes les adresses IP sont autorisées")
520
+
521
+ except Exception as e:
522
+ st.error(f"❌ Erreur lors de la mise à jour des restrictions : {str(e)}")
523
+
524
+ if restrictions.get('is_email_active') or restrictions.get('is_ip_active'):
525
+ st.subheader("📊 Statistiques des restrictions")
526
+ try:
527
+ users = supabase.table("users").select("email").execute()
528
+ if users.data:
529
+ domains_count = {}
530
+ for user in users.data:
531
+ domain = user['email'].split('@')[1].lower()
532
+ domains_count[domain] = domains_count.get(domain, 0) + 1
533
+
534
+ st.markdown("##### Répartition des utilisateurs par domaine")
535
+ for domain, count in domains_count.items():
536
+ status = "✅" if not restrictions.get('allowed_email_domains') or domain in restrictions.get('allowed_email_domains', []) else "❌"
537
+ st.text(f"{status} {domain}: {count} utilisateur(s)")
538
+ except Exception as e:
539
+ st.error(f"Erreur lors du calcul des statistiques : {str(e)}")
540
+
541
+
542
+ with tab_chatbot:
543
+ st.subheader("💬 Chatbot")
544
+
545
+ def add_flowise_chat():
546
+ assistant_type = st.selectbox(
547
+ "Sélectionner l'assistant",
548
+ options=['SANTE', 'CAR', 'BTP', 'RH', 'PILOTAGE'],
549
+ format_func=lambda x: {
550
+ 'SANTE': 'Colibri Vitalité ',
551
+ 'CAR': 'Colibri Carburant ',
552
+ 'BTP': 'Colibri Batisseur ',
553
+ 'RH': 'Colibri Equipage ',
554
+ 'PILOTAGE': 'Colibri Tatillon '
555
+ }[x],
556
+ key="chatbot_assistant_select"
557
+ )
558
+
559
+ # Définir le titre du chatbot en fonction de l'assistant sélectionné
560
+ assistant_titles = {
561
+ 'SANTE': 'Colibri Vitalité',
562
+ 'CAR': 'Colibri Carburant',
563
+ 'BTP': 'Colibri Batisseur',
564
+ 'RH': 'Colibri Equipage',
565
+ 'PILOTAGE': 'Colibri Tatillon'
566
+ }
567
+ chatbot_title = assistant_titles.get(assistant_type, 'Flowise Bot')
568
+
569
+ # Récupérer l'URL complète de l'API depuis la variable d'environnement
570
+ api_url_var = f"FLOWISE_API_URL_{assistant_type}"
571
+ api_url = os.getenv(api_url_var)
572
+ if not api_url:
573
+ st.error(f"URL de l'API non trouvée pour l'assistant {assistant_type}")
574
+ return
575
+
576
+ # Extraction de l'URL de base et du chatflowid
577
+ apiHost = '/'.join(api_url.split('/')[:3]) # Supprimer /prediction/ID
578
+ # Remplacer "flowise" par "localhost" dans apiHost
579
+ apiHost = apiHost.replace("flowise", "localhost")
580
+ chatflowid = api_url.split('/')[-1] # Récupérer l'ID
581
+
582
+ # Configuration du chatbot en HTML
583
+ st.components.v1.html(f'''
584
+ <script type="module">
585
+ import Chatbot from 'https://cdn.jsdelivr.net/npm/flowise-embed/dist/web.js';
586
+ Chatbot.init({{
587
+ chatflowid: '{chatflowid}',
588
+ apiHost: '{apiHost}',
589
+ chatflowConfig: {{
590
+ // topK: 2
591
+ }},
592
+ observersConfig: {{
593
+ // (optional) Allows you to execute code in parent based upon signal observations within the chatbot.
594
+ // The userinput field submitted to bot ("" when reset by bot)
595
+ observeUserInput: (userInput) => {{
596
+ console.log({{ userInput }});
597
+ }},
598
+ // The bot message stack has changed
599
+ observeMessages: (messages) => {{
600
+ console.log({{ messages }});
601
+ }},
602
+ // The bot loading signal changed
603
+ observeLoading: (loading) => {{
604
+ console.log({{ loading }});
605
+ }},
606
+ }},
607
+ theme: {{
608
+ button: {{
609
+ backgroundColor: '#3B81F6',
610
+ right: 20,
611
+ bottom: 20,
612
+ size: 48,
613
+ // small | medium | large | number
614
+ dragAndDrop: true,
615
+ iconColor: 'white',
616
+ customIconSrc: 'https://raw.githubusercontent.com/walkxcode/dashboard-icons/main/svg/google-messages.svg',
617
+ autoWindowOpen: {{
618
+ autoOpen: true,
619
+ //parameter to control automatic window opening
620
+ openDelay: 2,
621
+ // Optional parameter for delay time in seconds
622
+ autoOpenOnMobile: false,
623
+ //parameter to control automatic window opening in mobile
624
+ }},
625
+ }},
626
+ tooltip: {{
627
+ showTooltip: true,
628
+ tooltipMessage: 'Bonjour !',
629
+ tooltipBackgroundColor: 'black',
630
+ tooltipTextColor: 'white',
631
+ tooltipFontSize: 16,
632
+ }},
633
+ disclaimer: {{
634
+ title: 'Avertissement',
635
+ message: 'By using this chatbot, you agree to the <a target="_blank" href="https://ecg-pereire-assurances.com/mentions-legales/">Terms & Condition</a>',
636
+ }},
637
+ chatWindow: {{
638
+ showTitle: true,
639
+ showAgentMessages: true,
640
+ title: '{chatbot_title}',
641
+ titleAvatarSrc: 'https://raw.githubusercontent.com/walkxcode/dashboard-icons/main/svg/google-messages.svg',
642
+ welcomeMessage: 'Hello! This is custom welcome message',
643
+ errorMessage: 'This is a custom error message',
644
+ backgroundColor: '#ffffff',
645
+ backgroundImage: 'enter image path or link',
646
+ // If set, this will overlap the background color of the chat window.
647
+ height: 700,
648
+ width: 400,
649
+ fontSize: 16,
650
+ starterPrompts: ['What is a bot?', 'Who are you?'],
651
+ // It overrides the starter prompts set by the chat flow passed
652
+ starterPromptFontSize: 15,
653
+ clearChatOnReload: false,
654
+ // If set to true, the chat will be cleared when the page reloads
655
+ sourceDocsTitle: 'Sources:',
656
+ renderHTML: true,
657
+ botMessage: {{
658
+ backgroundColor: '#f7f8ff',
659
+ textColor: '#303235',
660
+ showAvatar: true,
661
+ avatarSrc: 'https://raw.githubusercontent.com/zahidkhawaja/langchain-chat-nextjs/main/public/parroticon.png',
662
+ }},
663
+ userMessage: {{
664
+ backgroundColor: '#3B81F6',
665
+ textColor: '#ffffff',
666
+ showAvatar: true,
667
+ avatarSrc: 'https://raw.githubusercontent.com/zahidkhawaja/langchain-chat-nextjs/main/public/usericon.png',
668
+ }},
669
+ textInput: {{
670
+ placeholder: 'Type your question',
671
+ backgroundColor: '#ffffff',
672
+ textColor: '#303235',
673
+ sendButtonColor: '#3B81F6',
674
+ //maxChars: 50,
675
+ maxCharsWarningMessage: 'You exceeded the characters limit. Please input less than 50 characters.',
676
+ autoFocus: true,
677
+ // If not used, autofocus is disabled on mobile and enabled on desktop. true enables it on both, false disables it on both.
678
+ sendMessageSound: true,
679
+ // sendSoundLocation: "send_message.mp3",
680
+ // If this is not used, the default sound effect will be played if sendSoundMessage is true.
681
+ receiveMessageSound: true,
682
+ // receiveSoundLocation: "receive_message.mp3",
683
+ // If this is not used, the default sound effect will be played if receiveSoundMessage is true.
684
+ }},
685
+ feedback: {{
686
+ color: '#303235',
687
+ }},
688
+ dateTimeToggle: {{
689
+ date: true,
690
+ time: true,
691
+ }},
692
+ footer: {{
693
+ textColor: '#303235',
694
+ text: 'Powered by',
695
+ company: 'Colibri',
696
+ companyLink: 'https://ecg-pereire-assurances.com',
697
+ }},
698
+ }},
699
+ }},
700
+ }});
701
+ </script>
702
+ ''', height=800)
703
+ add_flowise_chat()
704
+
705
+
706
+ with tab_chatreact:
707
+ def add_flowise_chatreact():
708
+ assistant_type = st.selectbox(
709
+ "Sélectionner l'assistant",
710
+ options=['SANTE', 'CAR', 'BTP', 'RH', 'PILOTAGE'],
711
+ format_func=lambda x: {
712
+ 'SANTE': 'Colibri Vitalité ❣️',
713
+ 'CAR': 'Colibri Carburant 🚘',
714
+ 'BTP': 'Colibri Batisseur 🏠',
715
+ 'RH': 'Colibri Equipage 🪪',
716
+ 'PILOTAGE': 'Colibri Tatillon 👨🏻‍✈️'
717
+ }[x],
718
+ key="chatbot_full_assistant_select"
719
+ )
720
+ # Définir le titre du chatbot en fonction de l'assistant sélectionné
721
+ assistant_titles = {
722
+ 'SANTE': 'Colibri Vitalité',
723
+ 'CAR': 'Colibri Carburant',
724
+ 'BTP': 'Colibri Batisseur',
725
+ 'RH': 'Colibri Equipage',
726
+ 'PILOTAGE': 'Colibri Tatillon'
727
+ }
728
+ chatbot_title = assistant_titles.get(assistant_type, 'Flowise Bot')
729
+ # Récupérer l'URL complète de l'API depuis la variable d'environnement
730
+ api_url_var = f"FLOWISE_API_URL_{assistant_type}"
731
+ api_url = os.getenv(api_url_var)
732
+ if not api_url:
733
+ st.error(f"URL de l'API non trouvée pour l'assistant {assistant_type}")
734
+ return
735
+ # Extraction de l'URL de base et du chatflowid
736
+ apiHost = '/'.join(api_url.split('/')[:3]) # Supprimer /prediction/ID
737
+ # Remplacer "flowise" par "localhost" dans apiHost
738
+ apiHost = apiHost.replace("flowise", "localhost")
739
+ chatflowid = api_url.split('/')[-1] # Récupérer l'ID
740
+
741
+ # Configuration du chatbot en HTML
742
+ st.components.v1.html(f'''
743
+ <flowise-fullchatbot></flowise-fullchatbot>
744
+ <body style="margin: 0">
745
+ <script type="module">
746
+ import Chatbot from "https://cdn.jsdelivr.net/npm/flowise-embed/dist/web.js";
747
+ Chatbot.initFull({{
748
+ chatflowid: "{chatflowid}",
749
+ apiHost: "{apiHost}",
750
+ theme: {{
751
+ chatWindow: {{
752
+ showTitle: true,
753
+ title: "{chatbot_title}",
754
+ titleAvatarSrc: 'https://raw.githubusercontent.com/walkxcode/dashboard-icons/main/svg/google-messages.svg',
755
+ showAgentMessages: true,
756
+ welcomeMessage: 'Hello! This is custom welcome message',
757
+ errorMessage: 'This is a custom error message',
758
+ backgroundColor: "#ffffff",
759
+ backgroundImage: '',
760
+ fontSize: 16,
761
+ starterPromptFontSize: 15,
762
+ clearChatOnReload: false,
763
+ botMessage: {{
764
+ backgroundColor: "#f7f8ff",
765
+ textColor: "#303235",
766
+ showAvatar: true,
767
+ avatarSrc: "https://raw.githubusercontent.com/zahidkhawaja/langchain-chat-nextjs/main/public/parroticon.png",
768
+ }},
769
+ userMessage: {{
770
+ backgroundColor: "#3B81F6",
771
+ textColor: "#ffffff",
772
+ showAvatar: true,
773
+ avatarSrc: "https://raw.githubusercontent.com/zahidkhawaja/langchain-chat-nextjs/main/public/usericon.png",
774
+ }},
775
+ textInput: {{
776
+ placeholder: 'Tapez votre question ici...',
777
+ backgroundColor: '#ffffff',
778
+ textColor: '#303235',
779
+ sendButtonColor: '#3B81F6',
780
+ autoFocus: true,
781
+ sendMessageSound: true,
782
+ receiveMessageSound: true,
783
+ }},
784
+ feedback: {{
785
+ color: '#303235',
786
+ }},
787
+ footer: {{
788
+ textColor: '#303235',
789
+ text: 'Powered by',
790
+ company: 'Colibri',
791
+ companyLink: 'https://ecg-pereire-assurances.com/',
792
+ }}
793
+ }}
794
+ }}
795
+ }});
796
+ </script>
797
+ ''', height=800)
798
+ add_flowise_chatreact()
799
+
800
+
801
+ with testfooter:
802
+ st.components.v1.html('''
803
+ <flowise-fullchatbot></flowise-fullchatbot>
804
+ <body style="margin: 0">
805
+ <script type="module">
806
+ import Chatbot from "https://cdn.jsdelivr.net/npm/flowise-embed/dist/web.js";
807
+ Chatbot.initFull({
808
+ chatflowid: "c7772c60-e798-4d02-809f-654170560e91",
809
+ apiHost: "http://localhost:3000",
810
+ theme: {
811
+ chatWindow: {
812
+ showTitle: true,
813
+ title: 'Flowise Bot',
814
+ titleAvatarSrc: 'https://raw.githubusercontent.com/walkxcode/dashboard-icons/main/svg/google-messages.svg',
815
+ showAgentMessages: true,
816
+ welcomeMessage: 'Hello! This is custom welcome message',
817
+ errorMessage: 'This is a custom error message',
818
+ backgroundColor: "#ffffff",
819
+ backgroundImage: '',
820
+ //height: 700,
821
+ //width: 400,
822
+ fontSize: 16,
823
+ starterPromptFontSize: 15,
824
+ clearChatOnReload: false,
825
+ botMessage: {
826
+ backgroundColor: "#f7f8ff",
827
+ textColor: "#303235",
828
+ showAvatar: true,
829
+ avatarSrc: "https://raw.githubusercontent.com/zahidkhawaja/langchain-chat-nextjs/main/public/parroticon.png",
830
+ },
831
+ userMessage: {
832
+ backgroundColor: "#3B81F6",
833
+ textColor: "#ffffff",
834
+ showAvatar: true,
835
+ avatarSrc: "https://raw.githubusercontent.com/zahidkhawaja/langchain-chat-nextjs/main/public/usericon.png",
836
+ },
837
+ textInput: {
838
+ placeholder: 'Tapez votre question ici...',
839
+ backgroundColor: '#ffffff',
840
+ textColor: '#303235',
841
+ sendButtonColor: '#3B81F6',
842
+ //maxChars: 50,
843
+ //maxCharsWarningMessage: 'You exceeded the characters limit. Please input less than 50 characters.',
844
+ autoFocus: true,
845
+ sendMessageSound: true,
846
+ receiveMessageSound: true,
847
+ },
848
+ feedback: {
849
+ color: '#303235',
850
+ },
851
+ footer: {
852
+ textColor: '#303235',
853
+ text: 'Powered by',
854
+ company: 'Clibri',
855
+ companyLink: 'https://ecg-pereire-assurances.com/',
856
+ }
857
+ }
858
+ }
859
+ });
860
+ </script>
861
+ ''', height=800)
862
+
863
+
864
+
865
+
866
+
867
+
868
+
869
+
870
+
871
+
872
+
873
+ # Exécution de la fonction principale
874
+ if __name__ == "__main__":
875
+ asyncio.run(render_admin_dashboard())
assets/logo.jfif ADDED
Binary file (8.13 kB). View file
 
assets/logo.png ADDED
assets/logo.svg ADDED
assets/logo2.svg ADDED
docker-compose.yml ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ services:
2
+ streamlit-app:
3
+ build: .
4
+ ports:
5
+ - "8501:8501"
6
+ environment:
7
+ - SUPABASE_URL=${SUPABASE_URL}
8
+ - SUPABASE_KEY=${SUPABASE_KEY}
9
+ - JWT_SECRET=${JWT_SECRET}
10
+ - FLOWISE_API_URL_SANTE=${FLOWISE_API_URL_SANTE}
11
+ - FLOWISE_API_URL_CAR=${FLOWISE_API_URL_CAR}
12
+ - FLOWISE_API_URL_BTP=${FLOWISE_API_URL_BTP}
13
+ - FLOWISE_API_URL_RH=${FLOWISE_API_URL_RH}
14
+ - FLOWISE_API_URL_PILOTAGE=${FLOWISE_API_URL_PILOTAGE}
15
+ - PYTHONUNBUFFERED=1 # Pour un meilleur logging
16
+ # partie concernant le reset du mot de passe
17
+ - EMAIL_HOST=${EMAIL_HOST}
18
+ - EMAIL_PORT=${EMAIL_PORT}
19
+ - EMAIL_HOST_USER=${EMAIL_HOST_USER}
20
+ - EMAIL_HOST_USER_PASSWORD=${EMAIL_HOST_USER_PASSWORD}
21
+ - DEFAULT_FROM_EMAIL=${DEFAULT_FROM_EMAIL}
22
+ - APP_URL=${APP_URL}
23
+ # Ajout de la configuration Redis
24
+ - REDIS_HOST=${REDIS_HOST}
25
+ - REDIS_PORT=${REDIS_PORT}
26
+ - QDRANT_URL=${QDRANT_URL}
27
+ extra_hosts:
28
+ - "host.docker.internal:host-gateway"
29
+ volumes:
30
+ - ./app.py:/app/app.py # Pour faciliter les modifications sans rebuilds
31
+ depends_on:
32
+ - redis
33
+ - flowise
34
+ - qdrant
35
+ networks:
36
+ - redis-network
37
+ - flowise-network
38
+ - qdrant-network
39
+ restart: always
40
+
41
+ redis:
42
+ image: redis:alpine
43
+ ports:
44
+ - "6379:6379"
45
+ volumes:
46
+ - redis_data:/data
47
+ networks:
48
+ - redis-network
49
+ restart: always
50
+
51
+ redisinsight:
52
+ image: redislabs/redisinsight:1.14.0
53
+ ports:
54
+ - "8001:8001"
55
+ volumes:
56
+ - redisinsight_data:/db
57
+ depends_on:
58
+ - redis
59
+ networks:
60
+ - redis-network
61
+ restart: always
62
+ pull_policy: always
63
+
64
+ flowise:
65
+ image: flowiseai/flowise:latest
66
+ ports:
67
+ - "3000:3000"
68
+ environment:
69
+ - PORT=3000
70
+ # backup dans dans postrgrs supabase
71
+ #- DATABASE_TYPE=${DATABASE_TYPE}
72
+ #- DATABASE_HOST=${DATABASE_HOST}
73
+ #- DATABASE_PORT=${DATABASE_PORT}
74
+ #- DATABASE_NAME=${DATABASE_NAME}
75
+ #- DATABASE_USER=${DATABASE_USER}
76
+ #- DATABASE_PASSWORD=${DATABASE_PASSWORD}
77
+ #- PGSSLMODE=${PGSSLMODE}
78
+ # autres variables
79
+ - DATABASE_PATH=/root/.flowise
80
+ - APIKEY_PATH=/root/.flowise
81
+ - SECRETKEY_PATH=/root/.flowise
82
+ - LOG_PATH=/root/.flowise/logs
83
+ - BLOB_STORAGE_PATH=/root/.flowise/storage
84
+ - ENCRYPTION_KEY_PATH=/root/.flowise/encryption.key
85
+ - FLOWISE_USERNAME=${FLOWISE_USERNAME}
86
+ - FLOWISE_PASSWORD=${FLOWISE_PASSWORD}
87
+ #- FLOWISE_AUTH_TOKEN=${FLOWISE_AUTH_TOKEN} # pas nécessaire pour l'instant
88
+
89
+ volumes:
90
+ - flowise_data:/root/.flowise
91
+ networks:
92
+ - flowise-network
93
+ restart: always
94
+ pull_policy: always
95
+
96
+ qdrant:
97
+ image: qdrant/qdrant:latest
98
+ ports:
99
+ - "6333:6333"
100
+ volumes:
101
+ - qdrant_data:/qdrant/storage
102
+ networks:
103
+ - flowise-network
104
+ restart: always
105
+ pull_policy: always
106
+
107
+ volumes:
108
+ redis_data:
109
+ redisinsight_data:
110
+ flowise_data:
111
+ qdrant_data:
112
+
113
+ networks:
114
+ redis-network:
115
+ name: redis-network
116
+ flowise-network:
117
+ name: flowise-network
118
+ qdrant-network:
119
+ name: qdrant-network
pages/Verify_email.py ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import sys
3
+ import os
4
+ from datetime import datetime
5
+ import pytz
6
+ import logging # Ajout de l'import logging
7
+ from utils.admin import supabase
8
+
9
+ # Configuration du logging
10
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
11
+
12
+ # IMPORTANT : set_page_config doit être la première commande Streamlit
13
+ st.set_page_config(
14
+ page_title="Vérification de l'email",
15
+ page_icon="✉️",
16
+ layout="centered"
17
+ )
18
+
19
+ # Cacher les éléments de navigation
20
+ hide_streamlit_style = """
21
+ <style>
22
+ #MainMenu {visibility: hidden;}
23
+ header {visibility: hidden;}
24
+ .css-1544g2n {display: none;}
25
+ section[data-testid="stSidebar"] {display: none;}
26
+ </style>
27
+ """
28
+ st.markdown(hide_streamlit_style, unsafe_allow_html=True)
29
+
30
+ def verify_email_token(token: str) -> tuple[bool, str]:
31
+ """Vérifie le token et active le compte"""
32
+ try:
33
+ # Vérifier le token
34
+ verification = supabase.table("email_verification").select("*").eq(
35
+ "verification_token", token
36
+ ).eq("is_verified", False).execute()
37
+
38
+ if not verification.data:
39
+ return False, "Token de vérification invalide ou déjà utilisé."
40
+
41
+ verification_data = verification.data[0]
42
+ expires_at = datetime.fromisoformat(verification_data['expires_at'].replace('Z', '+00:00'))
43
+
44
+ if expires_at < datetime.now(pytz.UTC):
45
+ return False, "Le lien de vérification a expiré."
46
+
47
+ # Activer l'utilisateur
48
+ supabase.table("users").update({
49
+ "is_active": True
50
+ }).eq("id", verification_data['user_id']).execute()
51
+
52
+ # Marquer comme vérifié
53
+ supabase.table("email_verification").update({
54
+ "is_verified": True
55
+ }).eq("id", verification_data['id']).execute()
56
+
57
+ return True, "Email vérifié avec succès!"
58
+
59
+ except Exception as e:
60
+ logging.error(f"Erreur lors de la vérification : {str(e)}")
61
+ return False, f"Erreur lors de la vérification : {str(e)}"
62
+
63
+ # Titre de la page
64
+ st.title("✉️ Vérification de l'email")
65
+
66
+ # Debug: Afficher le token reçu dans les logs
67
+ token = st.query_params.get("token", None)
68
+ logging.info(f"Token reçu : {token if token else 'Aucun token'}")
69
+
70
+ if not token:
71
+ st.error("Token de vérification manquant.")
72
+ st.stop()
73
+
74
+ # Vérifier le token et activer le compte
75
+ success, message = verify_email_token(token)
76
+ if success:
77
+ st.success(message)
78
+ st.info("Votre compte est maintenant actif. Vous pouvez vous connecter.")
79
+
80
+ # Bouton pour aller à la page de connexion
81
+ if st.button("Aller à la page de connexion"):
82
+ st.switch_page("app.py")
83
+ else:
84
+ st.error(message)
85
+ if "expiré" in message:
86
+ st.info("Veuillez demander un nouveau lien de vérification.")
pages/profile.py ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ from utils.admin import supabase
3
+ import bcrypt
4
+ from utils.notification_handler import notify_security_alert #import du module de notifications (envoie une notification de sécurité en cas de modifications des données de l"utilisateur)
5
+
6
+
7
+ # Configuration de la page
8
+ st.set_page_config(
9
+ page_title="Profil Utilisateur",
10
+ page_icon="👤",
11
+ layout="centered"
12
+ )
13
+
14
+ st.title("Mon Profil")
15
+
16
+ # Vérification de l'authentification
17
+ if 'user' not in st.session_state:
18
+ st.error("Vous devez être connecté pour accéder à cette page.")
19
+ st.stop()
20
+
21
+
22
+ # Récupérer les informations actuelles de l'utilisateur
23
+ user_id = st.session_state['user']['id']
24
+ user_info = supabase.table("users").select("*").eq("id", user_id).execute()
25
+ current_user = user_info.data[0] if user_info.data else None
26
+
27
+ # Section Informations Personnelles
28
+ st.header("📋 Informations Personnelles")
29
+ with st.form("personal_info_form"):
30
+ col1, col2 = st.columns(2)
31
+ with col1:
32
+ new_nom = st.text_input("Nom", value=current_user.get('nom', ''))
33
+ with col2:
34
+ new_prenom = st.text_input("Prénom", value=current_user.get('prenom', ''))
35
+
36
+ # Informations professionnelles
37
+ st.subheader("Informations professionnelles")
38
+ professional_info = current_user.get('professional_info', {})
39
+ if isinstance(professional_info, str):
40
+ import json
41
+ professional_info = json.loads(professional_info)
42
+
43
+ new_profession = st.text_input(
44
+ "Profession",
45
+ value=professional_info.get('profession', '')
46
+ )
47
+ new_entreprise = st.text_input(
48
+ "Entreprise",
49
+ value=professional_info.get('entreprise', '')
50
+ )
51
+
52
+ submit_personal = st.form_submit_button("Mettre à jour mes informations")
53
+
54
+ if submit_personal:
55
+ try:
56
+ # Préparer les données à mettre à jour
57
+ update_data = {
58
+ "nom": new_nom,
59
+ "prenom": new_prenom,
60
+ "professional_info": {
61
+ "profession": new_profession,
62
+ "entreprise": new_entreprise
63
+ }
64
+ }
65
+
66
+ # Mettre à jour dans la base de données
67
+ supabase.table("users").update(update_data).eq("id", user_id).execute()
68
+ st.success("✅ Informations personnelles mises à jour avec succès!")
69
+ st.rerun() # Recharger la page pour afficher les nouvelles informations
70
+ # Notifier l'utilisateur
71
+ except Exception as e:
72
+ st.error(f"Une erreur est survenue : {str(e)}")
73
+ notify_security_alert(user_id, # notifier l'utilisateur du changement de ses impofrmations personnelles
74
+ "Vos informations personnelles ont été mises à jour."
75
+ )
76
+
77
+
78
+ # Section Email et Mot de passe
79
+ st.header("🔐 Email et Mot de passe")
80
+ with st.form("security_form"):
81
+ # Email
82
+ new_email = st.text_input("Email", value=current_user.get('email', ''))
83
+
84
+ # Mot de passe
85
+ st.subheader("Changer le mot de passe")
86
+ current_password = st.text_input("Mot de passe actuel", type="password")
87
+ new_password = st.text_input("Nouveau mot de passe", type="password")
88
+ confirm_password = st.text_input("Confirmer le nouveau mot de passe", type="password")
89
+
90
+ submit_security = st.form_submit_button("Mettre à jour les informations de sécurité")
91
+
92
+ if submit_security:
93
+ try:
94
+ update_data = {}
95
+
96
+ # Vérifier si l'email a changé
97
+ if new_email != current_user.get('email'):
98
+ # Vérifier si le nouvel email existe déjà
99
+ existing_email = supabase.table("users").select("id").eq("email", new_email).neq("id", user_id).execute()
100
+ if existing_email.data:
101
+ st.error("Cet email est déjà utilisé par un autre utilisateur.")
102
+ else:
103
+ update_data["email"] = new_email
104
+
105
+ # Vérifier si le mot de passe doit être mis à jour
106
+ if current_password and new_password and confirm_password:
107
+ # Vérifier l'ancien mot de passe
108
+ if not bcrypt.checkpw(current_password.encode(), current_user['password'].encode()):
109
+ st.error("Le mot de passe actuel est incorrect.")
110
+ elif new_password != confirm_password:
111
+ st.error("Les nouveaux mots de passe ne correspondent pas.")
112
+ elif len(new_password) < 8:
113
+ st.error("Le nouveau mot de passe doit contenir au moins 8 caractères.")
114
+ else:
115
+ # Hasher le nouveau mot de passe
116
+ hashed_password = bcrypt.hashpw(new_password.encode(), bcrypt.gensalt()).decode()
117
+ update_data["password"] = hashed_password
118
+
119
+ # Effectuer la mise à jour si nécessaire
120
+ if update_data:
121
+ supabase.table("users").update(update_data).eq("id", user_id).execute()
122
+ st.success("✅ Informations de sécurité mises à jour avec succès!")
123
+ if "email" in update_data:
124
+ st.warning("⚠️ Votre email a été modifié. Vous devrez vous reconnecter.")
125
+ st.session_state.clear()
126
+ st.rerun()
127
+ else:
128
+ st.info("Aucune modification à effectuer.")
129
+
130
+ except Exception as e:
131
+ st.error(f"Une erreur est survenue : {str(e)}")
132
+
133
+
134
+
135
+ # Section Préférences de Notification
136
+ st.header("🔔 Préférences de Notification")
137
+
138
+ # Récupérer les préférences actuelles
139
+ notification_prefs = supabase.table("notification_preferences").select("*").eq("user_id", user_id).execute()
140
+ current_prefs = notification_prefs.data[0] if notification_prefs.data else {
141
+ "email_notifications": True,
142
+ "security_alerts": True,
143
+ "assistant_updates": True,
144
+ "daily_summary": False
145
+ }
146
+
147
+ with st.form("notification_preferences_form"):
148
+ st.markdown("Choisissez les notifications que vous souhaitez recevoir :")
149
+
150
+ email_notif = st.toggle(
151
+ "Notifications par email",
152
+ value=current_prefs.get("email_notifications", True),
153
+ help="Recevoir les notifications par email"
154
+ )
155
+
156
+ security_alerts = st.toggle(
157
+ "Alertes de sécurité",
158
+ value=current_prefs.get("security_alerts", True),
159
+ help="Notifications concernant la sécurité de votre compte"
160
+ )
161
+
162
+ assistant_updates = st.toggle(
163
+ "Mises à jour des assistants",
164
+ value=current_prefs.get("assistant_updates", True),
165
+ help="Notifications sur les nouvelles fonctionnalités des assistants"
166
+ )
167
+
168
+ daily_summary = st.toggle(
169
+ "Résumé quotidien",
170
+ value=current_prefs.get("daily_summary", False),
171
+ help="Recevoir un résumé quotidien de vos interactions avec les assistants"
172
+ )
173
+
174
+ submit_notif = st.form_submit_button("Enregistrer mes préférences")
175
+
176
+ if submit_notif:
177
+ try:
178
+ # Préparer les données
179
+ notif_data = {
180
+ "user_id": user_id,
181
+ "email_notifications": email_notif,
182
+ "security_alerts": security_alerts,
183
+ "assistant_updates": assistant_updates,
184
+ "daily_summary": daily_summary,
185
+ "updated_at": "NOW()"
186
+ }
187
+
188
+ # Vérifier si des préférences existent déjà
189
+ if notification_prefs.data:
190
+ # Mettre à jour les préférences existantes
191
+ supabase.table("notification_preferences").update(notif_data).eq("user_id", user_id).execute()
192
+ else:
193
+ # Créer de nouvelles préférences
194
+ notif_data["created_at"] = "NOW()"
195
+ supabase.table("notification_preferences").insert(notif_data).execute()
196
+
197
+ st.success("✅ Préférences de notification mises à jour avec succès!")
198
+ except Exception as e:
199
+ st.error(f"Une erreur est survenue : {str(e)}")
200
+
201
+
202
+
203
+ # Maintenant que nous avons implémenté le système de préférences de notification dans le fichoer actuel, nous ajoutons la fonctionnalité pour envoyer effectivement ces notifications aux utilisateurs.
204
+ # Nous créyons un nouveau module pour gérer l'envoi des notifications.
205
+ # Voici les étapes :
206
+ # Créez un nouveau fichier utils/notification_handler.py qui gérera l'envoi des notifications.
207
+
208
+
209
+
210
+ # tester les notification (fonctions temporaires à supprimer en prod)
211
+ st.header("🧪 Test des Notifications")
212
+
213
+ test_col1, test_col2 = st.columns(2)
214
+
215
+ with test_col1:
216
+ with st.form("test_security_notification"):
217
+ st.subheader("Test Alerte de Sécurité")
218
+ test_security_msg = st.text_input(
219
+ "Message de test (sécurité)",
220
+ value="Ceci est un test d'alerte de sécurité"
221
+ )
222
+ if st.form_submit_button("Tester Alerte Sécurité"):
223
+ from utils.notification_handler import notify_security_alert
224
+ if notify_security_alert(user_id, test_security_msg):
225
+ st.success("✅ Notification de sécurité envoyée!")
226
+ else:
227
+ st.error("❌ Erreur lors de l'envoi de la notification")
228
+
229
+ with test_col2:
230
+ with st.form("test_assistant_notification"):
231
+ st.subheader("Test Mise à jour Assistant")
232
+ test_assistant_msg = st.text_input(
233
+ "Message de test (assistant)",
234
+ value="Ceci est un test de mise à jour assistant"
235
+ )
236
+ if st.form_submit_button("Tester Notification Assistant"):
237
+ from utils.notification_handler import notify_assistant_update
238
+ if notify_assistant_update(user_id, test_assistant_msg):
239
+ st.success("✅ Notification assistant envoyée!")
240
+ else:
241
+ st.error("❌ Erreur lors de l'envoi de la notification")
242
+
243
+ # Test du résumé quotidien
244
+ with st.form("test_daily_summary"):
245
+ st.subheader("Test Résumé Quotidien")
246
+ test_summary = {
247
+ "conversations": 5,
248
+ "questions": 15,
249
+ "usage_time": 45
250
+ }
251
+
252
+ st.write("Données de test :")
253
+ st.json(test_summary)
254
+
255
+ if st.form_submit_button("Tester Résumé Quotidien"):
256
+ from utils.notification_handler import send_daily_summary
257
+ if send_daily_summary(user_id, test_summary):
258
+ st.success("✅ Résumé quotidien envoyé!")
259
+ else:
260
+ st.error("❌ Erreur lors de l'envoi du résumé")
pages/reset_password.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import sys
3
+ import os
4
+ import time
5
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
6
+ from utils.password_reset import verify_reset_token, reset_password
7
+
8
+ # Configuration de la page pour cacher complètement la sidebar
9
+ st.set_page_config(
10
+ page_title="Réinitialisation du mot de passe",
11
+ page_icon="🔒",
12
+ layout="centered",
13
+ initial_sidebar_state="collapsed"
14
+ )
15
+
16
+ # Cacher tous les éléments de la sidebar et le menu hamburger
17
+ hide_streamlit_style = """
18
+ <style>
19
+ #MainMenu {visibility: hidden;}
20
+ header {visibility: hidden;}
21
+ .css-1544g2n {display: none;}
22
+ .css-14xtw13 e8zbici0 {display: none;}
23
+ section[data-testid="stSidebar"] {display: none;}
24
+ </style>
25
+ """
26
+ st.markdown(hide_streamlit_style, unsafe_allow_html=True)
27
+
28
+ # Titre de la page
29
+ st.title("Réinitialisation du mot de passe")
30
+
31
+ # Récupérer le token depuis l'URL
32
+ token = st.query_params.get("token", None)
33
+
34
+ if not token:
35
+ st.error("Token de réinitialisation manquant.")
36
+ st.stop()
37
+
38
+ # Vérifier la validité du token
39
+ valid, user_id = verify_reset_token(token)
40
+
41
+ if not valid:
42
+ st.error("Le lien de réinitialisation est invalide ou a expiré.")
43
+ time.sleep(3) # Attendre 3 secondes
44
+ st.switch_page("app.py") # Rediriger vers la page de connexion
45
+ st.stop()
46
+
47
+ # Formulaire de nouveau mot de passe
48
+ with st.form("reset_password_form"):
49
+ new_password = st.text_input("Nouveau mot de passe", type="password")
50
+ confirm_password = st.text_input("Confirmez le mot de passe", type="password")
51
+
52
+ submitted = st.form_submit_button("Réinitialiser le mot de passe")
53
+
54
+ if submitted:
55
+ if new_password != confirm_password:
56
+ st.error("Les mots de passe ne correspondent pas.")
57
+ elif len(new_password) < 8:
58
+ st.error("Le mot de passe doit contenir au moins 8 caractères.")
59
+ else:
60
+ success, message = reset_password(token, new_password)
61
+ if success:
62
+ st.success("Votre mot de passe a été mis à jour avec succès.")
63
+ st.info("Redirection vers la page de connexion...")
64
+ time.sleep(3) # Attendre 3 secondes
65
+ st.switch_page("app.py") # Rediriger vers la page de connexion
66
+ else:
67
+ st.error(message)
utils/admin.py ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, List, Optional, Tuple
2
+ from datetime import datetime
3
+ import pytz
4
+ from supabase import Client, create_client
5
+ import os
6
+ from dotenv import load_dotenv
7
+ 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)
8
+ from utils.logging_utils import log_to_file # logging définis et importés depuis le fichier dédié: logging_utils.py
9
+
10
+
11
+ """
12
+ le fichier utils/admin.py gère :
13
+ La gestion des utilisateurs
14
+ Les permissions des assistants
15
+ Les statistiques utilisateurs
16
+ Les mises à jour des informations utilisateur
17
+
18
+ Pour tester les modifications après, nous pourrons :
19
+ Nous connecter en tant qu'administrateur
20
+ Effectuer des opérations dans le dashboard admin (modifier un utilisateur, changer des permissions, etc.)
21
+ """
22
+
23
+
24
+ # Charger les variables d'environnement
25
+ load_dotenv()
26
+ supabase: Client = create_client(os.getenv("SUPABASE_URL"), os.getenv("SUPABASE_KEY"))
27
+
28
+
29
+ def get_all_users() -> List[Dict]:
30
+ """
31
+ Récupère la liste de tous les utilisateurs (fonction admin).
32
+
33
+ Returns:
34
+ List[Dict]: Liste des utilisateurs avec leurs informations
35
+ """
36
+ try:
37
+ response = supabase.table("users").select("*").execute()
38
+ return response.data if response.data else []
39
+ except Exception as e:
40
+ log_to_file(f"Erreur lors de la récupération des utilisateurs : {str(e)}", level=logging.ERROR)
41
+ return []
42
+
43
+ def toggle_user_status(user_id: int, is_active: bool) -> Tuple[bool, str]:
44
+ """
45
+ Active ou désactive un compte utilisateur (fonction admin).
46
+
47
+ Args:
48
+ user_id (int): ID de l'utilisateur
49
+ is_active (bool): Nouvel état du compte
50
+
51
+ Returns:
52
+ Tuple[bool, str]: (Succès, Message)
53
+ """
54
+ try:
55
+ supabase.table("users").update({"is_active": is_active}).eq("id", user_id).execute()
56
+ status = "activé" if is_active else "désactivé"
57
+ return True, f"Compte utilisateur {status} avec succès"
58
+ except Exception as e:
59
+ log_to_file(f"Erreur lors de la modification du statut utilisateur : {str(e)}", level=logging.ERROR)
60
+ return False, f"Erreur lors de la modification du statut : {str(e)}"
61
+
62
+ def change_user_role(user_id: int, new_role: str) -> Tuple[bool, str]:
63
+ """
64
+ Modifie le rôle d'un utilisateur (fonction admin).
65
+
66
+ Args:
67
+ user_id (int): ID de l'utilisateur
68
+ new_role (str): Nouveau rôle ('admin' ou 'user')
69
+
70
+ Returns:
71
+ Tuple[bool, str]: (Succès, Message)
72
+ """
73
+ if new_role not in ['admin', 'user']:
74
+ return False, "Rôle invalide"
75
+
76
+ try:
77
+ supabase.table("users").update({"role": new_role}).eq("id", user_id).execute()
78
+ return True, f"Rôle modifié avec succès en {new_role}"
79
+ except Exception as e:
80
+ log_to_file(f"Erreur lors de la modification du rôle : {str(e)}", level=logging.ERROR)
81
+ return False, f"Erreur lors de la modification du rôle : {str(e)}"
82
+
83
+ def get_user_stats() -> Dict:
84
+ """
85
+ Récupère des statistiques sur les utilisateurs (fonction admin).
86
+
87
+ Returns:
88
+ Dict: Statistiques des utilisateurs
89
+ """
90
+ try:
91
+ total = supabase.table("users").select("count", count="exact").execute()
92
+ active = supabase.table("users").select("count", count="exact").eq("is_active", True).execute()
93
+ admins = supabase.table("users").select("count", count="exact").eq("role", "admin").execute()
94
+
95
+ return {
96
+ "total_users": total.count if hasattr(total, 'count') else 0,
97
+ "active_users": active.count if hasattr(active, 'count') else 0,
98
+ "admin_users": admins.count if hasattr(admins, 'count') else 0,
99
+ "last_updated": datetime.now(pytz.UTC).isoformat()
100
+ }
101
+ except Exception as e:
102
+ log_to_file(f"Erreur lors de la récupération des statistiques : {str(e)}", level=logging.ERROR)
103
+ return {
104
+ "total_users": 0,
105
+ "active_users": 0,
106
+ "admin_users": 0,
107
+ "error": str(e)
108
+ }
109
+
110
+ def update_user_info(user_id: int, updates: Dict) -> Tuple[bool, str]:
111
+ """
112
+ Met à jour les informations d'un utilisateur (fonction admin).
113
+
114
+ Args:
115
+ user_id (int): ID de l'utilisateur
116
+ updates (Dict): Dictionnaire contenant les champs à mettre à jour
117
+
118
+ Returns:
119
+ Tuple[bool, str]: (Succès, Message)
120
+ """
121
+ allowed_fields = {'nom', 'prenom', 'email', 'professional_info', 'is_active', 'role'}
122
+ update_data = {k: v for k, v in updates.items() if k in allowed_fields}
123
+
124
+ if not update_data:
125
+ return False, "Aucun champ valide à mettre à jour"
126
+
127
+ try:
128
+ supabase.table("users").update(update_data).eq("id", user_id).execute()
129
+ return True, "Informations utilisateur mises à jour avec succès"
130
+ except Exception as e:
131
+ log_to_file(f"Erreur lors de la mise à jour des informations utilisateur : {str(e)}", level=logging.ERROR)
132
+ return False, f"Erreur lors de la mise à jour : {str(e)}"
133
+
134
+ def get_user_permissions(user_id: int) -> Dict[str, bool]:
135
+ """
136
+ Récupère les autorisations d'accès aux assistants pour un utilisateur.
137
+ """
138
+ try:
139
+ response = supabase.table("user_assistant_permissions").select(
140
+ "assistant_type", "is_authorized"
141
+ ).eq("user_id", user_id).execute()
142
+
143
+ # Convertir la réponse en dictionnaire
144
+ permissions = {
145
+ 'insuranceSANTE': False,
146
+ 'insuranceCAR': False,
147
+ 'insuranceBTP': False,
148
+ 'RH': False,
149
+ 'Pilotage': False
150
+ }
151
+ if response.data:
152
+ for perm in response.data:
153
+ permissions[perm['assistant_type']] = perm['is_authorized']
154
+ return permissions
155
+ except Exception as e:
156
+ log_to_file(f"Erreur lors de la récupération des permissions : {str(e)}", level=logging.ERROR)
157
+ return {
158
+ 'insuranceSANTE': False,
159
+ 'insuranceCAR': False,
160
+ 'insuranceBTP': False,
161
+ 'RH': False,
162
+ 'Pilotage': False
163
+ }
164
+
165
+
166
+ def update_user_permissions(user_id: int, assistant_type: str, is_authorized: bool) -> Tuple[bool, str]:
167
+ """
168
+ Met à jour les autorisations d'accès d'un utilisateur pour un assistant spécifique.
169
+ """
170
+ try:
171
+ # Convertir user_id en int standard Python
172
+ user_id = int(user_id) # Ajoutez cette ligne
173
+
174
+ # Vérifier si une permission existe déjà
175
+ existing = supabase.table("user_assistant_permissions").select("*").eq(
176
+ "user_id", user_id
177
+ ).eq("assistant_type", assistant_type).execute()
178
+
179
+ if existing.data:
180
+ # Mettre à jour la permission existante
181
+ supabase.table("user_assistant_permissions").update({
182
+ "is_authorized": is_authorized,
183
+ "modified_at": datetime.now(pytz.UTC).isoformat()
184
+ }).eq("user_id", user_id).eq("assistant_type", assistant_type).execute()
185
+ else:
186
+ # Créer une nouvelle permission
187
+ supabase.table("user_assistant_permissions").insert({
188
+ "user_id": user_id,
189
+ "assistant_type": assistant_type,
190
+ "is_authorized": is_authorized
191
+ }).execute()
192
+
193
+ return True, f"Autorisation {'accordée' if is_authorized else 'retirée'} avec succès"
194
+ except Exception as e:
195
+ log_to_file(f"Erreur lors de la mise à jour des permissions : {str(e)}", level=logging.ERROR)
196
+ return False, str(e)
197
+
198
+
199
+ def initialize_user_permissions(user_id: int) -> bool:
200
+ """
201
+ Initialise les permissions par défaut pour un nouvel utilisateur.
202
+ """
203
+ try:
204
+ default_permissions = [
205
+ {"user_id": user_id, "assistant_type": "insuranceSANTE", "is_authorized": False},
206
+ {"user_id": user_id, "assistant_type": "insuranceCAR", "is_authorized": False},
207
+ {"user_id": user_id, "assistant_type": "insuranceBTP", "is_authorized": False},
208
+ {"user_id": user_id, "assistant_type": "Pilotage", "is_authorized": False},
209
+ {"user_id": user_id, "assistant_type": "RH", "is_authorized": False},
210
+ ]
211
+
212
+ supabase.table("user_assistant_permissions").insert(default_permissions).execute()
213
+ return True
214
+ except Exception as e:
215
+ log_to_file(f"Erreur lors de l'initialisation des permissions : {str(e)}", level=logging.ERROR)
216
+ return False
utils/async_flowise.py ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import aiohttp
3
+ import logging
4
+ from typing import Dict, Optional, Any, AsyncGenerator
5
+ import json
6
+ from datetime import datetime, timedelta
7
+ import pytz
8
+ from utils.logging_utils import log_to_file
9
+ import os
10
+ import hashlib
11
+ from dotenv import load_dotenv
12
+ from utils.rate_limiter import RateLimiter
13
+ from utils.circuit_breaker import AsyncCircuitBreaker
14
+ from utils.cache_manager import CacheManager
15
+
16
+ FLOWISE_API_URL_SANTE = os.getenv("FLOWISE_API_URL_SANTE")
17
+ FLOWISE_API_URL_CAR = os.getenv("FLOWISE_API_URL_CAR")
18
+ FLOWISE_API_URL_BTP = os.getenv("FLOWISE_API_URL_BTP")
19
+ FLOWISE_API_URL_RH = os.getenv("FLOWISE_API_URL_RH")
20
+ FLOWISE_API_URL_PILOTAGE = os.getenv("FLOWISE_API_URL_PILOTAGE")
21
+
22
+ class AsyncFlowiseClient:
23
+ def __init__(self):
24
+ """Initialise le client asynchrone pour Flowise"""
25
+ self.api_urls = {
26
+ 'insuranceSANTE': FLOWISE_API_URL_SANTE,
27
+ 'insuranceCAR': FLOWISE_API_URL_CAR,
28
+ 'insuranceBTP': FLOWISE_API_URL_BTP,
29
+ 'RH': FLOWISE_API_URL_RH,
30
+ 'Pilotage': FLOWISE_API_URL_PILOTAGE
31
+ }
32
+ self.session = None
33
+ self.rate_limiter = RateLimiter(requests_per_second=5, burst_limit=10)
34
+ self.circuit_breaker = AsyncCircuitBreaker(
35
+ failure_threshold=5,
36
+ recovery_timeout=60,
37
+ half_open_timeout=30
38
+ )
39
+ self.cache_manager = CacheManager()
40
+
41
+ def _generate_cache_key(self, question: str, assistant_type: str) -> str:
42
+ """Génère une clé de cache unique pour une question"""
43
+ question_hash = hashlib.md5(question.encode()).hexdigest()
44
+ return f"assistant_response:{assistant_type}:{question_hash}"
45
+
46
+ def _clean_question(self, question: str) -> str:
47
+ """Nettoie la question des métadonnées"""
48
+ try:
49
+ data = json.loads(question)
50
+ if isinstance(data, dict) and "next_inputs" in data and "query" in data["next_inputs"]:
51
+ return data["next_inputs"]["query"]
52
+ except json.JSONDecodeError:
53
+ pass
54
+ return question
55
+
56
+ def _process_response(self, response_data: Any) -> Dict[str, str]:
57
+ """Extrait uniquement le texte de la réponse"""
58
+ try:
59
+ # Si c'est une chaîne, essayer de la parser comme JSON
60
+ if isinstance(response_data, str):
61
+ try:
62
+ data = json.loads(response_data)
63
+ if isinstance(data, dict) and "text" in data:
64
+ return {"answer": data["text"]}
65
+ except json.JSONDecodeError:
66
+ return {"answer": response_data}
67
+
68
+ # Si c'est déjà un dictionnaire
69
+ if isinstance(response_data, dict) and "text" in response_data:
70
+ return {"answer": response_data["text"]}
71
+
72
+ # Si on ne peut pas extraire le texte, retourner une erreur
73
+ return {"error": "Format de réponse non reconnu"}
74
+
75
+ except Exception as e:
76
+ log_to_file(f"Erreur lors du traitement de la réponse : {str(e)}", level=logging.ERROR)
77
+ return {"error": "Format de réponse non reconnu"}
78
+
79
+ async def query_assistant_stream(self, question: str, assistant_type: str, user_id: Optional[str] = None) -> AsyncGenerator[str, None]:
80
+ """Envoie une requête en streaming à Flowise"""
81
+ if assistant_type not in self.api_urls:
82
+ log_to_file(f"Type d'assistant non valide : {assistant_type}", level=logging.ERROR)
83
+ yield json.dumps({"error": f"Type d'assistant non valide : {assistant_type}"})
84
+ return
85
+
86
+ if not await self.rate_limiter.acquire(user_id):
87
+ yield json.dumps({"error": "Trop de requêtes. Veuillez réessayer dans quelques instants."})
88
+ return
89
+
90
+ if not self.session:
91
+ self.session = aiohttp.ClientSession()
92
+
93
+ try:
94
+ url = self.api_urls[assistant_type]
95
+ clean_question = self._clean_question(question)
96
+ payload = {"question": clean_question}
97
+
98
+ async with self.session.post(url, json=payload) as response:
99
+ if response.status != 200:
100
+ error_text = await response.text()
101
+ log_to_file(f"Erreur lors de la requête à {assistant_type}: {error_text}", level=logging.ERROR)
102
+ yield json.dumps({"error": f"Erreur {response.status}: {error_text}"})
103
+ return
104
+
105
+ async for chunk in response.content.iter_any():
106
+ if chunk:
107
+ try:
108
+ text = chunk.decode('utf-8')
109
+ processed_response = self._process_response(text)
110
+ if "answer" in processed_response:
111
+ yield processed_response["answer"]
112
+ except Exception as e:
113
+ log_to_file(f"Erreur de décodage du chunk : {str(e)}", level=logging.ERROR)
114
+ continue
115
+
116
+ except Exception as e:
117
+ error_message = str(e)
118
+ log_to_file(f"Erreur lors du streaming pour {assistant_type}: {error_message}", level=logging.ERROR)
119
+ yield json.dumps({"error": error_message})
120
+
121
+ async def query_assistant(self, question: str, assistant_type: str, user_id: Optional[str] = None) -> Dict[str, Any]:
122
+ """Envoie une requête asynchrone à Flowise avec cache"""
123
+ if assistant_type not in self.api_urls:
124
+ log_to_file(f"Type d'assistant non valide : {assistant_type}", level=logging.ERROR)
125
+ return {"error": f"Type d'assistant non valide : {assistant_type}"}
126
+
127
+ if not await self.rate_limiter.acquire(user_id):
128
+ return {"error": "Trop de requêtes. Veuillez réessayer dans quelques instants."}
129
+
130
+ cache_key = self._generate_cache_key(question, assistant_type)
131
+ cached_response = await self.cache_manager.get(cache_key)
132
+
133
+ if cached_response:
134
+ log_to_file(f"Réponse trouvée dans le cache pour {assistant_type}", level=logging.INFO)
135
+ return cached_response
136
+
137
+ if not self.session:
138
+ self.session = aiohttp.ClientSession()
139
+
140
+ try:
141
+ url = self.api_urls[assistant_type]
142
+ clean_question = self._clean_question(question)
143
+ payload = {"question": clean_question}
144
+
145
+ async with self.session.post(url, json=payload) as response:
146
+ if response.status != 200:
147
+ error_text = await response.text()
148
+ log_to_file(f"Erreur lors de la requête à {assistant_type}: {error_text}", level=logging.ERROR)
149
+ return {"error": f"Erreur {response.status}: {error_text}"}
150
+
151
+ response_text = await response.text()
152
+ processed_response = self._process_response(response_text)
153
+
154
+ if "error" not in processed_response:
155
+ await self.cache_manager.set(
156
+ cache_key,
157
+ processed_response,
158
+ expiry=timedelta(hours=1)
159
+ )
160
+ log_to_file(f"Réponse mise en cache pour {assistant_type}", level=logging.INFO)
161
+
162
+ return processed_response
163
+
164
+ except Exception as e:
165
+ error_message = str(e)
166
+ if "Circuit breaker ouvert" in error_message:
167
+ log_to_file(f"Service {assistant_type} temporairement indisponible (Circuit breaker)", level=logging.ERROR)
168
+ return {"error": "Service temporairement indisponible. Veuillez réessayer plus tard."}
169
+
170
+ log_to_file(f"Erreur lors de la requête à {assistant_type}: {error_message}", level=logging.ERROR)
171
+ return {"error": error_message}
172
+
173
+ async def check_health(self) -> Dict[str, bool]:
174
+ """Vérifie la santé des connexions aux différents assistants"""
175
+ if not self.session:
176
+ self.session = aiohttp.ClientSession()
177
+
178
+ results = {}
179
+ for assistant_type, url in self.api_urls.items():
180
+ try:
181
+ base_url = '/'.join(url.split('/')[:-4]) # Obtenir l'URL de base
182
+ async with self.session.get(
183
+ f"{base_url}/health",
184
+ timeout=aiohttp.ClientTimeout(total=5)
185
+ ) as response:
186
+ results[assistant_type] = response.status == 200
187
+ log_to_file(f"Statut santé {assistant_type}: {response.status}", level=logging.INFO)
188
+ except Exception as e:
189
+ log_to_file(f"Erreur de santé pour {assistant_type}: {str(e)}", level=logging.ERROR)
190
+ results[assistant_type] = False
191
+
192
+ return results
193
+
194
+ async def close(self):
195
+ """Ferme la session client"""
196
+ if self.session:
197
+ await self.session.close()
198
+ self.session = None
utils/auth_cache.py ADDED
@@ -0,0 +1,277 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from datetime import datetime, timedelta
3
+ import pytz
4
+ from typing import Optional, List, Dict, Any
5
+ import logging
6
+ from utils.logging_utils import log_to_file
7
+ from utils.cache_manager import CacheManager
8
+ from utils.admin import supabase
9
+
10
+ class AuthenticationCache:
11
+ def __init__(self):
12
+ self.cache_manager = CacheManager() # initialise la connexion Redis
13
+ self.cache_duration = timedelta(hours=24) # Duree de validité du cache (24h)
14
+ self.cache_version_key = "auth_cache_version" # Pour suivre la version du cache
15
+ self.batch_size = 2 # Taille du batch pour le chargement par lots
16
+ self.session_cache_key = "user_session:"
17
+ self.history_cache_key = "chat_history:" # Ajout de cet attribut
18
+
19
+ async def is_cache_initialized(self) -> bool:
20
+ """Vérifie si le cache est déjà initialisé"""
21
+ try:
22
+ all_keys = self.cache_manager.redis_client.keys("auth_user:*")
23
+ return len(all_keys) > 0
24
+ except Exception as e:
25
+ log_to_file(f"Erreur lors de la vérification du cache : {str(e)}", level=logging.ERROR)
26
+ return False
27
+
28
+ async def get_cache_version(self) -> str:
29
+ """Récupère la version actuelle du cache"""
30
+ try:
31
+ return await self.cache_manager.get(self.cache_version_key) or ""
32
+ except Exception as e:
33
+ log_to_file(f"Erreur lors de la récupération de la version du cache : {str(e)}", level=logging.ERROR)
34
+ return ""
35
+
36
+ async def update_cache_version(self) -> None:
37
+ """Met à jour la version du cache"""
38
+ try:
39
+ current_time = datetime.now(pytz.UTC).isoformat()
40
+ await self.cache_manager.set(self.cache_version_key, current_time, expiry=self.cache_duration)
41
+ except Exception as e:
42
+ log_to_file(f"Erreur lors de la mise à jour de la version du cache : {str(e)}", level=logging.ERROR)
43
+
44
+ async def is_cache_outdated(self) -> bool:
45
+ """Vérifie si le cache doit être mis à jour"""
46
+ try:
47
+ # Vérifier la dernière mise à jour du cache
48
+ cache_version = await self.get_cache_version()
49
+ if not cache_version:
50
+ return True
51
+
52
+ # Vérifier les modifications récentes dans Supabase
53
+ last_update = datetime.fromisoformat(cache_version)
54
+ recent_changes = supabase.table("users").select("updated_at").gt(
55
+ "updated_at", last_update.isoformat()
56
+ ).execute()
57
+
58
+ return bool(recent_changes.data)
59
+ except Exception as e:
60
+ log_to_file(f"Erreur lors de la vérification de l'état du cache : {str(e)}", level=logging.ERROR)
61
+ return True
62
+
63
+ async def get_user_from_cache(self, email: str) -> Optional[Dict[str, Any]]:
64
+ """Récupère les informations utilisateur depuis le cache.
65
+ Args:
66
+ email: L'adresse email de l'utilisateur à rechercher dans le cache.
67
+
68
+ Returns:
69
+ Un dictionnaire contenant les informations de l'utilisateur si trouvé dans le cache, None sinon.
70
+ """
71
+ try:
72
+ # Construit la clé de cache en utilisant l'email.
73
+ cache_key = f"auth_user:{email}"
74
+ # Récupère l'utilisateur depuis le cache en utilisant la clé.
75
+ return await self.cache_manager.get(cache_key)
76
+ except Exception as e:
77
+ # Enregistre une erreur si une exception se produit pendant la récupération du cache.
78
+ log_to_file(f"Erreur lors de la récupération du cache utilisateur : {str(e)}", level=logging.ERROR)
79
+ # Retourne None en cas d'erreur.
80
+ return None
81
+
82
+ async def set_user_in_cache(self, user_data: Dict[str, Any]) -> bool:
83
+ """Stocke les informations utilisateur dans le cache"""
84
+ try:
85
+ cache_key = f"auth_user:{user_data['email']}"
86
+ return await self.cache_manager.set(cache_key, user_data, expiry=self.cache_duration)
87
+ except Exception as e:
88
+ log_to_file(f"Erreur lors du stockage en cache : {str(e)}", level=logging.ERROR)
89
+ return False
90
+
91
+ async def load_all_users_to_cache(self) -> bool:
92
+ try:
93
+ log_to_file("Début du chargement des utilisateurs dans Redis", level=logging.INFO)
94
+
95
+ # Récupérer le nombre total d'utilisateurs
96
+ total_users = supabase.table("users").select("id", count="exact").execute()
97
+ total_count = total_users.count
98
+ log_to_file(f"Nombre total d'utilisateurs : {total_count}", level=logging.INFO)
99
+
100
+ # Charger les utilisateurs par lots
101
+ loaded_count = 0
102
+ for offset in range(0, total_count, self.batch_size):
103
+ log_to_file(f"Chargement du lot {offset} - {offset + self.batch_size - 1}", level=logging.INFO)
104
+ users = supabase.table("users").select("*").range(offset, offset + self.batch_size - 1).execute()
105
+ if users.data:
106
+ for user in users.data:
107
+ if await self.set_user_in_cache(user):
108
+ loaded_count += 1
109
+ log_to_file(f"Utilisateur {user['email']} mis en cache", level=logging.INFO)
110
+ else:
111
+ log_to_file(f"Échec du chargement de l'utilisateur : {user['email']}", level=logging.ERROR)
112
+ else:
113
+ log_to_file(f"Aucun utilisateur trouvé dans la plage {offset} - {offset + self.batch_size - 1}", level=logging.WARNING)
114
+
115
+ log_to_file(f"Chargement terminé : {loaded_count}/{total_count} utilisateurs mis en cache", level=logging.INFO)
116
+
117
+ # Mettre à jour la version du cache
118
+ await self.update_cache_version()
119
+
120
+ # Debug
121
+ await self.debug_redis_content()
122
+
123
+ return True
124
+ except Exception as e:
125
+ log_to_file(f"Erreur lors du chargement des utilisateurs : {str(e)}", level=logging.ERROR)
126
+ return False
127
+
128
+
129
+ async def sync_cache_with_db(self) -> bool:
130
+ """Synchronise le cache avec la base de données"""
131
+ try:
132
+ cache_version = await self.get_cache_version()
133
+ if not cache_version:
134
+ return await self.load_all_users_to_cache()
135
+
136
+ last_update = datetime.fromisoformat(cache_version)
137
+ updated_users = supabase.table("users").select("*").gt(
138
+ "updated_at", last_update.isoformat()
139
+ ).execute()
140
+
141
+ if updated_users.data:
142
+ for user in updated_users.data:
143
+ await self.set_user_in_cache(user)
144
+ await self.update_cache_version()
145
+ log_to_file(f"Cache mis à jour avec {len(updated_users.data)} modifications", level=logging.INFO)
146
+
147
+ return True
148
+ except Exception as e:
149
+ log_to_file(f"Erreur lors de la synchronisation du cache : {str(e)}", level=logging.ERROR)
150
+ return False
151
+
152
+
153
+ async def load_user_sessions_to_cache(self) -> bool:
154
+ """Charge toutes les sessions actives dans Redis"""
155
+ try:
156
+ log_to_file("Chargement des sessions utilisateurs dans Redis", level=logging.INFO)
157
+
158
+ # Récupérer toutes les sessions actives
159
+ sessions = supabase.table("chat_sessions").select("*").eq(
160
+ "is_active", True
161
+ ).execute()
162
+
163
+ if not sessions.data:
164
+ log_to_file("Aucune session active trouvée", level=logging.INFO)
165
+ return True
166
+
167
+ # Charger chaque session dans Redis
168
+ for session in sessions.data:
169
+ session_key = f"{self.session_cache_key}{session['user_id']}"
170
+ await self.cache_manager.set(
171
+ session_key,
172
+ session,
173
+ expiry=self.cache_duration
174
+ )
175
+
176
+ # Charger l'historique associé
177
+ history = supabase.table("messages").select("*").eq(
178
+ "session_id", session['session_id']
179
+ ).order('created_at', desc=False).execute()
180
+
181
+ if history.data:
182
+ history_key = f"{self.history_cache_key}{session['session_id']}"
183
+ await self.cache_manager.set(
184
+ history_key,
185
+ history.data,
186
+ expiry=self.cache_duration
187
+ )
188
+
189
+ log_to_file(f"Sessions et historiques chargés : {len(sessions.data)} sessions", level=logging.INFO)
190
+ return True
191
+
192
+ except Exception as e:
193
+ log_to_file(f"Erreur lors du chargement des sessions : {str(e)}", level=logging.ERROR)
194
+ return False
195
+
196
+ async def get_active_session(self, user_id: int) -> Optional[Dict]:
197
+ """Récupère la session active d'un utilisateur depuis le cache"""
198
+ try:
199
+ session_key = f"{self.session_cache_key}{user_id}"
200
+ session = await self.cache_manager.get(session_key)
201
+
202
+ if not session:
203
+ # Si pas dans le cache, chercher dans Supabase et mettre en cache
204
+ db_session = supabase.table("chat_sessions").select("*").eq(
205
+ "user_id", user_id
206
+ ).eq("is_active", True).execute()
207
+
208
+ if db_session.data:
209
+ session = db_session.data[0]
210
+ await self.cache_manager.set(
211
+ session_key,
212
+ session,
213
+ expiry=self.cache_duration
214
+ )
215
+
216
+ return session
217
+ except Exception as e:
218
+ log_to_file(f"Erreur lors de la récupération de la session : {str(e)}", level=logging.ERROR)
219
+ return None
220
+
221
+ async def get_chat_history(self, session_id: str) -> List[Dict]:
222
+ """Récupère l'historique du chat depuis le cache"""
223
+ try:
224
+ history_key = f"{self.history_cache_key}{session_id}"
225
+ history = await self.cache_manager.get(history_key)
226
+
227
+ if not history:
228
+ # Si pas dans le cache, chercher dans Supabase et mettre en cache
229
+ db_history = supabase.table("messages").select("*").eq(
230
+ "session_id", session_id
231
+ ).order('created_at', desc=False).execute()
232
+
233
+ if db_history.data:
234
+ history = db_history.data
235
+ await self.cache_manager.set(
236
+ history_key,
237
+ history,
238
+ expiry=self.cache_duration
239
+ )
240
+
241
+ return history or []
242
+ except Exception as e:
243
+ log_to_file(f"Erreur lors de la récupération de l'historique : {str(e)}", level=logging.ERROR)
244
+ return []
245
+
246
+ async def update_session_and_history(self, session_data: Dict, new_message: Dict = None):
247
+ """Met à jour la session et l'historique dans le cache"""
248
+ try:
249
+ session_key = f"{self.session_cache_key}{session_data['user_id']}"
250
+ await self.cache_manager.set(
251
+ session_key,
252
+ session_data,
253
+ expiry=self.cache_duration
254
+ )
255
+
256
+ if new_message:
257
+ history_key = f"{self.history_cache_key}{session_data['session_id']}"
258
+ current_history = await self.get_chat_history(session_data['session_id'])
259
+ current_history.append(new_message)
260
+ await self.cache_manager.set(
261
+ history_key,
262
+ current_history,
263
+ expiry=self.cache_duration
264
+ )
265
+ except Exception as e:
266
+ log_to_file(f"Erreur lors de la mise à jour de la session : {str(e)}", level=logging.ERROR)
267
+
268
+
269
+ async def debug_redis_content(self):
270
+ """Affiche le contenu de Redis pour debug"""
271
+ try:
272
+ all_keys = self.cache_manager.redis_client.keys("auth_user:*")
273
+ log_to_file(f"Nombre d'utilisateurs en cache : {len(all_keys)}", level=logging.INFO)
274
+ for key in all_keys:
275
+ log_to_file(f"Utilisateur en cache : {key}", level=logging.INFO)
276
+ except Exception as e:
277
+ log_to_file(f"Erreur debug Redis : {str(e)}", level=logging.ERROR)
utils/cache_manager.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import redis
2
+ import json
3
+ import logging
4
+ from typing import Any, Optional
5
+ from datetime import timedelta
6
+ import os
7
+ from dotenv import load_dotenv
8
+ from utils.logging_utils import log_to_file
9
+
10
+ load_dotenv()
11
+
12
+ class CacheManager:
13
+ def __init__(self):
14
+ """Initialise la connexion Redis"""
15
+ self.redis_client = redis.Redis(
16
+ host=os.getenv("REDIS_HOST"),
17
+ port=int(os.getenv("REDIS_PORT")),
18
+ decode_responses=True # Pour automatiquement décoder les réponses en str
19
+ )
20
+ self.default_expiry = timedelta(hours=1) # Expiration par défaut : 1 heure
21
+
22
+ async def get(self, key: str) -> Optional[Any]:
23
+ """Récupère une valeur du cache"""
24
+ try:
25
+ value = self.redis_client.get(key)
26
+ if value:
27
+ log_to_file(f"Cache hit pour la clé: {key}", level=logging.INFO)
28
+ return json.loads(value)
29
+ log_to_file(f"Cache miss pour la clé: {key}", level=logging.INFO)
30
+ return None
31
+ except Exception as e:
32
+ log_to_file(f"Erreur lors de la lecture du cache: {str(e)}", level=logging.ERROR)
33
+ return None
34
+
35
+ async def set(self, key: str, value: Any, expiry: Optional[timedelta] = None) -> bool:
36
+ """Enregistre une valeur dans le cache"""
37
+ try:
38
+ expiry_seconds = int((expiry or self.default_expiry).total_seconds())
39
+ success = self.redis_client.setex(
40
+ key,
41
+ expiry_seconds,
42
+ json.dumps(value)
43
+ )
44
+ if success:
45
+ log_to_file(f"Valeur mise en cache pour la clé: {key}", level=logging.INFO)
46
+ return success
47
+ except Exception as e:
48
+ log_to_file(f"Erreur lors de l'écriture dans le cache: {str(e)}", level=logging.ERROR)
49
+ return False
50
+
51
+ async def delete(self, key: str) -> bool:
52
+ """Supprime une valeur du cache"""
53
+ try:
54
+ return bool(self.redis_client.delete(key))
55
+ except Exception as e:
56
+ log_to_file(f"Erreur lors de la suppression du cache: {str(e)}", level=logging.ERROR)
57
+ return False
58
+
59
+ async def clear_all(self) -> bool:
60
+ """Vide tout le cache"""
61
+ try:
62
+ self.redis_client.flushall()
63
+ log_to_file("Cache entièrement vidé", level=logging.INFO)
64
+ return True
65
+ except Exception as e:
66
+ log_to_file(f"Erreur lors du vidage du cache: {str(e)}", level=logging.ERROR)
67
+ return False
utils/circuit_breaker.py ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ from enum import Enum
3
+ from datetime import datetime, timedelta
4
+ import logging
5
+ from typing import Optional, Callable, Any
6
+ from utils.logging_utils import log_to_file
7
+
8
+ class CircuitState(Enum):
9
+ CLOSED = "CLOSED" # Circuit fermé - Opérations normales
10
+ OPEN = "OPEN" # Circuit ouvert - Échecs détectés
11
+ HALF_OPEN = "HALF_OPEN" # Circuit semi-ouvert - Test de récupération
12
+
13
+ class AsyncCircuitBreaker:
14
+ def __init__(
15
+ self,
16
+ failure_threshold: int = 5,
17
+ recovery_timeout: int = 60,
18
+ half_open_timeout: int = 30
19
+ ):
20
+ self.failure_threshold = failure_threshold
21
+ self.recovery_timeout = recovery_timeout
22
+ self.half_open_timeout = half_open_timeout
23
+ self.state = CircuitState.CLOSED
24
+ self.failure_count = 0
25
+ self.last_failure_time: Optional[datetime] = None
26
+ self.lock = asyncio.Lock()
27
+
28
+ async def call(self, func: Callable, *args, **kwargs) -> Any:
29
+ """Exécute la fonction avec la logique du circuit breaker"""
30
+ async with self.lock:
31
+ if self.state == CircuitState.OPEN:
32
+ if self._should_attempt_recovery():
33
+ self.state = CircuitState.HALF_OPEN
34
+ log_to_file("Circuit breaker passé en état HALF_OPEN", level=logging.INFO)
35
+ else:
36
+ raise Exception("Circuit breaker ouvert - Service indisponible")
37
+
38
+ try:
39
+ result = await func(*args, **kwargs)
40
+ if self.state == CircuitState.HALF_OPEN:
41
+ self._reset()
42
+ return result
43
+
44
+ except Exception as e:
45
+ await self._handle_failure(e)
46
+ raise
47
+
48
+ def _should_attempt_recovery(self) -> bool:
49
+ """Vérifie si on doit tenter une récupération"""
50
+ if not self.last_failure_time:
51
+ return True
52
+
53
+ recovery_time = self.last_failure_time + timedelta(seconds=self.recovery_timeout)
54
+ return datetime.now() > recovery_time
55
+
56
+ async def _handle_failure(self, exception: Exception) -> None:
57
+ """Gère un échec d'appel"""
58
+ self.failure_count += 1
59
+ self.last_failure_time = datetime.now()
60
+
61
+ if self.state == CircuitState.HALF_OPEN or self.failure_count >= self.failure_threshold:
62
+ self.state = CircuitState.OPEN
63
+ log_to_file(f"Circuit breaker ouvert après {self.failure_count} échecs", level=logging.WARNING)
64
+
65
+ def _reset(self) -> None:
66
+ """Réinitialise le circuit breaker"""
67
+ self.state = CircuitState.CLOSED
68
+ self.failure_count = 0
69
+ self.last_failure_time = None
70
+ log_to_file("Circuit breaker réinitialisé et fermé", level=logging.INFO)
71
+
72
+ def get_state(self) -> CircuitState:
73
+ """Retourne l'état actuel du circuit breaker"""
74
+ return self.state
utils/email_utils.py ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import smtplib
2
+ from email.mime.text import MIMEText
3
+ from email.mime.multipart import MIMEMultipart
4
+ import os
5
+ from dotenv import load_dotenv
6
+ from typing import Optional
7
+ from utils.logging_utils import log_to_file # remplace "import logging" car maintenat les logging sont définis et importés depuis le fichier dédié: logging_utils.py
8
+
9
+ load_dotenv()
10
+
11
+ # Configuration email
12
+ EMAIL_HOST = os.getenv("EMAIL_HOST", "smtp.gmail.com")
13
+ EMAIL_PORT = int(os.getenv("EMAIL_PORT", "587"))
14
+ EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER")
15
+ EMAIL_HOST_USER_PASSWORD = os.getenv("EMAIL_HOST_USER_PASSWORD")
16
+ DEFAULT_FROM_EMAIL = os.getenv("DEFAULT_FROM_EMAIL")
17
+
18
+ """
19
+ Le fichier email_utils.py est responsable de l'envoi de tous les emails dans l'application, notamment :
20
+
21
+ Les emails de vérification lors de l'inscription d'un nouvel utilisateur
22
+ Les emails de réinitialisation de mot de passe quand un utilisateur clique sur "Mot de passe oublié"
23
+ Les emails de notification (activité du compte, changements de permission, etc.)
24
+
25
+ Pour tester les modifications :
26
+ Essayez de créer un nouveau compte (pour tester l'email de vérification)
27
+ Ou cliquez sur "Mot de passe oublié" sur la page de connexion
28
+ """
29
+
30
+ # Configuration du logging
31
+ #logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
32
+
33
+ # Tentative de chargement du .env
34
+ env_path = '.env'
35
+ if os.path.exists(env_path):
36
+ load_dotenv(env_path)
37
+ log_to_file(f"Fichier .env trouvé et chargé")
38
+ # Afficher les variables pour vérification
39
+ log_to_file(f"EMAIL_HOST: {os.getenv('EMAIL_HOST')}")
40
+ log_to_file(f"EMAIL_PORT: {os.getenv('EMAIL_PORT')}")
41
+ log_to_file(f"EMAIL_HOST_USER: {os.getenv('EMAIL_HOST_USER')}")
42
+ log_to_file(f"DEFAULT_FROM_EMAIL: {os.getenv('DEFAULT_FROM_EMAIL')}")
43
+ # Ne pas logger le mot de passe pour des raisons de sécurité
44
+ else:
45
+ log_to_file(f"Fichier .env non trouvé à l'emplacement: {os.path.abspath(env_path)}")
46
+
47
+ def create_smtp_connection() -> Optional[smtplib.SMTP]:
48
+ """
49
+ Crée et retourne une connexion SMTP sécurisée.
50
+ """
51
+ try:
52
+ server = smtplib.SMTP(EMAIL_HOST, EMAIL_PORT)
53
+ server.ehlo() # Identifier le client au serveur
54
+ server.starttls() # Activer le chiffrement TLS
55
+ server.login(EMAIL_HOST_USER, EMAIL_HOST_USER_PASSWORD)
56
+ return server
57
+ except Exception as e:
58
+ log_to_file(f"Erreur lors de la connexion SMTP : {str(e)}")
59
+ return None
60
+
61
+ def send_email(to_email: str, subject: str, html_content: str) -> bool:
62
+ """
63
+ Envoie un email avec un contenu HTML.
64
+ """
65
+ if not all([EMAIL_HOST_USER, EMAIL_HOST_USER_PASSWORD, DEFAULT_FROM_EMAIL]):
66
+ log_to_file("Configuration email manquante dans le fichier .env")
67
+ return False
68
+
69
+ try:
70
+ # Création du message
71
+ msg = MIMEMultipart('alternative')
72
+ msg['Subject'] = subject
73
+ msg['From'] = DEFAULT_FROM_EMAIL
74
+ msg['To'] = to_email
75
+
76
+ # Ajout du contenu HTML
77
+ html_part = MIMEText(html_content, 'html')
78
+ msg.attach(html_part)
79
+
80
+ # Création de la connexion SMTP et envoi
81
+ server = create_smtp_connection()
82
+ if not server:
83
+ return False
84
+
85
+ try:
86
+ server.send_message(msg)
87
+ log_to_file(f"Email envoyé avec succès à {to_email}")
88
+ return True
89
+ except Exception as e:
90
+ log_to_file(f"Erreur lors de l'envoi de l'email : {str(e)}")
91
+ return False
92
+ finally:
93
+ try:
94
+ server.quit()
95
+ except:
96
+ pass
97
+
98
+ except Exception as e:
99
+ log_to_file(f"Erreur lors de la préparation de l'email : {str(e)}")
100
+ return False
utils/ip_utils.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ 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)
3
+ from utils.logging_utils import log_to_file # système de logging défini sur un fichier séparé pour une meilleure lisibilité.
4
+
5
+
6
+ def get_user_ip():
7
+
8
+ """Récupère l'adresse IP publique de l'utilisateur.
9
+ Cette fonction utilise la bibliothèque requests pour récupérer l'adresse IP publique de l'utilisateur.
10
+ Si une erreur est rencontrée lors de la récupération de l'adresse IP, la fonction renvoie None.
11
+ """
12
+ try:
13
+ response = requests.get('https://api.ipify.org?format=json')
14
+ response.raise_for_status() # Lever une exception si la requête a échoué
15
+ return response.json()['ip']
16
+ except requests.exceptions.RequestException as e:
17
+ # Gérer les erreurs de requête (connexion, timeout, etc.)
18
+ log_to_file(f"-(ip_utils.py/def get_user_ip():)Erreur lors de la récupération de l'adresse IP : {e}", level=logging.ERROR)
19
+ return None
utils/logging_utils.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from datetime import datetime
3
+
4
+
5
+ #Nouveau système de lod
6
+ def log_to_file(message, level=logging.INFO):
7
+ """Fonction centralisée pour la gestion des logs"""
8
+ logger = logging.getLogger(__name__)
9
+ logger.log(level, message)
10
+
11
+ # Écrire également dans un fichier
12
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
13
+ with open("app_logs.txt", "a") as log_file:
14
+ log_file.write(f"{timestamp} - {logging.getLevelName(level)} - {message}\n")
utils/notification_handler.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # utils/notification_handler.py (fichier en relation avec profile.py)
2
+ import streamlit as st
3
+ from utils.email_utils import send_email
4
+ from utils.admin import supabase
5
+ from datetime import datetime
6
+ import pytz
7
+ import logging
8
+
9
+ def send_user_notification(user_id: int, notification_type: str, message: str) -> bool:
10
+ """
11
+ Envoie une notification à l'utilisateur en respectant ses préférences
12
+ """
13
+ try:
14
+ logging.info(f"Début de l'envoi de notification pour user_id: {user_id}, type: {notification_type}")
15
+ # Récupérer les préférences de l'utilisateur
16
+ user_prefs = supabase.table("notification_preferences").select("*").eq("user_id", user_id).execute()
17
+ user_info = supabase.table("users").select("email").eq("id", user_id).execute()
18
+
19
+ if not user_prefs.data:
20
+ logging.error(f"Aucune préférence trouvée pour user_id: {user_id}")
21
+ return False
22
+ if not user_info.data:
23
+ logging.error(f"Aucun utilisateur trouvé pour user_id: {user_id}")
24
+ return False
25
+
26
+
27
+ prefs = user_prefs.data[0]
28
+ user_email = user_info.data[0]['email']
29
+ logging.info(f"Préférences trouvées pour {user_email}: {prefs}")
30
+
31
+ # Vérifier si les notifications sont activées pour ce type
32
+ if notification_type == "security" and not prefs['security_alerts']:
33
+ logging.info(f"Alertes de sécurité désactivées pour {user_email}")
34
+ return False
35
+ elif notification_type == "assistant" and not prefs['assistant_updates']:
36
+ logging.info(f"Mises à jour assistant désactivées pour {user_email}")
37
+ return False
38
+ elif notification_type == "daily" and not prefs['daily_summary']:
39
+ logging.info(f"Résumé quotidien désactivé pour {user_email}")
40
+ return False
41
+
42
+ # Si les notifications par email sont activées
43
+ logging.info(f"Envoi d'email à {user_email}")
44
+ success = send_email(user_email,
45
+ f"Notification - {notification_type.capitalize()}",
46
+ f"""
47
+ <html>
48
+ <body>
49
+ <h2>Notification {notification_type}</h2>
50
+ <p>{message}</p>
51
+ <p>Date : {datetime.now(pytz.UTC).strftime('%Y-%m-%d %H:%M:%S UTC')}</p>
52
+ </body>
53
+ </html>
54
+ """)
55
+
56
+ if success:
57
+ logging.info(f"Email envoyé avec succès à {user_email}")
58
+ else:
59
+ logging.error(f"Échec de l'envoi de l'email à {user_email}")
60
+
61
+ return success
62
+
63
+ except Exception as e:
64
+ logging.error(f"Erreur lors de l'envoi de la notification : {str(e)}")
65
+ logging.error(f"Traceback complet : ", exc_info=True)
66
+ return False
67
+
68
+ def notify_security_alert(user_id: int, message: str) -> bool:
69
+ """Envoie une alerte de sécurité"""
70
+ return send_user_notification(user_id, "security", message)
71
+
72
+ def notify_assistant_update(user_id: int, message: str) -> bool:
73
+ """Envoie une notification de mise à jour des assistants"""
74
+ return send_user_notification(user_id, "assistant", message)
75
+
76
+ def send_daily_summary(user_id: int, summary_data: dict) -> bool:
77
+ """Envoie le résumé quotidien"""
78
+ message = f"""
79
+ Résumé de vos activités :
80
+ - Nombre de conversations : {summary_data.get('conversations', 0)}
81
+ - Questions posées : {summary_data.get('questions', 0)}
82
+ - Temps total d'utilisation : {summary_data.get('usage_time', '0')} minutes
83
+ """
84
+ return send_user_notification(user_id, "daily", message)
utils/password_reset.py ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ from datetime import datetime, timedelta
3
+ import pytz
4
+ import secrets
5
+ import bcrypt
6
+ from supabase import create_client
7
+ import os
8
+ from dotenv import load_dotenv
9
+ from utils.email_utils import send_email
10
+ from urllib.parse import urljoin, urlparse
11
+ from utils.logging_utils import log_to_file # remplace "import logging" car maintenat les logging sont définis et importés depuis le fichier dédié: logging_utils.py
12
+
13
+ load_dotenv()
14
+ supabase = create_client(os.getenv("SUPABASE_URL"), os.getenv("SUPABASE_KEY"))
15
+
16
+ # Fonction pour générer un token de réinitialisation de mot de passe
17
+ def generate_reset_token() -> str:
18
+ """Génère un token sécurisé pour la réinitialisation de mot de passe."""
19
+ return secrets.token_urlsafe(32)
20
+
21
+ # Fonction pour créer un token de réinitialisation de mot de passe pour un utilisateur spécifique
22
+ def create_reset_token(user_id: int) -> tuple[bool, str]:
23
+ """
24
+ Crée un token de réinitialisation de mot de passe.
25
+ """
26
+ try:
27
+ token = generate_reset_token()
28
+ expires_at = datetime.now(pytz.UTC) + timedelta(hours=24)
29
+
30
+ # Invalider les tokens précédents
31
+ supabase.table("user_tokens").update({
32
+ "used": True
33
+ }).eq("user_id", user_id).eq("type", "reset_password").execute()
34
+
35
+ # Créer un nouveau token
36
+ supabase.table("user_tokens").insert({
37
+ "user_id": user_id,
38
+ "token": token,
39
+ "type": "reset_password",
40
+ "expires_at": expires_at.isoformat()
41
+ }).execute()
42
+
43
+ return True, token
44
+ except Exception as e:
45
+ log_to_file(f"Erreur lors de la création du token : {str(e)}")
46
+ return False, ""
47
+
48
+ # Fonction pour envoyer l'email de réinitialisation
49
+ def send_reset_email(email: str, reset_url: str) -> bool:
50
+ """
51
+ Envoie l'email de réinitialisation.
52
+ """
53
+ subject = "Réinitialisation de votre mot de passe"
54
+ html_content = f"""
55
+ <html>
56
+ <body>
57
+ <h2>Réinitialisation de votre mot de passe</h2>
58
+ <p>Vous avez demandé la réinitialisation de votre mot de passe.</p>
59
+ <p>Cliquez sur le lien ci-dessous pour définir un nouveau mot de passe :</p>
60
+ <p><a href="{reset_url}">Réinitialiser mon mot de passe</a></p>
61
+ <p>Ce lien est valable pendant 24 heures.</p>
62
+ <p>Si vous n'avez pas demandé cette réinitialisation, ignorez cet email.</p>
63
+ </body>
64
+ </html>
65
+ """
66
+ return send_email(email, subject, html_content)
67
+
68
+
69
+ # Fonction pour initier le processus de réinitialisation du mot de passe
70
+ def initiate_password_reset(email: str) -> tuple[bool, str]:
71
+ """
72
+ Initie le processus de réinitialisation de mot de passe.
73
+ """
74
+ try:
75
+ # Vérifier si l'utilisateur existe
76
+ user = supabase.table("users").select("id").eq("email", email).execute()
77
+ if not user.data:
78
+ return False, "Aucun compte associé à cet email."
79
+
80
+ user_id = user.data[0]['id']
81
+ success, token = create_reset_token(user_id)
82
+
83
+ if not success:
84
+ return False, "Erreur lors de la création du token."
85
+
86
+
87
+ # Construire l'URL de réinitialisation de manière sécurisée
88
+ base_url = os.getenv('APP_URL', '').rstrip('/')
89
+ # Nettoyer l'URL de base
90
+ if base_url.startswith('http://'):
91
+ base_url = base_url[7:]
92
+ elif base_url.startswith('https://'):
93
+ base_url = base_url[8:]
94
+
95
+ base_url = base_url.strip('/')
96
+ reset_url = f"http://{base_url}/reset_password?token={token}"
97
+
98
+ # Envoyer l'email
99
+ if send_reset_email(email, reset_url):
100
+ return True, "Instructions envoyées par email."
101
+ return False, "Erreur lors de l'envoi de l'email."
102
+
103
+ except Exception as e:
104
+ log_to_file(f"Erreur lors de l'initiation de la réinitialisation : {str(e)}")
105
+ return False, "Une erreur est survenue."
106
+
107
+
108
+ # Fonction pour vérifier la validité d'un token de réinitialisation
109
+ def verify_reset_token(token: str) -> tuple[bool, int]:
110
+ """
111
+ Vérifie la validité d'un token de réinitialisation.
112
+ """
113
+ try:
114
+ response = supabase.table("user_tokens").select("*").eq(
115
+ "token", token
116
+ ).eq("type", "reset_password").eq("used", False).execute()
117
+
118
+ if not response.data:
119
+ return False, 0
120
+
121
+ token_data = response.data[0]
122
+ expires_at = datetime.fromisoformat(token_data['expires_at'].replace('Z', '+00:00'))
123
+
124
+ if expires_at < datetime.now(pytz.UTC):
125
+ return False, 0
126
+
127
+ return True, token_data['user_id']
128
+ except Exception as e:
129
+ log_to_file(f"Erreur lors de la vérification du token : {str(e)}")
130
+ return False, 0
131
+
132
+
133
+ # Fonction pour réinitialiser le mot de passe avec le token de réinitialisation
134
+ def reset_password(token: str, new_password: str) -> tuple[bool, str]:
135
+ """
136
+ Réinitialise le mot de passe avec le token.
137
+ """
138
+ try:
139
+ valid, user_id = verify_reset_token(token)
140
+ if not valid:
141
+ return False, "Token invalide ou expiré."
142
+
143
+ # Hasher le nouveau mot de passe
144
+ hashed_password = bcrypt.hashpw(new_password.encode(), bcrypt.gensalt()).decode()
145
+
146
+ # Mettre à jour le mot de passe
147
+ supabase.table("users").update({
148
+ "password": hashed_password
149
+ }).eq("id", user_id).execute()
150
+
151
+ # Marquer le token comme utilisé
152
+ supabase.table("user_tokens").update({
153
+ "used": True
154
+ }).eq("token", token).execute()
155
+
156
+ return True, "Mot de passe mis à jour avec succès."
157
+ except Exception as e:
158
+ log_to_file(f"Erreur lors de la réinitialisation du mot de passe : {str(e)}")
159
+ return False, "Une erreur est survenue."
utils/rate_limiter.py ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import pytz
3
+ from datetime import datetime, timedelta
4
+ import time
5
+ from typing import Dict, Optional
6
+ import logging
7
+ from utils.logging_utils import log_to_file
8
+ from utils.admin import supabase # Ajoutez cet import avec les autres
9
+ from utils.cache_manager import CacheManager
10
+ cache_manager = CacheManager() # optimiser les statistiques d'utilisation avec le cache
11
+
12
+ class RateLimiter:
13
+ def __init__(self, requests_per_second: int = 10, burst_limit: int = 20): # 10 requetes par seconde, 20 requetes en rafale
14
+ """
15
+ Initialise le rate limiter
16
+ requests_per_second: Nombre maximal de requêtes par seconde
17
+ burst_limit: Nombre maximal de requêtes en rafale
18
+ """
19
+ self.rate = requests_per_second # Nombre maximal de requetes pas seconde (aujourd'hui)
20
+ self.burst_limit = burst_limit # Nombre maximal de requetes en rafale (aujourd'hui)
21
+ self.tokens = burst_limit # Nombre de jetons restants en rafale (aujourd'hui)
22
+ self.last_update = time.monotonic() # Derniere mise à jour
23
+ self.lock = asyncio.Lock() # Prise en charge du verrouillage
24
+ self._requests_count: Dict[str, int] = {} # Compteur de requetes
25
+ self.last_cleanup = time.time() # Date du dernier nettoyage
26
+ self.cleanup_interval = 3600 # 1 heure en secondes pour le nettoyage au bout d'une heure
27
+ self.cache_manager = CacheManager() # utilisation du cache pour stocker les statisqtiques d'utilisation (aujourd'hui)
28
+
29
+
30
+ async def acquire(self, user_id: Optional[str] = None) -> bool:
31
+ """Tente d'acquérir un token pour faire une requête"""
32
+ async with self.lock:
33
+ now = time.monotonic()
34
+ time_passed = now - self.last_update
35
+ self.tokens = min(
36
+ self.burst_limit,
37
+ self.tokens + time_passed * self.rate
38
+ )
39
+ self.last_update = now
40
+
41
+ if self.tokens >= 1:
42
+ self.tokens -= 1
43
+ if user_id:
44
+ await self._track_request(user_id)
45
+ return True
46
+
47
+ log_to_file(f"Rate limit atteint pour l'utilisateur {user_id}", level=logging.WARNING)
48
+ return False
49
+
50
+
51
+ async def _get_stats_from_cache(self, user_id: str) -> Optional[Dict]:
52
+ """Récupère les statistiques depuis le cache"""
53
+ cache_key = f"usage_stats:{user_id}"
54
+ return await cache_manager.get(cache_key)
55
+
56
+ async def _update_stats_cache(self, user_id: str, stats: Dict):
57
+ """Met à jour les statistiques dans le cache"""
58
+ cache_key = f"usage_stats:{user_id}"
59
+ await cache_manager.set(cache_key, stats, expiry=timedelta(hours=1))
60
+
61
+ async def _track_request(self, user_id: str) -> None:
62
+ """Suit les requêtes par utilisateur avec cache"""
63
+ try:
64
+ current_time = datetime.now(pytz.UTC).isoformat()
65
+ stats_data = None # Initialiser stats_data
66
+
67
+ # Vérifier d'abord le cache
68
+ cached_stats = await self._get_stats_from_cache(user_id)
69
+
70
+ if cached_stats:
71
+ # Mettre à jour les stats en cache
72
+ cached_stats['request_count'] += 1
73
+ cached_stats['last_request'] = current_time
74
+ await self._update_stats_cache(user_id, cached_stats)
75
+ stats_data = cached_stats
76
+
77
+ # Mettre à jour Supabase de manière asynchrone
78
+ supabase.table("usage_statistics").update({
79
+ "request_count": cached_stats['request_count'],
80
+ "last_request": current_time
81
+ }).eq("user_id", user_id).execute()
82
+ else:
83
+ # Vérifier Supabase
84
+ existing = supabase.table("usage_statistics").select("*").eq("user_id", user_id).execute()
85
+
86
+ if existing.data:
87
+ # Mettre à jour les stats existantes
88
+ stats = existing.data[0]
89
+ new_count = stats.get('request_count', 0) + 1
90
+ stats_data = {
91
+ "request_count": new_count,
92
+ "last_request": current_time
93
+ }
94
+
95
+ supabase.table("usage_statistics").update(stats_data).eq("user_id", user_id).execute()
96
+ await self._update_stats_cache(user_id, stats_data)
97
+ else:
98
+ # Créer nouvelles stats
99
+ stats_data = {
100
+ "user_id": user_id,
101
+ "request_count": 1,
102
+ "last_request": current_time,
103
+ "last_cleanup": current_time
104
+ }
105
+
106
+ supabase.table("usage_statistics").insert(stats_data).execute()
107
+ await self._update_stats_cache(user_id, stats_data)
108
+
109
+ if stats_data: # Vérifier que stats_data existe
110
+ self._requests_count[user_id] = stats_data['request_count']
111
+ log_to_file(f"Nouvelle requête pour l'utilisateur {user_id}. Total: {stats_data['request_count']}", level=logging.INFO)
112
+
113
+ except Exception as e:
114
+ log_to_file(f"Erreur lors du suivi des requêtes: {str(e)}", level=logging.ERROR)
115
+ # Continuer avec le comptage en mémoire en cas d'erreur
116
+ if user_id not in self._requests_count:
117
+ self._requests_count[user_id] = 1
118
+ else:
119
+ self._requests_count[user_id] += 1
120
+
121
+
122
+
123
+
124
+ async def get_user_requests_count(self, user_id: str) -> int:
125
+ """Retourne le nombre de requêtes avec cache"""
126
+ try:
127
+ cached_stats = await self._get_stats_from_cache(user_id)
128
+ if cached_stats:
129
+ return cached_stats.get('request_count', 0)
130
+
131
+ response = supabase.table("usage_statistics").select("request_count").eq("user_id", user_id).execute()
132
+ if response.data:
133
+ count = response.data[0].get('request_count', 0)
134
+ await self._update_stats_cache(user_id, {"request_count": count})
135
+ return count
136
+ return 0
137
+ except Exception as e:
138
+ log_to_file(f"Erreur lors de la récupération du compte des requêtes: {str(e)}", level=logging.ERROR)
139
+ return self._requests_count.get(user_id, 0)
utils/theme_utils.py ADDED
@@ -0,0 +1,558 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import base64
3
+ import os
4
+ from datetime import datetime
5
+ import pytz
6
+ import logging
7
+ from utils.admin import supabase # Pour load_theme_preference et save_theme_preference
8
+ from utils.logging_utils import log_to_file
9
+
10
+
11
+ # Fonction pour charger la préférence de thème depuis la base de données
12
+ def load_theme_preference():
13
+ """Charge la préférence de thème depuis la base de données"""
14
+ if 'user' in st.session_state and st.session_state['user']:
15
+ user_id = st.session_state['user'].get('id')
16
+ if user_id:
17
+ try:
18
+ response = supabase.table("notification_preferences").select("theme").eq("user_id", user_id).execute()
19
+ if response.data:
20
+ theme = response.data[0]['theme']
21
+ log_to_file(f"Thème '{theme}' chargé pour l'utilisateur {user_id}")
22
+ return theme
23
+ except Exception as e:
24
+ log_to_file(f"Erreur lors du chargement du thème : {str(e)}", logging.ERROR)
25
+ return 'Clair' # Thème par défaut
26
+
27
+
28
+ # Fonction pour sauvegarder la préférence de thème dans la base de données
29
+ def save_theme_preference(theme):
30
+ """Sauvegarde la préférence de thème dans la base de données"""
31
+ if 'user' in st.session_state and st.session_state['user']:
32
+ user_id = st.session_state['user'].get('id')
33
+ if user_id:
34
+ try:
35
+ # Vérifier si une préférence existe déjà
36
+ existing_preference = supabase.table("notification_preferences").select("*").eq("user_id", user_id).execute()
37
+
38
+ if existing_preference.data:
39
+ # Mettre à jour la ligne existante
40
+ supabase.table("notification_preferences").update({"theme": theme}).eq("user_id", user_id).execute()
41
+ else:
42
+ # Insérer une nouvelle ligne
43
+ supabase.table("notification_preferences").insert({"user_id": user_id, "theme": theme}).execute()
44
+
45
+ st.session_state.user_theme = theme
46
+ log_to_file(f"Thème '{theme}' sauvegardé pour l'utilisateur {user_id}")
47
+ except Exception as e:
48
+ log_to_file(f"Erreur lors de la sauvegarde du thème : {str(e)}", logging.ERROR)
49
+
50
+
51
+ def apply_theme(theme):
52
+ # Définition des couleurs communes selon le thème choisi (sombre ou clair)
53
+
54
+ # Texte principal : Gris clair (#E0E0E0) pour le thème sombre, Gris foncé (#4a4a4a) pour le thème clair
55
+ text_color = "#E0E0E0" if theme == "Sombre" else "#4a4a4a"
56
+
57
+ # Couleur de fond de la page : Gris très foncé (#1E1E1E) pour le thème sombre, blanc cassé (#f7f9fc) pour le thème clair
58
+ bg_color = "#1E1E1E" if theme == "Sombre" else "#f7f9fc"
59
+
60
+ # Couleur de fond des zones de saisie (inputs) : Gris foncé (#333333) pour le thème sombre, gris très clair avec une légère teinte bleue (#eef3f7) pour le thème clair
61
+ input_bg_color= "#333333" if theme == "Sombre" else "#eef3f7"
62
+
63
+ # Couleur de fond des zones de saisie (inputs) : Gris foncé (#333333) pour le thème sombre, rouge saumon clair (#ffcccb pour le thème clair
64
+ input_text_area_inactive = "#333333" if theme == "Sombre" else "#ffcccb"
65
+
66
+ # Couleur de fond des zones de saisie active (inputs) : Gris foncé (#999999) pour le thème sombre, Blanc pur (#ffffff) pour le thème clair
67
+ input_text_area_active = "#666666" if theme == "Sombre" else "#ffffff"
68
+
69
+
70
+ # Définir la couleur du curseur (caret) selon le thème
71
+ caret_color = "white" if theme == "Sombre" else "black" # Blanc pour le thème sombre, noir pour le thème clair
72
+
73
+ # Couleur de fond de la barre latérale : Gris très foncé (#2D2D2D) pour le thème sombre, gris bleu très clair ( #e3e9f1) pour le thème clair
74
+ sidebar_bg_color = "#2D2D2D" if theme == "Sombre" else " #e3e9f1"
75
+
76
+ # Couleurs spécifiques pour les messages utilisateur et assistant :
77
+
78
+ # Messages de l'utilisateur : Bleu foncé (#0E4C92) pour le thème sombre, Bleu clair désaturé (#d1e7ff) pour le thème clair
79
+ user_msg_bg = "#0E4C92" if theme == "Sombre" else "#d1e7ff"
80
+
81
+ # Messages de l'assistant : Vert foncé (#1A472A) pour le thème sombre, Gris perle (#f0f0f0) pour le thème clair
82
+ assistant_msg_bg = "#1A472A" if theme == "Sombre" else "#f0f0f0"
83
+
84
+ # Couleurs pour les boutons et éléments interactifs :
85
+
86
+ # Fond des boutons : Bleu acier (#4682B4) pour le thème sombre, Or (#FFD700) pour le thème clair
87
+ button_bg = "#4682B4" if theme == "Sombre" else "#FFD700"
88
+
89
+ # Texte des boutons : Blanc (#FFFFFF) pour le thème sombre, Vert mer (#8FBC8F) pour le thème clair
90
+ button_text = "white" if theme == "Sombre" else "#8FBC8F"
91
+
92
+ # Fond du selectbox : Bleu foncé (#0E4C92) pour le thème sombre, gris très clair avec une légère teinte bleue (#eef3f7) pour le thème clair
93
+ select_bg = "#ff5e4d" if theme == "Sombre" else "#eef3f7"
94
+
95
+ # Fond du selectbox : Bleu foncé (#0E4C92) pour le thème sombre, Gris clair (#e6f7ff) pour le thème clair
96
+ select_option_bg = "#1c39bb" if theme == "Sombre" else "#e6f7ff"
97
+
98
+ select_text_color = "black" if theme == "Sombre" else "#333333" # Couleur du texte du selectbox
99
+
100
+ # Génération des styles CSS pour Streamlit
101
+ st.markdown(f"""
102
+ <style>
103
+ /* Styles généraux */
104
+ .stApp {{
105
+ background-color: {bg_color}; /* Couleur de fond générale */
106
+ color: {text_color} !important; /* Couleur de texte principale */
107
+ }}
108
+
109
+ /* Styles pour les messages de chat */
110
+ .stChatMessage {{
111
+ background-color: {input_bg_color} !important; /* Fond des messages */
112
+ color: {text_color} !important; /* Couleur de texte dans les messages */
113
+ }}
114
+ .stChatMessage.user {{ background-color: {user_msg_bg} !important; }} /* Fond des messages utilisateur */
115
+ .stChatMessage.assistant {{ background-color: {assistant_msg_bg} !important; }} /* Fond des messages assistant */
116
+ .stChatMessage * {{ color: {text_color} !important; }} /* Couleur de texte dans tous les éléments des messages */
117
+
118
+ /* Style pour le conteneur de la zone de saisie */
119
+ [data-testid="stBottomBlockContainer"] {{
120
+ background-color: {input_bg_color} !important; /* Fond de la zone de saisie */
121
+ }}
122
+
123
+ /* Styles pour la zone de saisie et son conteneur interne */
124
+ .stChatInput,
125
+ .stChatInput > div,
126
+ .stChatInput textarea {{
127
+ background-color: {input_text_area_inactive} !important; /* Fond de la zone de saisie */
128
+ border-color: {"#555555" if theme == "Sombre" else "#b0e0e6"} !important; /* Bordure des champs de saisie */
129
+ color: {text_color} !important; /* Texte dans la zone de saisie */
130
+ }}
131
+ .stChatInput textarea:focus {{
132
+ background-color: {input_text_area_active} !important; /* Fond quand la zone est active */
133
+ color: {text_color} !important; /* Texte actif */
134
+ caret-color: {caret_color} !important; /* Couleur du curseur actif */
135
+ box-shadow: none !important; /* Pas de halo lumineux lors de la saisie */
136
+ }}
137
+
138
+ /* Style pour le bouton d'envoi */
139
+ .stChatInput button {{
140
+ background-color: {button_bg} !important; /* Fond des boutons */
141
+ color: {button_text} !important; /* Couleur du texte des boutons */
142
+ }}
143
+
144
+ /* Styles pour la barre latérale */
145
+ [data-testid="stSidebar"] {{
146
+ background-color: {sidebar_bg_color}; /* Couleur de fond de la barre latérale */
147
+ color: {text_color}; /* Texte de la barre latérale */
148
+ }}
149
+ [data-testid="stSidebar"] .st-bw,
150
+ [data-testid="stSidebar"] h3 {{ color: {text_color} !important; }} /* Couleur des titres et textes dans la barre latérale */
151
+
152
+ /* Styles pour les boutons */
153
+ .stButton > button {{
154
+ background-color: {button_bg}; /* Fond des boutons généraux */
155
+ color: {button_text}; /* Couleur du texte des boutons */
156
+ }}
157
+
158
+ /* Styles pour les expandeurs */
159
+ .streamlit-expanderHeader {{
160
+ background-color: {input_bg_color}; /* Fond des en-têtes d'expandeur */
161
+ color: {text_color}; /* Couleur du texte des en-têtes d'expandeur */
162
+ transition: all 0.3s ease-out; /* Transition fluide pour l'expansion */
163
+ }}
164
+ .streamlit-expanderHeader[aria-expanded="true"] {{ transform: translateX(10px); }} /* Effet de déplacement quand l'expandeur est ouvert */
165
+ .streamlit-expanderContent {{
166
+ background-color: {"#1E90FF" if theme == "Sombre" else "#F0F0F0"} !important; /* Couleur de fond du contenu de l'expandeur */
167
+ padding: 10px;
168
+ border-radius: 5px; /* Coins arrondis du contenu de l'expandeur */
169
+ }}
170
+
171
+ /* Styles pour le selectbox */
172
+ .stSelectbox > div > div:first-child,
173
+ .stSelectbox > div > div:nth-child(2),
174
+ .stSelectbox ul {{
175
+ background-color: {select_bg} !important; /* Fond des éléments du selectbox */
176
+ color: {"white" if theme == "Sombre" else "#333333"} !important; /* Couleur du texte des éléments du selectbox */
177
+ }}
178
+ .stSelectbox ul li:hover, .stSelectbox ul li[aria-selected="true"] {{
179
+ background-color: {"#ffe4e1" if theme == "Sombre" else "#e6f7ff"} !important; /* Fond des éléments au survol et sélectionnés */
180
+ }}
181
+
182
+ /* Styles pour le selectbox */
183
+ [data-testid="stSelectboxVirtualDropdown"], .st-emotion-cache-1o2fhjg.e1811lun0 {{
184
+ background-color: {select_option_bg} !important; /* Fond des éléments du selectbox */
185
+ color: {select_text_color} !important; /* Couleur du texte des options */
186
+ }}
187
+
188
+ [data-testid="stSelectboxVirtualDropdown"] li[aria-selected="true"], .st-emotion-cache-1o2fhjg.e1811lun0[aria-selected="true"] {{
189
+ background-color: {"#1A6BBF" if theme == "Sombre" else "#ffcccb"} !important; /* Fond des options sélectionnées */
190
+ }}
191
+
192
+
193
+ [data-testid="stSelectboxVirtualDropdown"] li:hover {{
194
+ background-color: {"#1A6BBF" if theme == "Sombre" else "#ffa07a"} !important; /* Fond au survol des options */
195
+ }}
196
+
197
+ /* Styles pour les boutons de feedback */
198
+ .stChatMessage .stButton > button {{
199
+ background-color: transparent !important; /* Fond transparent pour les boutons dans les messages */
200
+ border: none !important;
201
+ box-shadow: none !important;
202
+ padding: 0 !important;
203
+ }}
204
+ .stChatMessage .stButton > button:hover {{
205
+ background-color: rgba(0, 0, 0, 0.05) !important; /* Léger changement de fond au survol */
206
+ }}
207
+
208
+ /* Styles pour les boutons de la barre latérale */
209
+ [data-testid="stSidebar"] .stButton > button {{
210
+ background-color: {"#1E90FF" if theme == "Sombre" else "#FFD700"} !important; /* Fond des boutons spécifiques dans la barre latérale */
211
+ color: {button_text} !important; /* Couleur du texte des boutons dans la barre latérale */
212
+ border: inherit !important;
213
+ box-shadow: inherit !important;
214
+ padding: inherit !important;
215
+ }}
216
+
217
+ /* Assurez-vous que tous les textes sont lisibles */
218
+ body, p, h1, h2, h3, h4, h5, h6, span, div {{
219
+ color: {"#E0E0E0" if theme == "Sombre" else "#4a4a4a"} !important; /* Couleur des textes dans tout le contenu de la page, */
220
+ }}
221
+
222
+ </style>
223
+ """, unsafe_allow_html=True)
224
+
225
+
226
+ # ajout de logo sur l'en -tête de l'application (cette focntion est appelé dans inject_custom_css(theme))
227
+ def get_base64_encoded_image(image_file):
228
+ with open(image_file, "rb") as img_file:
229
+ return base64.b64encode(img_file.read()).decode('utf-8')
230
+
231
+ # ajout de logo sur l'en -tête de l'application et animation du logo
232
+ def inject_custom_css(theme):
233
+ # Définition des paramètres visuels en fonction du thème
234
+ is_dark_theme = theme == "Sombre"
235
+
236
+ # Sélection du logo et des couleurs selon le thème
237
+ logo_file = "assets/logo2.svg" if is_dark_theme else "assets/logo.svg"
238
+ background_color = "#333333" if is_dark_theme else "#eef3f7"
239
+ text_color = "#E0E0E0" if is_dark_theme else "#333333"
240
+ # Encodage du logo en base64
241
+ logo_base64 = get_base64_encoded_image(logo_file)
242
+
243
+ st.markdown(
244
+ f"""
245
+ <style>
246
+ @keyframes rotate {{
247
+ from {{
248
+ transform: rotate(0deg);
249
+ }}
250
+ to {{
251
+ transform: rotate(360deg);
252
+ }}
253
+ }}
254
+
255
+ .stApp header {{
256
+ display: flex;
257
+ align-items: center;
258
+ background-color: {background_color} !important;
259
+ padding-left: 240px !important;
260
+ }}
261
+
262
+ .stApp header::before {{
263
+ content: "";
264
+ background-image: url("data:image/svg+xml;base64,{logo_base64}");
265
+ background-repeat: no-repeat;
266
+ background-position: center;
267
+ background-size: contain;
268
+ width: 190px;
269
+ height: 190px;
270
+ position: absolute;
271
+ left: 350px;
272
+ animation: rotate 10s linear infinite;
273
+ }}
274
+
275
+ .stApp header::after {{
276
+ content: "Colibri IA: la précision d'un colibri, la puissance d'un assistant";
277
+ font-size: 15px;
278
+ font-weight: normal;
279
+ color: {text_color};
280
+ margin-left: 250px;
281
+ background-color: rgba(0, 0, 0, 0.02) !important;
282
+ }}
283
+ </style>
284
+ """,
285
+ unsafe_allow_html=True,)
286
+
287
+
288
+ # ajout de logo sur la sidebar avec un texte simulant un en-tête
289
+ def add_sidebar_logo(theme):
290
+ # Définition des couleurs en fonction du thème
291
+ text_color = "#E0E0E0" if theme == "Sombre" else "#333333"
292
+ # #E0E0E0 : Gris très clair, presque blanc (pour le thème sombre)
293
+ # #333333 : Gris très foncé, presque noir (pour le thème clair)
294
+
295
+ # Utilisation d'un contexte 'with' pour la barre latérale
296
+ with st.sidebar:
297
+ # Ajout du logo (identique pour les deux thèmes)
298
+ st.image("assets/logo.png", width=150)
299
+
300
+ # Ajout du titre avec la couleur appropriée
301
+ st.markdown(
302
+ f"""
303
+ <h2 style="color:{text_color};">Colibri Assistant</h2>
304
+ """,
305
+ unsafe_allow_html=True
306
+ )
307
+
308
+ # Ajout d'une ligne de séparation
309
+ st.write("---")
310
+
311
+
312
+ def apply_chat_styles(theme):
313
+ light_user_bg = "#d1e7ff" # bleu clair désaturé (arrière-plan des messages utilisateur en thème clair)
314
+ light_assistant_bg = "#f0f0f0" # Gris perle (arrière-plan des messages assistant en thème clair)
315
+ dark_user_bg = "#0E4C92" # #0E4C92 = bleu foncé (arrière-plan des messages utilisateur en thème sombre)
316
+ dark_assistant_bg = "#1A472A" # #1A472A = vert foncé (arrière-plan des messages assistant en thème sombre)
317
+
318
+ user_bg = dark_user_bg if theme == "Sombre" else light_user_bg # Utilise la couleur bleu foncé en thème sombre ou bleu clair désaturé en thème clair pour l'utilisateur
319
+ assistant_bg = dark_assistant_bg if theme == "Sombre" else light_assistant_bg # Utilise la couleur vert foncé en thème sombre ou Gris perle en thème clair pour l'assistant
320
+ text_color = "#E0E0E0" if theme == "Sombre" else "#000000" # Utilise la couleur gris clair (#E0E0E0) en thème sombre ou noir (#000000) en thème clair pour le texte
321
+
322
+ st.markdown(f"""
323
+ <style>
324
+ [data-testid="stChatMessage"] {{
325
+ width: fit-content !important;
326
+ max-width: 80% !important;
327
+ padding: 10px !important;
328
+ border-radius: 15px !important;
329
+ margin-bottom: 10px !important;
330
+ clear: both !important;
331
+ }}
332
+
333
+ [data-testid="stChatMessage"]:has([data-testid="stChatMessageAvatarUser"]) {{
334
+ background-color: {user_bg} !important; # Arrière-plan des messages utilisateur (dépend du thème)
335
+ border-radius: 15px 15px 0 15px !important;
336
+ margin-left: auto !important;
337
+ margin-right: 10px !important;
338
+ text-align: right !important;
339
+ float: right !important;
340
+ }}
341
+
342
+ [data-testid="stChatMessage"]:not(:has([data-testid="stChatMessageAvatarUser"])) {{
343
+ background-color: {assistant_bg} !important; # Arrière-plan des messages assistant (dépend du thème)
344
+ border-radius: 15px 15px 15px 0 !important;
345
+ margin-right: auto !important;
346
+ margin-left: 10px !important;
347
+ text-align: left !important;
348
+ float: left !important;
349
+ }}
350
+
351
+ [data-testid="stChatMessage"] * {{
352
+ color: {text_color} !important; # Couleur du texte (dépend du thème)
353
+ }}
354
+
355
+ </style>
356
+ """, unsafe_allow_html=True)
357
+
358
+ def inject_custom_toggle_css(theme):
359
+ # Définition de la couleur du texte en fonction du thème
360
+ text_color = "#e0e0e0" if theme == "Sombre" else "#666666"
361
+ # #e0e0e0 : Gris très clair, presque blanc (pour le thème sombre)
362
+ # #666666 : Gris moyen foncé (pour le thème clair)
363
+
364
+ # CSS personnalisé pour le style des toggles
365
+ custom_css = f"""
366
+ <style>
367
+ /* Style pour le titre du toggle */
368
+ .stExpander .stToggleButton label,
369
+ .streamlit-expanderHeader .stToggleButton label {{
370
+ color: {text_color} !important;
371
+ font-weight: bold;
372
+ }}
373
+ </style>
374
+ """
375
+ # Injection du CSS personnalisé dans la page Streamlit
376
+ st.markdown(custom_css, unsafe_allow_html=True)
377
+
378
+
379
+ def create_custom_footer():
380
+ footer = """
381
+ <style>
382
+ .footer {
383
+ position: fixed;
384
+ left: 0;
385
+ bottom: 0;
386
+ width: 100%;
387
+ background-color: #0E1117; # Couleur de fond sombre, ajustez selon votre thème
388
+ color: #FAFAFA; # Couleur du texte claire
389
+ text-align: center;
390
+ padding: 10px;
391
+ font-size: 14px;
392
+ }
393
+ </style>
394
+ <div class="footer">
395
+ <p>© 2024 Votre Entreprise. Tous droits réservés.</p>
396
+ </div>
397
+ """
398
+ st.markdown(footer, unsafe_allow_html=True)
399
+
400
+ # Fonction pour permettre à l'utilisateur de changer de thème avec un toggle
401
+ def theme_switcher():
402
+ current_theme = st.session_state.get('user_theme', 'Clair')
403
+
404
+ # Utiliser des colonnes Streamlit pour l'alignement
405
+ col1, col2 = st.columns([0.9, 4]) # Ajustez les ratios selon vos besoins
406
+
407
+ with col1:
408
+ # Toggle sans label
409
+ is_dark = st.toggle("", value=current_theme == "Sombre", key=f"theme_toggle", label_visibility="collapsed")
410
+
411
+ with col2:
412
+ # Label personnalisé
413
+ title_color = "#FF6B6B" if current_theme == "Sombre" else "#1E90FF"
414
+ st.markdown(f'<p style="color: {title_color}; font-weight: normal; margin-top: 5px;">Thème sombre</i>', unsafe_allow_html=True)
415
+
416
+ new_theme = "Sombre" if is_dark else "Clair"
417
+
418
+ if new_theme != current_theme:
419
+ st.session_state.user_theme = new_theme
420
+ save_theme_preference(new_theme)
421
+ apply_theme(new_theme)
422
+ apply_chat_styles(new_theme)
423
+ st.rerun()
424
+
425
+ # Cette fonction supprimera certains styles par défaut de Streamlit qui pourraient interférer avec vos styles personnalisés.
426
+ def remove_streamlit_style():
427
+ st.markdown("""
428
+ <style>
429
+ #MainMenu {visibility: hidden;}
430
+ footer {visibility: hidden;}
431
+ </style>
432
+ """, unsafe_allow_html=True)
433
+ # Appelez cette fonction au début de votre improved_ui
434
+
435
+
436
+ def force_streamlit_style_update():
437
+ st.markdown("""
438
+ <script>
439
+ // Force Streamlit to re-evaluate the styles
440
+ window.parent.postMessage({
441
+ type: "streamlit:setComponentValue",
442
+ value: true
443
+ }, "*")
444
+ </script>
445
+ """, unsafe_allow_html=True
446
+ )
447
+
448
+ # Fonction pour cacher les pages de navigation spécifiques (voir dossier pages)
449
+ def hide_pages():
450
+ hide_pages_style = """
451
+ <style>
452
+ /* Cache les liens de navigation spécifiques */
453
+ .st-emotion-cache-79elbk .st-emotion-cache-1ekxtbt li:nth-child(2), /* admin page */
454
+ .st-emotion-cache-79elbk .st-emotion-cache-1ekxtbt li:nth-child(3) { /* reset password */
455
+ display: none !important;
456
+ }
457
+
458
+ /* Cache le séparateur si nécessaire */
459
+ .st-emotion-cache-uzxc3z {
460
+ display: none !important;
461
+ }
462
+
463
+ /* Pour s'assurer que tous les liens sont cachés même si les classes changent */
464
+ [data-testid="stSidebarNavItems"] li a[href*="/admin_page"],
465
+ [data-testid="stSidebarNavItems"] li a[href*="/reset_password"] {
466
+ display: none !important;
467
+ }
468
+
469
+ /* Cache les conteneurs parents si nécessaire */
470
+ [data-testid="stSidebarNavItems"] li:has(a[href*="/admin_page"]),
471
+ [data-testid="stSidebarNavItems"] li:has(a[href*="/reset_password"]) {
472
+ display: none !important;
473
+ }
474
+ </style>
475
+ """
476
+ st.markdown(hide_pages_style, unsafe_allow_html=True)
477
+
478
+
479
+ # styles pour le dashboard administrateur (tooltip et bouttons)
480
+ def apply_tooltip_and_button_styles(theme):
481
+ # Définition des couleurs en fonction du thème
482
+ text_color = "#E0E0E0" if theme == "Sombre" else "#333333"
483
+ background_color = "#333333" if theme == "Sombre" else "#fff"
484
+ border_color = "#ccc" if theme == "Sombre" else "#333"
485
+ button_background_color = "#4682B4" if theme == "Sombre" else "#FFD700"
486
+ button_text_color = "white" if theme == "Sombre" else "#8FBC8F"
487
+
488
+ # Injection des styles CSS pour les tooltips et le bouton de soumission du formulaire
489
+ st.markdown(
490
+ f"""
491
+ <style>
492
+ /* Style pour le conteneur principal du tooltip */
493
+ .st-emotion-cache-oj1fi {{
494
+ background-color: {background_color} !important;
495
+ color: {text_color} !important;
496
+ border: 1px solid {border_color} !important;
497
+ }}
498
+
499
+ /* Style pour le contenu du tooltip */
500
+ .stTooltipIcon {{
501
+ background-color: {background_color} !important;
502
+ color: {text_color} !important;
503
+ }}
504
+
505
+ /* Style pour le texte du tooltip */
506
+ .st-emotion-cache-1whk732 {{
507
+ color: {text_color} !important;
508
+ }}
509
+
510
+ /* Style pour l'icône du tooltip */
511
+ .stTooltipIcon svg {{
512
+ stroke: {text_color} !important;
513
+ }}
514
+
515
+ /* Style pour la couche externe du popup */
516
+ div[data-baseweb="tooltip"] {{
517
+ background-color: {background_color} !important;
518
+ border: 1px solid {border_color} !important;
519
+ padding: 5px 10px !important;
520
+ border-radius: 4px !important;
521
+ font-size: 12px !important;
522
+ z-index: 999999 !important;
523
+ }}
524
+
525
+ /* Style pour la couche interne du popup et son contenu */
526
+ div[data-baseweb="tooltip"] > div,
527
+ div[data-baseweb="tooltip"] > div > div {{
528
+ background-color: {background_color} !important;
529
+ color: {text_color} !important;
530
+ }}
531
+
532
+ /* Force la couleur du texte sur toutes les couches possibles */
533
+ div[data-baseweb="tooltip"] *,
534
+ div[role="tooltip"] * {{
535
+ color: {text_color} !important;
536
+ background-color: {background_color} !important;
537
+ }}
538
+
539
+ /* Style pour le bouton de soumission du formulaire */
540
+ div[data-testid="stFormSubmitButton"] button {{
541
+ background-color: {button_background_color} !important;
542
+ color: {button_text_color} !important;
543
+ border: none !important;
544
+ padding: 0.5rem 1rem !important;
545
+ border-radius: 0.5rem !important;
546
+ font-weight: bold !important;
547
+ transition: all 0.3s ease !important;
548
+ }}
549
+
550
+ /* Style pour le bouton de soumission du formulaire lorsqu'il est survolé */
551
+ div[data-testid="stFormSubmitButton"] button:hover {{
552
+ border: 1px solid red !important; # Ajoutez cette ligne pour définir la bordure rouge au survol
553
+ opacity: 0.8 !important;
554
+ }}
555
+ </style>
556
+ """,
557
+ unsafe_allow_html=True
558
+ )