ernestmindres commited on
Commit
a5248df
·
verified ·
1 Parent(s): bc3c8fb

Update baserow_storage.py

Browse files
Files changed (1) hide show
  1. baserow_storage.py +491 -583
baserow_storage.py CHANGED
@@ -1,584 +1,492 @@
1
- # baserow_storage.py
2
-
3
- import os
4
- import requests
5
- import json
6
- import sys
7
- from datetime import datetime
8
- from typing import Optional, Dict
9
- import logging
10
- # Configuration du logger (ajoutez ceci en haut du fichier)
11
- logger = logging.getLogger(__name__)
12
- # --- Configuration Baserow (Doit être défini dans les secrets) ---
13
- HEALTH_CHECK_URL = "https://api.baserow.io/api/database/rows/table/"
14
-
15
- # 2. URL de BASE CORRECTE pour la construction des requêtes de données (connexion, inscription, etc.)
16
- DATA_BASE_URL = "https://api.baserow.io/api/database/rows/"
17
- API_TOKEN = os.environ.get("BASEROW_API_TOKEN")
18
-
19
- # Les IDs de table seront récupérés depuis les variables d'environnement
20
- PRIMARY_USERS_TABLE_ID = os.environ.get("PRIMARY_USERS_TABLE_ID")
21
- END_USERS_TABLE_ID = os.environ.get("END_USERS_TABLE_ID")
22
-
23
- # Headers pour l'authentification
24
- HEADERS = {
25
- "Authorization": f"Token {API_TOKEN}",
26
- "Content-Type": "application/json"
27
- }
28
-
29
- # ----------------------------------------------------------------------
30
- # --- Noms de Colonnes pour la Table des Utilisateurs Principaux (Primary Users) ---
31
- # ----------------------------------------------------------------------
32
- FIELD_ID = 'ID' # Correspond à 'user_id' dans le code
33
- FIELD_EMAIL = 'Email' # Correspond à 'email'
34
- FIELD_USERNAME = 'Nom d\'utilisateur' # Correspond à 'username'
35
- FIELD_PASSWORD_HASH = 'Hachage Mot de Passe' # Correspond à 'password_hash'
36
- FIELD_API_KEY = 'Clé API' # Correspond à 'api_key'
37
- FIELD_API_KEY_2 = 'Clé API 2'
38
- FIELD_API_KEY_3 = 'Clé API 3'
39
- FIELD_API_KEY_4 = 'Clé API 4'
40
- FIELD_API_KEY_5 = 'Clé API 5'
41
- FIELD_SECURITY_Q = 'Question de Sécurité'
42
- FIELD_SECURITY_A_HASH = 'Hachage Réponse Secrète'
43
- FIELD_PLAN_ID = 'Plan ID'
44
- FIELD_STRIPE_SUB_ID = 'ID Abonnement Stripe'
45
- FIELD_DATE_CREATION = 'Date Création'
46
- FIELD_DATE_PLAN_START = 'Date Plan Start'
47
- FIELD_API_CALLS_MONTH = 'API Calls Month' # À vérifier avec votre nom exact dans Baserow!
48
- FIELD_STATUS = 'Status'
49
- FIELD_END_USER_ID = 'ID Utilisateur Final' # Correspond à 'end_user_id'
50
- FIELD_END_USER_IDENTIFIER = 'Identifiant' # Correspond à 'identifier'
51
- FIELD_END_USER_METADATA = 'Métadonnées' # Correspond à 'metadata'
52
- FIELD_CLIENT_ID_LINK = 'ID Client Principal' # Lien vers Primary_Users
53
-
54
-
55
- # ----------------------------------------------------------------------
56
- # --- Noms de Colonnes pour la Table des Utilisateurs Finaux (End Users) ---
57
- # ----------------------------------------------------------------------
58
- # Ces champs sont spécifiques à la table END_USERS
59
- FIELD_END_USER_ID = 'ID Utilisateur Final'
60
- FIELD_END_USER_IDENTIFIER = 'Identifiant' # Champ pour compatibilité ou recherche
61
- FIELD_END_USER_EMAIL = 'Email' # NOUVEAU
62
- FIELD_END_USER_USERNAME = 'Nom d\'utilisateur' # NOUVEAU
63
- FIELD_END_USER_SECURITY_Q = 'Question de Sécurité' # NOUVEAU (Peut être différent de Primary)
64
- FIELD_END_USER_SECURITY_A_HASH = 'Hachage Réponse Secrète' # NOUVEAU
65
- FIELD_END_USER_STATUS = 'Statut' # NOUVEAU
66
- FIELD_END_USER_METADATA = 'Métadonnées'
67
- FIELD_PASSWORD_HASH_END_USER = 'Hachage Mot de Passe End User' # Renommer pour éviter le conflit si possible
68
- FIELD_CLIENT_ID_LINK = 'ID Client Principal'
69
-
70
- def _get_table_url(table_id: str) -> str:
71
- """Construit l'URL d'API pour une table donnée (avec le bon endpoint)."""
72
- return f"{DATA_BASE_URL}table/{table_id}/"
73
-
74
- def _baserow_record_to_user(record: Dict, is_end_user: bool) -> Dict:
75
- """
76
- Convertit un enregistrement Baserow (avec noms de champs utilisateur)
77
- en format de dictionnaire Python attendu par le backend.
78
- """
79
-
80
- # Si c'est un utilisateur final, utilisez la logique existante pour l'utilisateur final.
81
- if is_end_user:
82
- user_data = {
83
- # Champs communs / End Users
84
- 'baserow_row_id': record['id'],
85
- # Note: J'utilise FIELD_DATE_CREATION mais vous pouvez définir FIELD_END_USER_DATE_CREATION si Baserow a un nom de colonne différent.
86
- 'date_creation': record.get(FIELD_DATE_CREATION),
87
- # End User specific fields (Ajout des NOUVEAUX champs)
88
- 'end_user_id': record.get(FIELD_END_USER_ID),
89
- 'identifier': record.get(FIELD_END_USER_IDENTIFIER),
90
- 'email': record.get(FIELD_END_USER_EMAIL), # NOUVEAU
91
- 'username': record.get(FIELD_END_USER_USERNAME), # NOUVEAU
92
- 'security_question': record.get(FIELD_END_USER_SECURITY_Q), # NOUVEAU
93
- 'security_answer_hash': record.get(FIELD_END_USER_SECURITY_A_HASH), # NOUVEAU
94
- 'status': record.get(FIELD_END_USER_STATUS), # NOUVEAU
95
-
96
- # CORRECTION CRUCIALE : Utilisation du nom de champ correct pour l'End User
97
- 'password_hash': record.get(FIELD_PASSWORD_HASH_END_USER),
98
-
99
- # Le lien est un tableau, on extrait la valeur 'user_id' du client principal
100
- 'client_user_id': record.get(FIELD_CLIENT_ID_LINK)[0]['value'] if record.get(FIELD_CLIENT_ID_LINK) and record.get(FIELD_CLIENT_ID_LINK)[0]['value'] else None,
101
- # Les métadonnées
102
- 'metadata': record.get(FIELD_END_USER_METADATA),
103
- }
104
- # Nettoyage des clés None ou non-pertinentes
105
- return {k: v for k, v in user_data.items() if v is not None}
106
-
107
- # LOGIQUE POUR L'UTILISATEUR PRINCIPAL (PRIMARY USER)
108
-
109
- # 1. Récupération des champs individuels
110
- user_data = {
111
- # Champs communs / Primary Users
112
- 'baserow_row_id': record['id'], # ID interne de la ligne Baserow (pour les mises à jour)
113
- 'date_creation': record.get(FIELD_DATE_CREATION),
114
-
115
- # Primary User specific fields
116
- 'user_id': record.get(FIELD_ID),
117
- 'email': record.get(FIELD_EMAIL),
118
- 'username': record.get(FIELD_USERNAME),
119
- 'password_hash': record.get(FIELD_PASSWORD_HASH),
120
-
121
- # Récupération des 5 clés individuelles (pour l'authentification par clé)
122
- 'api_key': record.get(FIELD_API_KEY),
123
- 'api_key_2': record.get(FIELD_API_KEY_2),
124
- 'api_key_3': record.get(FIELD_API_KEY_3),
125
- 'api_key_4': record.get(FIELD_API_KEY_4),
126
- 'api_key_5': record.get(FIELD_API_KEY_5),
127
-
128
- 'security_question': record.get(FIELD_SECURITY_Q),
129
- 'security_answer_hash': record.get(FIELD_SECURITY_A_HASH),
130
- 'plan_id': record.get(FIELD_PLAN_ID),
131
- 'stripe_subscription_id': record.get(FIELD_STRIPE_SUB_ID),
132
- 'date_plan_start': record.get(FIELD_DATE_PLAN_START),
133
- }
134
-
135
- # 2. ÉTAPE CRUCIALE AJOUTÉE : Création de la liste 'api_keys' pour l'affichage
136
- # Cette liste est nécessaire pour que la boucle dans api_key.html fonctionne correctement.
137
- user_data['api_keys'] = [
138
- user_data['api_key'],
139
- user_data['api_key_2'],
140
- user_data['api_key_3'],
141
- user_data['api_key_4'],
142
- user_data['api_key_5'],
143
- ]
144
-
145
- # Nettoyage des clés None ou non-pertinentes
146
- return {k: v for k, v in user_data.items() if v is not None}
147
-
148
-
149
- def _user_to_baserow_data(user_data: Dict, is_end_user: bool) -> Dict:
150
- """
151
- Convertit le format de dictionnaire Python du backend en format
152
- JSON attendu par l'API Baserow (avec noms de champs utilisateur).
153
- """
154
- if is_end_user:
155
- # End User fields (Ajout des NOUVEAUX champs)
156
- baserow_data = {
157
- FIELD_END_USER_ID: user_data.get('end_user_id'),
158
- FIELD_END_USER_IDENTIFIER: user_data.get('identifier'),
159
- FIELD_END_USER_EMAIL: user_data.get('email'), # NOUVEAU
160
- FIELD_END_USER_USERNAME: user_data.get('username'), # NOUVEAU
161
- FIELD_END_USER_SECURITY_Q: user_data.get('security_question'), # NOUVEAU
162
- FIELD_END_USER_SECURITY_A_HASH: user_data.get('security_answer_hash'), # NOUVEAU
163
- FIELD_END_USER_STATUS: user_data.get('status'), # NOUVEAU
164
-
165
- # CORRECTION CRUCIALE : Utilisation du nom de champ correct pour l'End User
166
- FIELD_PASSWORD_HASH_END_USER: user_data.get('password_hash'),
167
-
168
- FIELD_END_USER_METADATA: user_data.get('metadata'),
169
- FIELD_DATE_CREATION: user_data.get('date_creation'),
170
- # Le lien vers Primary_Users est géré dans save_end_user_data
171
- }
172
- else:
173
- # Primary User fields
174
- baserow_data = {
175
- FIELD_ID: user_data.get('user_id'),
176
- FIELD_EMAIL: user_data.get('email'),
177
- FIELD_USERNAME: user_data.get('username'),
178
- FIELD_PASSWORD_HASH: user_data.get('password_hash'),
179
- FIELD_API_KEY: user_data.get('api_key'),
180
- FIELD_API_KEY_2: user_data.get('api_key_2'),
181
- FIELD_API_KEY_3: user_data.get('api_key_3'),
182
- FIELD_API_KEY_4: user_data.get('api_key_4'),
183
- FIELD_API_KEY_5: user_data.get('api_key_5'),
184
- FIELD_SECURITY_Q: user_data.get('security_question'),
185
- FIELD_SECURITY_A_HASH: user_data.get('security_answer_hash'),
186
- FIELD_PLAN_ID: user_data.get('plan_id'),
187
- FIELD_STRIPE_SUB_ID: user_data.get('stripe_subscription_id'),
188
- FIELD_DATE_CREATION: user_data.get('date_creation'),
189
- FIELD_DATE_PLAN_START: user_data.get('date_plan_start'),
190
- FIELD_API_CALLS_MONTH: user_data.get('api_calls_month', 0),
191
- FIELD_STATUS: user_data.get('status', 'Active') # Assurez-vous que 'Active' est une option valide dans Baserow
192
- }
193
-
194
- # Suppression des clés non-valorisées (None)
195
- return {k: v for k, v in baserow_data.items() if v is not None}
196
-
197
-
198
- def _get_single_user_record(table_id: str, field_name: str, value: str, is_end_user: bool) -> Optional[Dict]:
199
- """Fonction générique pour rechercher un seul enregistrement par un champ (filtrage Baserow)."""
200
- url = _get_table_url(table_id)
201
- # Utilisation du paramètre de filtre de Baserow pour une recherche indexée (plus rapide)
202
- filter_param = f"filter__{field_name}__equal={value}"
203
-
204
- try:
205
- response = requests.get(
206
- f"{url}?user_field_names=true&{filter_param}",
207
- headers=HEADERS
208
- )
209
- response.raise_for_status()
210
-
211
- data = response.json()
212
- if data and data.get('results'):
213
- # On ne prend que le premier résultat (car ID/Email/API Key sont uniques)
214
- return _baserow_record_to_user(data['results'][0], is_end_user)
215
- return None
216
-
217
- except requests.exceptions.RequestException as e:
218
- print(f"Erreur de Baserow lors de la recherche par filtre {field_name}: {e}", file=sys.stderr)
219
- return None
220
-
221
- # ----------------------------------------------------------------------
222
- # --- Fonctions CRUD Primary_Users (Nouveau et Remplacement) ---
223
- # ----------------------------------------------------------------------
224
-
225
- def get_user_by_email(email: str) -> Optional[Dict]:
226
- """Recherche un utilisateur principal par son adresse Email."""
227
- return _get_single_user_record(PRIMARY_USERS_TABLE_ID, FIELD_EMAIL, email, is_end_user=False)
228
-
229
- def get_client_user_by_api_key(api_key: str) -> Optional[Dict]:
230
- """Recherche un utilisateur principal par sa Clé API."""
231
- return _get_single_user_record(PRIMARY_USERS_TABLE_ID, FIELD_API_KEY, api_key, is_end_user=False)
232
-
233
- # Remplacement de l'ancien load_primary_user_data(user_id)
234
- def load_primary_user_data(user_id: str) -> Optional[Dict]:
235
- """Recherche un utilisateur principal par son ID (user_id)."""
236
- return _get_single_user_record(PRIMARY_USERS_TABLE_ID, FIELD_ID, user_id, is_end_user=False)
237
-
238
-
239
- def save_primary_user_data(user_data: Dict, commit_msg: str = "") -> bool:
240
- """Crée ou met à jour un utilisateur principal, avec détection d'erreur ultra-précise."""
241
- row_id = user_data.get('baserow_row_id')
242
-
243
- # Définition de l'URL de base pour la table des utilisateurs principaux
244
- url = _get_table_url(PRIMARY_USERS_TABLE_ID)
245
-
246
- # 1. Conversion des données
247
- baserow_data = _user_to_baserow_data(user_data, is_end_user=False)
248
-
249
- # 2. Suppression des champs en lecture seule (comme dans la correction précédente)
250
- if baserow_data.pop(FIELD_ID, None):
251
- print(f"DEBUG: Suppression du champ '{FIELD_ID}' (UUID auto) avant l'envoi { 'POST' if not row_id else 'PATCH'}.", file=sys.stderr)
252
-
253
- try:
254
- # Détermination de l'action (PATCH ou POST)
255
- if row_id:
256
- action = "PATCH" # ⬅️ CORRECTION: Définition de 'action'
257
- # MISE À JOUR (PATCH)
258
- response = requests.patch(
259
- f"{url}{row_id}/?user_field_names=true",
260
- headers=HEADERS,
261
- json=baserow_data
262
- )
263
- else:
264
- action = "POST" # ⬅️ CORRECTION: Définition de 'action'
265
- # CRÉATION (POST)
266
- response = requests.post(
267
- f"{url}?user_field_names=true", # ⬅️ CORRECTION: Utilise l'URL de table 'url'
268
- headers=HEADERS,
269
- json=baserow_data
270
- )
271
-
272
- # Déclenche une exception requests.exceptions.HTTPError pour les statuts 4xx/5xx
273
- response.raise_for_status()
274
-
275
- # Succès
276
- if not row_id:
277
- new_record = response.json()
278
- # 1. Mettre à jour l'ID de ligne Baserow
279
- user_data['baserow_row_id'] = new_record.get('id')
280
- # 2. Mettre à jour l'UUID de l'utilisateur (généré par Baserow)
281
- user_data['user_id'] = new_record.get(FIELD_ID)
282
-
283
- print(f"DEBUG: UUID de l'utilisateur généré par Baserow et enregistré: {user_data['user_id']}", file=sys.stderr)
284
-
285
- print(f"DEBUG: Baserow Primary User action '{action}' réussie. Row ID: {user_data.get('baserow_row_id')}. Message: {commit_msg}", file=sys.stderr)
286
- return True
287
-
288
- except requests.exceptions.RequestException as e:
289
- # --- BLOC DE DÉTECTION D'ERREUR PRÉCISE (Ultra-Complet) ---
290
-
291
- # Note: 'action' est définie dans le bloc try/except, mais si l'erreur survient
292
- # AVANT la définition de 'action', nous devons la gérer.
293
- # Pour être sûr, nous allons la définir ici par défaut si elle n'existe pas.
294
- if 'action' not in locals():
295
- action = "INCONNU"
296
-
297
- error_message = f"🚨 ÉCHEC: Erreur lors de la sauvegarde/mise à jour du Primary User dans Baserow. Requête: {action}"
298
- error_details = ""
299
-
300
- if hasattr(e, 'response') and e.response is not None:
301
- # 1. Statut HTTP et URL
302
- error_details += f"\n -> STATUT HTTP: {e.response.status_code} ({e.response.reason})"
303
- error_details += f"\n -> URL de la requête: {e.response.url}"
304
-
305
- # 2. Tenter de décoder le corps de la réponse en JSON (contient les erreurs Baserow)
306
- try:
307
- response_json = e.response.json()
308
- error_details += f"\n\n -> ERREUR BASEROW DÉTAILLÉE (JSON):\n{json.dumps(response_json, indent=4)}"
309
-
310
- # Optionnel: Synthèse des erreurs de validation de champ
311
- if isinstance(response_json, dict):
312
- validation_errors = {k: v for k, v in response_json.items() if isinstance(v, list) and k != 'detail'}
313
- if validation_errors:
314
- error_details += "\n -> SYNTHÈSE DES CHAMPS INVALIDES (Vérifiez les noms de colonnes/IDs de table!):"
315
- for field_name, errors in validation_errors.items():
316
- error_details += f"\n - Champ '{field_name}': {', '.join([err.get('error', 'Erreur inconnue') for err in errors])}"
317
-
318
- except json.JSONDecodeError:
319
- # 3. Si le corps de la réponse n'est pas du JSON
320
- error_details += f"\n\n -> ERREUR BRUTE (Réponse non-JSON):\n{e.response.text[:500]}..."
321
-
322
- # 4. Afficher les données que nous avons tenté d'envoyer (après la suppression de l'ID si c'était une création)
323
- error_details += f"\n\n -> DONNÉES ENVOYÉES À BASEROW:\n{json.dumps(baserow_data, indent=4)}"
324
-
325
- # Log complet de l'erreur
326
- print(error_message + error_details, file=sys.stderr)
327
-
328
- return False
329
-
330
-
331
- # ----------------------------------------------------------------------
332
- # --- Fonctions CRUD End_Users (Remplacement) ---
333
- # ----------------------------------------------------------------------
334
-
335
- # baserow_storage.py : Dans la section CRUD End_Users
336
-
337
- def get_end_user_by_email(client_user_id: str, email: str) -> Optional[Dict]:
338
- """Recherche un utilisateur final par son Email, lié à un client principal (scope)."""
339
- client_row_id = _get_client_baserow_row_id(client_user_id)
340
- if not client_row_id:
341
- print(f"Avertissement: Client Principal {client_user_id} introuvable pour la recherche de l'utilisateur final.", file=sys.stderr)
342
- return None
343
-
344
- url = _get_table_url(END_USERS_TABLE_ID)
345
-
346
- # Filtre 1: Email de l'utilisateur final = email
347
- # Filtre 2: Lien vers le Client Principal = client_row_id
348
- filter_params = f"filter__{FIELD_END_USER_EMAIL}__equal={email}&filter__{FIELD_CLIENT_ID_LINK}__link_row_id={client_row_id}"
349
-
350
- try:
351
- response = requests.get(
352
- f"{url}?user_field_names=true&{filter_params}",
353
- headers=HEADERS
354
- )
355
- response.raise_for_status()
356
-
357
- data = response.json()
358
- if data and data.get('results'):
359
- # On ne prend que le premier résultat
360
- return _baserow_record_to_user(data['results'][0], is_end_user=True)
361
- return None
362
-
363
- except requests.exceptions.RequestException as e:
364
- print(f"Erreur Baserow lors de la recherche End User par Email: {e}", file=sys.stderr)
365
- return None
366
-
367
- def _get_client_baserow_row_id(client_user_id: str) -> Optional[int]:
368
- """Récupère l'ID de ligne interne Baserow du client principal pour le lien."""
369
- client_user = load_primary_user_data(client_user_id) # utilise la fonction déjà créée
370
- return client_user.get('baserow_row_id') if client_user else None
371
-
372
-
373
- # Remplacement de l'ancien load_end_user_data(client_user_id, end_user_id)
374
- def load_end_user_data(client_user_id: str, end_user_id: str) -> Optional[Dict]:
375
- """Recherche un utilisateur final par ID, lié à un client principal (recherche indexée)."""
376
- client_row_id = _get_client_baserow_row_id(client_user_id)
377
- if not client_row_id:
378
- return None
379
-
380
- url = _get_table_url(END_USERS_TABLE_ID)
381
-
382
- # Filtre 1: ID Utilisateur Final = end_user_id
383
- # Filtre 2: ID Client Principal (Lien) = client_row_id (Lien vers une autre table)
384
- filter_params = f"filter__{FIELD_END_USER_ID}__equal={end_user_id}&filter__{FIELD_CLIENT_ID_LINK}__link_row_id={client_row_id}"
385
-
386
- try:
387
- response = requests.get(
388
- f"{url}?user_field_names=true&{filter_params}",
389
- headers=HEADERS
390
- )
391
- response.raise_for_status()
392
-
393
- data = response.json()
394
- if data and data.get('results'):
395
- return _baserow_record_to_user(data['results'][0], is_end_user=True)
396
- return None
397
-
398
- except requests.exceptions.RequestException as e:
399
- print(f"Erreur Baserow lors de la recherche End User: {e}", file=sys.stderr)
400
- return None
401
-
402
-
403
- # Remplacement de l'ancien save_end_user_data(end_user_data)
404
- def save_end_user_data(end_user_data: Dict, commit_msg: str = "") -> bool:
405
- """Crée ou met à jour un utilisateur final."""
406
- row_id = end_user_data.get('baserow_row_id')
407
- client_user_id = end_user_data.get('client_user_id')
408
-
409
- # Étape cruciale: Trouver l'ID de ligne Baserow du client principal
410
- client_row_id = _get_client_baserow_row_id(client_user_id)
411
- if not client_row_id:
412
- print(f"Erreur: Client Principal {client_user_id} introuvable pour la sauvegarde de l'utilisateur final.", file=sys.stderr)
413
- return False
414
-
415
- url = _get_table_url(END_USERS_TABLE_ID)
416
-
417
- # 1. Conversion des données
418
- baserow_data = _user_to_baserow_data(end_user_data, is_end_user=True)
419
-
420
- # Ajout du lien vers le client principal (obligatoire pour la création/mise à jour)
421
- # L'API Baserow pour les champs de type 'Lien vers une autre table' attend une liste d'ID de ligne.
422
- baserow_data[FIELD_CLIENT_ID_LINK] = [client_row_id]
423
-
424
- try:
425
- if row_id:
426
- # MISE À JOUR (PATCH)
427
- response = requests.patch(
428
- f"{url}{row_id}/?user_field_names=true",
429
- headers=HEADERS,
430
- json=baserow_data
431
- )
432
- else:
433
- # CRÉATION (POST)
434
- response = requests.post(
435
- f"{url}?user_field_names=true",
436
- headers=HEADERS,
437
- json=baserow_data
438
- )
439
-
440
- response.raise_for_status()
441
-
442
- if not row_id and response.status_code == 200:
443
- new_record = response.json()
444
- end_user_data['baserow_row_id'] = new_record.get('id')
445
-
446
- print(f"DEBUG: Baserow End User action réussie. Row ID: {row_id or new_record.get('id')}. Message: {commit_msg}", file=sys.stderr)
447
- return True
448
-
449
- except requests.exceptions.RequestException as e:
450
- print(f"Erreur lors de la sauvegarde/mise à jour du End User dans Baserow: {e}", file=sys.stderr)
451
- return False
452
-
453
- def check_baserow_connection() -> str:
454
- """
455
- Vérifie l'état de connexion de la base de données Baserow.
456
- Retourne 'operational' ou 'outage'.
457
- """
458
- # Liste des IDs de tables critiques à vérifier
459
- CRITICAL_TABLE_IDS = [
460
- PRIMARY_USERS_TABLE_ID,
461
- END_USERS_TABLE_ID
462
- ]
463
-
464
- if not API_TOKEN:
465
- # Si le token API n'est pas défini, échec immédiat
466
- print("DEBUG: BASEROW_API_TOKEN manquant.", file=sys.stderr)
467
- return "outage"
468
-
469
- for table_id in CRITICAL_TABLE_IDS:
470
- if not table_id:
471
- # Si un des IDs de table critiques n'est pas défini, échec
472
- print(f"DEBUG: Un ID de table critique Baserow est manquant (ID: {table_id}).", file=sys.stderr)
473
- return "outage"
474
-
475
- # Tenter de faire un appel très léger (récupérer la première ligne)
476
- # On utilise page_size=1 pour minimiser la charge
477
- url = f"{DATA_BASE_URL}table/{table_id}/?page_size=1"
478
-
479
- try:
480
- response = requests.get(url, headers=HEADERS, timeout=5)
481
-
482
- if response.status_code != 200:
483
- # Si un 404, 403, ou autre erreur est retournée par Baserow pour CETTE table
484
- print(f"DEBUG: Baserow check failed for table {table_id} with status code {response.status_code}", file=sys.stderr)
485
- return "outage"
486
-
487
- except requests.exceptions.RequestException as e:
488
- # Erreur de réseau (timeout, DNS, etc.)
489
- print(f"DEBUG: Baserow connection error for table {table_id}: {e}", file=sys.stderr)
490
- return "outage"
491
-
492
- # Si toutes les tables critiques ont été vérifiées avec succès
493
- return "operational"
494
-
495
-
496
- def get_health_status() -> Dict:
497
- """
498
- Collecte l'état de santé de tous les services pour la page /statut.
499
- """
500
-
501
- db_status = check_baserow_connection()
502
-
503
- # L'état de l'authentification et de l'API principale sont
504
- # généralement liés à l'état de la DB pour une application simple.
505
- # Si la DB est HS, l'auth est HS. Sinon, ils sont OK.
506
-
507
- auth_status = db_status # Lié à la DB (pour charger les utilisateurs)
508
- api_endpoint_status = "operational" # L'endpoint Flask lui-même est considéré comme OK s'il tourne
509
-
510
- # Version du service (pour information)
511
- service_version = os.environ.get("SERVICE_VERSION", "1.0.0 (Baserow)")
512
-
513
-
514
- return {
515
- # Ces valeurs correspondent aux attributs 'data-status' dans statut.html
516
- "auth": auth_status,
517
- "data_storage": db_status,
518
- "api_endpoint": api_endpoint_status,
519
- "version": service_version,
520
- "last_update": datetime.now().strftime("%Y-%m-%d %H:%M:%S UTC")
521
- }
522
-
523
- def is_baserow_up() -> bool:
524
- """
525
- Vérifie l'état de Baserow en utilisant l'URL qui garantit un statut 100% fonctionnel
526
- sur Hugging Face, SANS utiliser la fonction de construction d'URL de table.
527
- """
528
- try:
529
- # Envoie une requête GET à l'URL qui répond positivement pour le health check.
530
- response = requests.get(
531
- HEALTH_CHECK_URL,
532
- headers=HEADERS,
533
- timeout=5
534
- )
535
- # On vérifie si la réponse est un succès (code 200).
536
- return response.status_code == 200
537
- except requests.exceptions.RequestException as e:
538
- print(f"DEBUG: Baserow health check failed: {e}")
539
- return False
540
-
541
- def log_baserow_api_call(method: str, url: str, headers: Dict, data: Optional[Dict] = None, log_response: bool = True):
542
- """
543
- Fonction utilitaire pour effectuer des appels API à Baserow et journaliser
544
- les requêtes et les réponses dans les logs du Space Hugging Face.
545
- """
546
-
547
- # 1. Journalisation de la requête
548
- logger.info(f"BASEROW REQUEST: {method} {url}")
549
- # ATTENTION: Ne pas logger le token API complet!
550
- logged_headers = {k: v.replace(API_TOKEN, '[TOKEN_MASKED]') if k == 'Authorization' else v for k, v in headers.items()}
551
- logger.debug(f"BASEROW REQUEST Headers: {logged_headers}")
552
- if data:
553
- # Pour les requêtes POST/PUT, logger les données (sans le hash du mot de passe si possible)
554
- logged_data = data.copy() if isinstance(data, dict) else data
555
- if isinstance(logged_data, dict) and 'Hachage du mot de passe' in logged_data:
556
- logged_data['Hachage du mot de passe'] = '[PASSWORD_HASH_MASKED]'
557
- logger.debug(f"BASEROW REQUEST Body: {logged_data}")
558
-
559
- # 2. Exécution de la requête
560
- try:
561
- if method == "GET":
562
- response = requests.get(url, headers=headers)
563
- elif method == "POST":
564
- response = requests.post(url, headers=headers, json=data)
565
- elif method == "PUT":
566
- response = requests.put(url, headers=headers, json=data)
567
- elif method == "DELETE":
568
- response = requests.delete(url, headers=headers)
569
- else:
570
- raise ValueError(f"Méthode HTTP non supportée: {method}")
571
-
572
- # 3. Journalisation de la réponse
573
- if log_response:
574
- logger.info(f"BASEROW RESPONSE: Status {response.status_code}")
575
- # Journaliser le contenu pour les erreurs
576
- if response.status_code >= 400:
577
- logger.error(f"BASEROW ERROR RESPONSE Body: {response.text}")
578
-
579
- # 4. Retourner la réponse
580
- return response
581
-
582
- except requests.exceptions.RequestException as e:
583
- logger.error(f"BASEROW CONNECTION ERROR: {e}")
584
  return None # Retourne None en cas d'erreur de connexion non gérée par le statut HTTP
 
1
+ # baserow_storage.py
2
+
3
+ import os
4
+ import requests
5
+ import json
6
+ import sys
7
+ from datetime import datetime
8
+ from typing import Optional, Dict
9
+ import logging
10
+ # Configuration du logger (ajoutez ceci en haut du fichier)
11
+ logger = logging.getLogger(__name__)
12
+ # --- Configuration Baserow (Doit être défini dans les secrets) ---
13
+ HEALTH_CHECK_URL = "https://api.baserow.io/api/database/rows/table/"
14
+
15
+ # 2. URL de BASE CORRECTE pour la construction des requêtes de données (connexion, inscription, etc.)
16
+ DATA_BASE_URL = "https://api.baserow.io/api/database/rows/"
17
+ API_TOKEN = os.environ.get("BASEROW_API_TOKEN")
18
+
19
+ # Les IDs de table seront récupérés depuis les variables d'environnement
20
+ PRIMARY_USERS_TABLE_ID = os.environ.get("PRIMARY_USERS_TABLE_ID")
21
+ END_USERS_TABLE_ID = os.environ.get("END_USERS_TABLE_ID")
22
+
23
+ # Headers pour l'authentification
24
+ HEADERS = {
25
+ "Authorization": f"Token {API_TOKEN}",
26
+ "Content-Type": "application/json"
27
+ }
28
+
29
+ # ----------------------------------------------------------------------
30
+ # --- Noms de Colonnes pour la Table des Utilisateurs Principaux (Primary Users) ---
31
+ # ----------------------------------------------------------------------
32
+ FIELD_ID = 'ID' # Correspond à 'user_id' dans le code
33
+ FIELD_EMAIL = 'Email' # Correspond à 'email'
34
+ FIELD_USERNAME = 'Nom d\'utilisateur' # Correspond à 'username'
35
+ FIELD_PASSWORD_HASH = 'Hachage Mot de Passe' # Correspond à 'password_hash'
36
+ FIELD_API_KEY = 'Clé API' # Correspond à 'api_key'
37
+ FIELD_API_KEY_2 = 'Clé API 2'
38
+ FIELD_API_KEY_3 = 'Clé API 3'
39
+ FIELD_API_KEY_4 = 'Clé API 4'
40
+ FIELD_API_KEY_5 = 'Clé API 5'
41
+ FIELD_SECURITY_Q = 'Question de Sécurité'
42
+ FIELD_SECURITY_A_HASH = 'Hachage Réponse Secrète'
43
+ FIELD_PLAN_ID = 'Plan ID'
44
+ FIELD_STRIPE_SUB_ID = 'ID Abonnement Stripe'
45
+ FIELD_DATE_CREATION = 'Date Création'
46
+ FIELD_DATE_PLAN_START = 'Date Plan Start'
47
+ FIELD_API_CALLS_MONTH = 'API Calls Month' # À vérifier avec votre nom exact dans Baserow!
48
+ FIELD_STATUS = 'Status'
49
+ FIELD_END_USER_ID = 'ID Utilisateur Final' # Correspond à 'end_user_id'
50
+ FIELD_END_USER_IDENTIFIER = 'Identifiant' # Correspond à 'identifier'
51
+ FIELD_END_USER_METADATA = 'Métadonnées' # Correspond à 'metadata'
52
+ FIELD_CLIENT_ID_LINK = 'ID Client Principal' # Lien vers Primary_Users
53
+
54
+
55
+ # ----------------------------------------------------------------------
56
+ # --- Noms de Colonnes pour la Table des Utilisateurs Finaux (End Users) ---
57
+ # ----------------------------------------------------------------------
58
+ # Ces champs sont spécifiques à la table END_USERS
59
+ FIELD_END_USER_ID = 'ID Utilisateur Final'
60
+ FIELD_END_USER_IDENTIFIER = 'Identifiant' # Champ pour compatibilité ou recherche
61
+ FIELD_END_USER_EMAIL = 'Email' # NOUVEAU
62
+ FIELD_END_USER_USERNAME = 'Nom d\'utilisateur' # NOUVEAU
63
+ FIELD_END_USER_SECURITY_Q = 'Question de Sécurité' # NOUVEAU (Peut être différent de Primary)
64
+ FIELD_END_USER_SECURITY_A_HASH = 'Hachage Réponse Secrète' # NOUVEAU
65
+ FIELD_END_USER_STATUS = 'Statut' # NOUVEAU
66
+ FIELD_END_USER_METADATA = 'Métadonnées'
67
+ FIELD_PASSWORD_HASH_END_USER = 'Hachage Mot de Passe End User' # Renommer pour éviter le conflit si possible
68
+ FIELD_CLIENT_ID_LINK = 'ID Client Principal'
69
+
70
+ def _get_table_url(table_id: str) -> str:
71
+ """Construit l'URL d'API pour une table donnée (avec le bon endpoint)."""
72
+ return f"{DATA_BASE_URL}table/{table_id}/"
73
+
74
+ def _baserow_record_to_user(record: Dict, is_end_user: bool) -> Dict:
75
+ """
76
+ Convertit un enregistrement Baserow (avec noms de champs utilisateur)
77
+ en format de dictionnaire Python attendu par le backend.
78
+ """
79
+
80
+
81
+
82
+ # LOGIQUE POUR L'UTILISATEUR PRINCIPAL (PRIMARY USER)
83
+
84
+ # 1. Récupération des champs individuels
85
+ user_data = {
86
+ # Champs communs / Primary Users
87
+ 'baserow_row_id': record['id'], # ID interne de la ligne Baserow (pour les mises à jour)
88
+ 'date_creation': record.get(FIELD_DATE_CREATION),
89
+
90
+ # Primary User specific fields
91
+ 'user_id': record.get(FIELD_ID),
92
+ 'email': record.get(FIELD_EMAIL),
93
+ 'username': record.get(FIELD_USERNAME),
94
+ 'password_hash': record.get(FIELD_PASSWORD_HASH),
95
+
96
+ # Récupération des 5 clés individuelles (pour l'authentification par clé)
97
+ 'api_key': record.get(FIELD_API_KEY),
98
+ 'api_key_2': record.get(FIELD_API_KEY_2),
99
+ 'api_key_3': record.get(FIELD_API_KEY_3),
100
+ 'api_key_4': record.get(FIELD_API_KEY_4),
101
+ 'api_key_5': record.get(FIELD_API_KEY_5),
102
+
103
+ 'security_question': record.get(FIELD_SECURITY_Q),
104
+ 'security_answer_hash': record.get(FIELD_SECURITY_A_HASH),
105
+ 'plan_id': record.get(FIELD_PLAN_ID),
106
+ 'stripe_subscription_id': record.get(FIELD_STRIPE_SUB_ID),
107
+ 'date_plan_start': record.get(FIELD_DATE_PLAN_START),
108
+ }
109
+
110
+ # 2. ÉTAPE CRUCIALE AJOUTÉE : Création de la liste 'api_keys' pour l'affichage
111
+ # Cette liste est nécessaire pour que la boucle dans api_key.html fonctionne correctement.
112
+ user_data['api_keys'] = [
113
+ user_data['api_key'],
114
+ user_data['api_key_2'],
115
+ user_data['api_key_3'],
116
+ user_data['api_key_4'],
117
+ user_data['api_key_5'],
118
+ ]
119
+
120
+ # Nettoyage des clés None ou non-pertinentes
121
+ return {k: v for k, v in user_data.items() if v is not None}
122
+
123
+
124
+ def _user_to_baserow_data(user_data: Dict, is_end_user: bool) -> Dict:
125
+ """
126
+ Convertit le format de dictionnaire Python du backend en format
127
+ JSON attendu par l'API Baserow (avec noms de champs utilisateur).
128
+ """
129
+ if is_end_user:
130
+ # End User fields (Ajout des NOUVEAUX champs)
131
+ baserow_data = {
132
+ FIELD_END_USER_ID: user_data.get('end_user_id'),
133
+ FIELD_END_USER_IDENTIFIER: user_data.get('identifier'),
134
+ FIELD_END_USER_EMAIL: user_data.get('email'), # NOUVEAU
135
+ FIELD_END_USER_USERNAME: user_data.get('username'), # NOUVEAU
136
+ FIELD_END_USER_SECURITY_Q: user_data.get('security_question'), # NOUVEAU
137
+ FIELD_END_USER_SECURITY_A_HASH: user_data.get('security_answer_hash'), # NOUVEAU
138
+ FIELD_END_USER_STATUS: user_data.get('status'), # NOUVEAU
139
+
140
+ # CORRECTION CRUCIALE : Utilisation du nom de champ correct pour l'End User
141
+ FIELD_PASSWORD_HASH_END_USER: user_data.get('password_hash'),
142
+
143
+ FIELD_END_USER_METADATA: user_data.get('metadata'),
144
+ FIELD_DATE_CREATION: user_data.get('date_creation'),
145
+ # Le lien vers Primary_Users est géré dans save_end_user_data
146
+ }
147
+ else:
148
+ # Primary User fields
149
+ baserow_data = {
150
+ FIELD_ID: user_data.get('user_id'),
151
+ FIELD_EMAIL: user_data.get('email'),
152
+ FIELD_USERNAME: user_data.get('username'),
153
+ FIELD_PASSWORD_HASH: user_data.get('password_hash'),
154
+ FIELD_API_KEY: user_data.get('api_key'),
155
+ FIELD_API_KEY_2: user_data.get('api_key_2'),
156
+ FIELD_API_KEY_3: user_data.get('api_key_3'),
157
+ FIELD_API_KEY_4: user_data.get('api_key_4'),
158
+ FIELD_API_KEY_5: user_data.get('api_key_5'),
159
+ FIELD_SECURITY_Q: user_data.get('security_question'),
160
+ FIELD_SECURITY_A_HASH: user_data.get('security_answer_hash'),
161
+ FIELD_PLAN_ID: user_data.get('plan_id'),
162
+ FIELD_STRIPE_SUB_ID: user_data.get('stripe_subscription_id'),
163
+ FIELD_DATE_CREATION: user_data.get('date_creation'),
164
+ FIELD_DATE_PLAN_START: user_data.get('date_plan_start'),
165
+ FIELD_API_CALLS_MONTH: user_data.get('api_calls_month', 0),
166
+ FIELD_STATUS: user_data.get('status', 'Active') # Assurez-vous que 'Active' est une option valide dans Baserow
167
+ }
168
+
169
+ # Suppression des clés non-valorisées (None)
170
+ return {k: v for k, v in baserow_data.items() if v is not None}
171
+
172
+
173
+ def _get_single_user_record(table_id: str, field_name: str, value: str, is_end_user: bool) -> Optional[Dict]:
174
+ """Fonction générique pour rechercher un seul enregistrement par un champ (filtrage Baserow)."""
175
+ url = _get_table_url(table_id)
176
+ # Utilisation du paramètre de filtre de Baserow pour une recherche indexée (plus rapide)
177
+ filter_param = f"filter__{field_name}__equal={value}"
178
+
179
+ try:
180
+ response = requests.get(
181
+ f"{url}?user_field_names=true&{filter_param}",
182
+ headers=HEADERS
183
+ )
184
+ response.raise_for_status()
185
+
186
+ data = response.json()
187
+ if data and data.get('results'):
188
+ # On ne prend que le premier résultat (car ID/Email/API Key sont uniques)
189
+ return _baserow_record_to_user(data['results'][0], is_end_user)
190
+ return None
191
+
192
+ except requests.exceptions.RequestException as e:
193
+ print(f"Erreur de Baserow lors de la recherche par filtre {field_name}: {e}", file=sys.stderr)
194
+ return None
195
+
196
+ # ----------------------------------------------------------------------
197
+ # --- Fonctions CRUD Primary_Users (Nouveau et Remplacement) ---
198
+ # ----------------------------------------------------------------------
199
+
200
+ def get_user_by_email(email: str) -> Optional[Dict]:
201
+ """Recherche un utilisateur principal par son adresse Email."""
202
+ return _get_single_user_record(PRIMARY_USERS_TABLE_ID, FIELD_EMAIL, email, is_end_user=False)
203
+
204
+ def get_client_user_by_api_key(api_key: str) -> Optional[Dict]:
205
+ """
206
+ Recherche un utilisateur principal par L'UNE de ses 5 clés API.
207
+ Utilise le filtre 'OR' de Baserow pour vérifier les 5 champs.
208
+ """
209
+ url = _get_table_url(PRIMARY_USERS_TABLE_ID)
210
+
211
+ # 1. Définition du filtre OR sur les 5 champs de clé API
212
+ filters = {
213
+ "filter_type": "OR",
214
+ "filters": [
215
+ # Filtre exact pour Clé API
216
+ {"field": FIELD_API_KEY, "type": "equal", "value": api_key},
217
+ {"field": FIELD_API_KEY_2, "type": "equal", "value": api_key},
218
+ {"field": FIELD_API_KEY_3, "type": "equal", "value": api_key},
219
+ {"field": FIELD_API_KEY_4, "type": "equal", "value": api_key},
220
+ {"field": FIELD_API_KEY_5, "type": "equal", "value": api_key},
221
+ ]
222
+ }
223
+
224
+ # 2. Construction de la requête Baserow.
225
+ # On utilise le paramètre 'filters' pour passer notre objet de filtre.
226
+ params = {
227
+ "user_field_names": "true",
228
+ "filters": json.dumps(filters) # Baserow a besoin du filtre en JSON string
229
+ }
230
+
231
+ # 3. Exécution de la requête (utilisez votre fonction _make_baserow_request ou équivalent pour GET)
232
+ # Assurez-vous que votre fonction de requête supporte le paramètre 'params'
233
+ try:
234
+ response = _make_baserow_request("GET", url, params=params) # Adaptation à votre structure de fonction d'appel
235
+
236
+ if response.status_code == 200:
237
+ data = response.json()
238
+ # La réponse contient 'results' (une liste)
239
+ if data and data['results']:
240
+ # Nous nous attendons à un seul résultat (la clé API est censée être unique)
241
+ record = data['results'][0]
242
+ # Convertir l'enregistrement Baserow en objet utilisateur (Primary User)
243
+ return _baserow_record_to_user(record) # Assurez-vous d'avoir retiré le paramètre is_end_user ici
244
+
245
+ except Exception as e:
246
+ logger.error(f"Erreur lors de la recherche par clé API: {e}", file=sys.stderr)
247
+
248
+ return None
249
+
250
+ # Remplacement de l'ancien load_primary_user_data(user_id)
251
+ def load_primary_user_data(user_id: str) -> Optional[Dict]:
252
+ """Recherche un utilisateur principal par son ID (user_id)."""
253
+ return _get_single_user_record(PRIMARY_USERS_TABLE_ID, FIELD_ID, user_id, is_end_user=False)
254
+
255
+
256
+ def save_primary_user_data(user_data: Dict, commit_msg: str = "") -> bool:
257
+ """Crée ou met à jour un utilisateur principal, avec détection d'erreur ultra-précise."""
258
+ row_id = user_data.get('baserow_row_id')
259
+
260
+ # Définition de l'URL de base pour la table des utilisateurs principaux
261
+ url = _get_table_url(PRIMARY_USERS_TABLE_ID)
262
+
263
+ # 1. Conversion des données
264
+ baserow_data = _user_to_baserow_data(user_data, is_end_user=False)
265
+
266
+ # 2. Suppression des champs en lecture seule (comme dans la correction précédente)
267
+ if baserow_data.pop(FIELD_ID, None):
268
+ print(f"DEBUG: Suppression du champ '{FIELD_ID}' (UUID auto) avant l'envoi { 'POST' if not row_id else 'PATCH'}.", file=sys.stderr)
269
+
270
+ try:
271
+ # Détermination de l'action (PATCH ou POST)
272
+ if row_id:
273
+ action = "PATCH" # ⬅️ CORRECTION: Définition de 'action'
274
+ # MISE À JOUR (PATCH)
275
+ response = requests.patch(
276
+ f"{url}{row_id}/?user_field_names=true",
277
+ headers=HEADERS,
278
+ json=baserow_data
279
+ )
280
+ else:
281
+ action = "POST" # ⬅️ CORRECTION: Définition de 'action'
282
+ # CRÉATION (POST)
283
+ response = requests.post(
284
+ f"{url}?user_field_names=true", # ⬅️ CORRECTION: Utilise l'URL de table 'url'
285
+ headers=HEADERS,
286
+ json=baserow_data
287
+ )
288
+
289
+ # Déclenche une exception requests.exceptions.HTTPError pour les statuts 4xx/5xx
290
+ response.raise_for_status()
291
+
292
+ # Succès
293
+ if not row_id:
294
+ new_record = response.json()
295
+ # 1. Mettre à jour l'ID de ligne Baserow
296
+ user_data['baserow_row_id'] = new_record.get('id')
297
+ # 2. Mettre à jour l'UUID de l'utilisateur (généré par Baserow)
298
+ user_data['user_id'] = new_record.get(FIELD_ID)
299
+
300
+ print(f"DEBUG: UUID de l'utilisateur généré par Baserow et enregistré: {user_data['user_id']}", file=sys.stderr)
301
+
302
+ print(f"DEBUG: Baserow Primary User action '{action}' réussie. Row ID: {user_data.get('baserow_row_id')}. Message: {commit_msg}", file=sys.stderr)
303
+ return True
304
+
305
+ except requests.exceptions.RequestException as e:
306
+ # --- BLOC DE DÉTECTION D'ERREUR PRÉCISE (Ultra-Complet) ---
307
+
308
+ # Note: 'action' est définie dans le bloc try/except, mais si l'erreur survient
309
+ # AVANT la définition de 'action', nous devons la gérer.
310
+ # Pour être sûr, nous allons la définir ici par défaut si elle n'existe pas.
311
+ if 'action' not in locals():
312
+ action = "INCONNU"
313
+
314
+ error_message = f"🚨 ÉCHEC: Erreur lors de la sauvegarde/mise à jour du Primary User dans Baserow. Requête: {action}"
315
+ error_details = ""
316
+
317
+ if hasattr(e, 'response') and e.response is not None:
318
+ # 1. Statut HTTP et URL
319
+ error_details += f"\n -> STATUT HTTP: {e.response.status_code} ({e.response.reason})"
320
+ error_details += f"\n -> URL de la requête: {e.response.url}"
321
+
322
+ # 2. Tenter de décoder le corps de la réponse en JSON (contient les erreurs Baserow)
323
+ try:
324
+ response_json = e.response.json()
325
+ error_details += f"\n\n -> ERREUR BASEROW DÉTAILLÉE (JSON):\n{json.dumps(response_json, indent=4)}"
326
+
327
+ # Optionnel: Synthèse des erreurs de validation de champ
328
+ if isinstance(response_json, dict):
329
+ validation_errors = {k: v for k, v in response_json.items() if isinstance(v, list) and k != 'detail'}
330
+ if validation_errors:
331
+ error_details += "\n -> SYNTHÈSE DES CHAMPS INVALIDES (Vérifiez les noms de colonnes/IDs de table!):"
332
+ for field_name, errors in validation_errors.items():
333
+ error_details += f"\n - Champ '{field_name}': {', '.join([err.get('error', 'Erreur inconnue') for err in errors])}"
334
+
335
+ except json.JSONDecodeError:
336
+ # 3. Si le corps de la réponse n'est pas du JSON
337
+ error_details += f"\n\n -> ERREUR BRUTE (Réponse non-JSON):\n{e.response.text[:500]}..."
338
+
339
+ # 4. Afficher les données que nous avons tenté d'envoyer (après la suppression de l'ID si c'était une création)
340
+ error_details += f"\n\n -> DONNÉES ENVOYÉES À BASEROW:\n{json.dumps(baserow_data, indent=4)}"
341
+
342
+ # Log complet de l'erreur
343
+ print(error_message + error_details, file=sys.stderr)
344
+
345
+ return False
346
+
347
+
348
+ # ----------------------------------------------------------------------
349
+ # --- Fonctions CRUD End_Users (Remplacement) ---
350
+ # ----------------------------------------------------------------------
351
+
352
+ # baserow_storage.py : Dans la section CRUD End_Users
353
+
354
+
355
+ def _get_client_baserow_row_id(client_user_id: str) -> Optional[int]:
356
+ """Récupère l'ID de ligne interne Baserow du client principal pour le lien."""
357
+ client_user = load_primary_user_data(client_user_id) # utilise la fonction déjà créée
358
+ return client_user.get('baserow_row_id') if client_user else None
359
+
360
+
361
+ def check_baserow_connection() -> str:
362
+ """
363
+ Vérifie l'état de connexion de la base de données Baserow.
364
+ Retourne 'operational' ou 'outage'.
365
+ """
366
+ # Liste des IDs de tables critiques à vérifier
367
+ CRITICAL_TABLE_IDS = [
368
+ PRIMARY_USERS_TABLE_ID,
369
+ END_USERS_TABLE_ID
370
+ ]
371
+
372
+ if not API_TOKEN:
373
+ # Si le token API n'est pas défini, échec immédiat
374
+ print("DEBUG: BASEROW_API_TOKEN manquant.", file=sys.stderr)
375
+ return "outage"
376
+
377
+ for table_id in CRITICAL_TABLE_IDS:
378
+ if not table_id:
379
+ # Si un des IDs de table critiques n'est pas défini, échec
380
+ print(f"DEBUG: Un ID de table critique Baserow est manquant (ID: {table_id}).", file=sys.stderr)
381
+ return "outage"
382
+
383
+ # Tenter de faire un appel très léger (récupérer la première ligne)
384
+ # On utilise page_size=1 pour minimiser la charge
385
+ url = f"{DATA_BASE_URL}table/{table_id}/?page_size=1"
386
+
387
+ try:
388
+ response = requests.get(url, headers=HEADERS, timeout=5)
389
+
390
+ if response.status_code != 200:
391
+ # Si un 404, 403, ou autre erreur est retournée par Baserow pour CETTE table
392
+ print(f"DEBUG: Baserow check failed for table {table_id} with status code {response.status_code}", file=sys.stderr)
393
+ return "outage"
394
+
395
+ except requests.exceptions.RequestException as e:
396
+ # Erreur de réseau (timeout, DNS, etc.)
397
+ print(f"DEBUG: Baserow connection error for table {table_id}: {e}", file=sys.stderr)
398
+ return "outage"
399
+
400
+ # Si toutes les tables critiques ont été vérifiées avec succès
401
+ return "operational"
402
+
403
+
404
+ def get_health_status() -> Dict:
405
+ """
406
+ Collecte l'état de santé de tous les services pour la page /statut.
407
+ """
408
+
409
+ db_status = check_baserow_connection()
410
+
411
+ # L'état de l'authentification et de l'API principale sont
412
+ # généralement liés à l'état de la DB pour une application simple.
413
+ # Si la DB est HS, l'auth est HS. Sinon, ils sont OK.
414
+
415
+ auth_status = db_status # Lié à la DB (pour charger les utilisateurs)
416
+ api_endpoint_status = "operational" # L'endpoint Flask lui-même est considéré comme OK s'il tourne
417
+
418
+ # Version du service (pour information)
419
+ service_version = os.environ.get("SERVICE_VERSION", "1.0.0 (Baserow)")
420
+
421
+
422
+ return {
423
+ # Ces valeurs correspondent aux attributs 'data-status' dans statut.html
424
+ "auth": auth_status,
425
+ "data_storage": db_status,
426
+ "api_endpoint": api_endpoint_status,
427
+ "version": service_version,
428
+ "last_update": datetime.now().strftime("%Y-%m-%d %H:%M:%S UTC")
429
+ }
430
+
431
+ def is_baserow_up() -> bool:
432
+ """
433
+ Vérifie l'état de Baserow en utilisant l'URL qui garantit un statut 100% fonctionnel
434
+ sur Hugging Face, SANS utiliser la fonction de construction d'URL de table.
435
+ """
436
+ try:
437
+ # Envoie une requête GET à l'URL qui répond positivement pour le health check.
438
+ response = requests.get(
439
+ HEALTH_CHECK_URL,
440
+ headers=HEADERS,
441
+ timeout=5
442
+ )
443
+ # On vérifie si la réponse est un succès (code 200).
444
+ return response.status_code == 200
445
+ except requests.exceptions.RequestException as e:
446
+ print(f"DEBUG: Baserow health check failed: {e}")
447
+ return False
448
+
449
+ def log_baserow_api_call(method: str, url: str, headers: Dict, data: Optional[Dict] = None, log_response: bool = True):
450
+ """
451
+ Fonction utilitaire pour effectuer des appels API à Baserow et journaliser
452
+ les requêtes et les réponses dans les logs du Space Hugging Face.
453
+ """
454
+
455
+ # 1. Journalisation de la requête
456
+ logger.info(f"BASEROW REQUEST: {method} {url}")
457
+ # ATTENTION: Ne pas logger le token API complet!
458
+ logged_headers = {k: v.replace(API_TOKEN, '[TOKEN_MASKED]') if k == 'Authorization' else v for k, v in headers.items()}
459
+ logger.debug(f"BASEROW REQUEST Headers: {logged_headers}")
460
+ if data:
461
+ # Pour les requêtes POST/PUT, logger les données (sans le hash du mot de passe si possible)
462
+ logged_data = data.copy() if isinstance(data, dict) else data
463
+ if isinstance(logged_data, dict) and 'Hachage du mot de passe' in logged_data:
464
+ logged_data['Hachage du mot de passe'] = '[PASSWORD_HASH_MASKED]'
465
+ logger.debug(f"BASEROW REQUEST Body: {logged_data}")
466
+
467
+ # 2. Exécution de la requête
468
+ try:
469
+ if method == "GET":
470
+ response = requests.get(url, headers=headers)
471
+ elif method == "POST":
472
+ response = requests.post(url, headers=headers, json=data)
473
+ elif method == "PUT":
474
+ response = requests.put(url, headers=headers, json=data)
475
+ elif method == "DELETE":
476
+ response = requests.delete(url, headers=headers)
477
+ else:
478
+ raise ValueError(f"Méthode HTTP non supportée: {method}")
479
+
480
+ # 3. Journalisation de la réponse
481
+ if log_response:
482
+ logger.info(f"BASEROW RESPONSE: Status {response.status_code}")
483
+ # Journaliser le contenu pour les erreurs
484
+ if response.status_code >= 400:
485
+ logger.error(f"BASEROW ERROR RESPONSE Body: {response.text}")
486
+
487
+ # 4. Retourner la réponse
488
+ return response
489
+
490
+ except requests.exceptions.RequestException as e:
491
+ logger.error(f"BASEROW CONNECTION ERROR: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
492
  return None # Retourne None en cas d'erreur de connexion non gérée par le statut HTTP