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

Update auth_backend.py

Browse files
Files changed (1) hide show
  1. auth_backend.py +253 -358
auth_backend.py CHANGED
@@ -1,359 +1,254 @@
1
- # auth_backend.py
2
-
3
- import json
4
- import uuid
5
- import secrets
6
- import string
7
- import sys # Nécessaire pour print(..., file=sys.stderr)
8
- from datetime import datetime
9
- from werkzeug.security import generate_password_hash, check_password_hash
10
- # --- NOUVEL IMPORT: Migration vers Baserow ---
11
- # Ces fonctions sont maintenant implémentées dans baserow_storage.py
12
- from baserow_storage import (
13
- # Fonctions de lecture/écriture pour les utilisateurs principaux (simulant l'ancienne API)
14
- get_client_user_by_api_key,
15
- get_end_user_by_email,
16
- load_primary_user_data,
17
- save_primary_user_data,
18
- # Fonctions de recherche indexées (nouvelles et plus efficaces)
19
- get_user_by_email,
20
- get_client_user_by_api_key,
21
- # Fonctions de lecture/écriture pour les utilisateurs finaux (simulant l'ancienne API)
22
- load_end_user_data,
23
- save_end_user_data,
24
- # load_users_data est désormais obsolète et retiré.
25
- )
26
- from flask import session
27
- from config import PLANS_CONFIG
28
- from typing import Optional, Dict
29
-
30
- # ----------------------------------------------------------------------
31
- # --- Fonctions Utilitaires et de Configuration ---
32
- # ----------------------------------------------------------------------
33
-
34
- def get_plan_limit(plan: str) -> float:
35
- """Retourne la limite de compte pour un plan donné."""
36
- return PLANS_CONFIG.get(plan, {}).get("limit", PLANS_CONFIG["free"]["limit"])
37
-
38
- def get_plan_details(plan_id: str) -> Optional[Dict]:
39
- """Retourne les détails complets d'un plan à partir de son ID."""
40
- return PLANS_CONFIG.get(plan_id)
41
-
42
- def generate_api_key(length: int = 32) -> str:
43
- """Génère une clé API sécurisée."""
44
- chars = string.ascii_letters + string.digits
45
- return ''.join(secrets.choice(chars) for _ in range(length))
46
-
47
- # ----------------------------------------------------------------------
48
- # --- Fonctions d'Authentification WEB (Primary Users) ---
49
- # ----------------------------------------------------------------------
50
-
51
- def register_user(username: str, email: str, password: str, confirm_password: str, security_question: str, security_answer: str) -> tuple[Optional[str], str, Optional[Dict]]:
52
- """
53
- Tente d'enregistrer un nouvel utilisateur principal.
54
- Retourne l'ID utilisateur (str), un message (str), et les données utilisateur complètes (Dict) ou None.
55
- """
56
- email = email.lower().strip()
57
-
58
- # Validation du formulaire
59
- if not all([username, email, password, confirm_password, security_question, security_answer]):
60
- # Retourne (ID, message, Data)
61
- return None, "Tous les champs sont requis.", None
62
-
63
- if password != confirm_password:
64
- return None, "Les mots de passe ne correspondent pas.", None
65
-
66
- if len(password) < 8:
67
- return None, "Le mot de passe est trop court (min 8 caractères).", None
68
-
69
- # 1. Vérification de l'existence de l'utilisateur par email
70
- if get_user_by_email(email):
71
- return None, "Un compte avec cette adresse e-mail existe déjà.", None
72
-
73
- # 2. Hachage des données
74
- password_hash = generate_password_hash(password)
75
- security_answer_hash = generate_password_hash(security_answer.lower())
76
-
77
- # 3. Création des données utilisateur
78
- user_id = str(uuid.uuid4())
79
- # Ligne optimisée: on génère les 5 clés en une seule liste
80
- api_keys = [generate_api_key() for _ in range(5)]
81
-
82
- new_user = {
83
- 'user_id': user_id,
84
- 'username': username,
85
- 'email': email,
86
- 'password_hash': password_hash,
87
- # ASSIGNATION DES 5 CLÉS API AUX CHAMPS CORRESPONDANTS
88
- 'api_key': api_keys[0],
89
- 'api_key_2': api_keys[1],
90
- 'api_key_3': api_keys[2],
91
- 'api_key_4': api_keys[3],
92
- 'api_key_5': api_keys[4],
93
- 'security_question': security_question,
94
- 'security_answer_hash': security_answer_hash,
95
- 'plan_id': PLANS_CONFIG['free']['baserow_value'],
96
- 'stripe_subscription_id': None, # Pas d'abonnement Stripe au début
97
- 'date_creation': datetime.now().isoformat(),
98
- 'date_plan_start': datetime.now().isoformat(),
99
- 'api_calls_month': 0,
100
- 'status': 'Active',
101
- 'baserow_row_id': None, # Sera rempli par la fonction save_primary_user_data si c'est une création
102
- # Note: D'autres champs pourraient être nécessaires ici, selon les besoins non-vus
103
- }
104
-
105
- # 4. Sauvegarde dans Baserow
106
- # La fonction save_primary_user_data mettra à jour 'baserow_row_id' dans new_user si la création est réussie.
107
- success = save_primary_user_data(new_user, commit_msg=f"feat: Création de l'utilisateur {user_id} ({email})")
108
-
109
- if success:
110
- # Retourne : ID utilisateur, message de succès, Dictionnaire utilisateur complet
111
- return user_id, "Inscription réussie. Vous pouvez maintenant vous connecter.", new_user
112
- else:
113
- # Échec de l'écriture dans la BDD (Baserow)
114
- # Retourne : None, message d'erreur, None
115
- return None, "Erreur interne lors de l'enregistrement de l'utilisateur.", None
116
-
117
-
118
- def login_user(username_or_email: str, password: str) -> tuple[Optional[str], str, Optional[Dict]]: # <-- NOUVEAU: Ajout de Optional[Dict] dans le type hint
119
- """
120
- Tente de connecter un utilisateur principal.
121
- Retourne l'ID utilisateur (str), un message (str) et les données utilisateur (Dict).
122
- """
123
- username_or_email = username_or_email.lower().strip()
124
-
125
- # 1. Recherche de l'utilisateur par email (Utilisation de la nouvelle fonction rapide Baserow)
126
- user = get_user_by_email(username_or_email)
127
-
128
- if user:
129
- # 2. Vérification du mot de passe
130
- if check_password_hash(user['password_hash'], password):
131
- session['user_id'] = user['user_id']
132
- # CORRECTION : Retourne 3 valeurs : ID, Message, Données Utilisateur
133
- return user['user_id'], "Connexion réussie.", user
134
-
135
- # CORRECTION : Retourne 3 valeurs : None ID, Message, None Data
136
- return None, "Email/Nom d'utilisateur ou mot de passe invalide.", None
137
-
138
-
139
- def get_user_by_id(user_id: str) -> Optional[Dict]:
140
- """
141
- Récupère un utilisateur principal par son ID.
142
- Utilise la fonction load_primary_user_data (qui est get_user_by_id dans Baserow).
143
- """
144
- return load_primary_user_data(user_id)
145
-
146
- def get_user_by_api_key(api_key: str) -> Optional[Dict]:
147
- """
148
- Récupère un utilisateur principal par sa Clé API (utilisé par le décorateur).
149
- Utilise la nouvelle fonction indexée et rapide de Baserow.
150
- """
151
- return get_client_user_by_api_key(api_key)
152
-
153
-
154
- def reset_password_via_security_question(username_or_email: str, question: str, answer: str, new_password: str) -> tuple[bool, str]:
155
- """Réinitialise le mot de passe via la question de sécurité."""
156
- username_or_email = username_or_email.lower().strip()
157
-
158
- # Validation du mot de passe
159
- if len(new_password) < 8:
160
- return False, "Le nouveau mot de passe est trop court (min 8 caractères)."
161
-
162
- # 1. Recherche de l'utilisateur
163
- user = get_user_by_email(username_or_email)
164
-
165
- if not user:
166
- return False, "Utilisateur introuvable."
167
-
168
- # 2. Vérification de la question/réponse
169
- if user.get('security_question') != question:
170
- return False, "Question de sécurité incorrecte."
171
-
172
- if not check_password_hash(user.get('security_answer_hash', ''), answer.lower()):
173
- return False, "Réponse de sécurité incorrecte."
174
-
175
- # 3. Hachage du nouveau mot de passe
176
- new_hashed_password = generate_password_hash(new_password)
177
-
178
- # 4. Mise à jour et sauvegarde en BDD (Baserow)
179
- user['password_hash'] = new_hashed_password
180
-
181
- success = save_primary_user_data(user, commit_msg=f"feat: Réinitialisation MDP utilisateur {user['user_id']}")
182
-
183
- if success:
184
- return True, "Mot de passe réinitialisé avec succès."
185
- else:
186
- return False, "Erreur interne lors de la mise à jour du mot de passe."
187
-
188
-
189
- def update_user_plan(user_id: str, new_plan_id: str, stripe_subscription_id: Optional[str]) -> bool:
190
- """Met à jour le plan et l'ID d'abonnement Stripe pour un utilisateur."""
191
-
192
- user = get_user_by_id(user_id)
193
-
194
- if not user:
195
- print(f"Erreur: Utilisateur {user_id} non trouvé pour la mise à jour du plan.", file=sys.stderr)
196
- return False
197
-
198
- user['plan_id'] = new_plan_id
199
- user['stripe_subscription_id'] = stripe_subscription_id
200
- user['date_plan_start'] = datetime.now().isoformat()
201
-
202
- success = save_primary_user_data(user, commit_msg=f"feat: Mise à jour du plan pour {user_id} vers {new_plan_id}")
203
-
204
- return success
205
-
206
- # ----------------------------------------------------------------------
207
- # --- Fonctions API (End Users) ---
208
- # ----------------------------------------------------------------------
209
-
210
- def register_end_user(client_user_id: str, email: str, username: str, password: str, security_question: Optional[str] = None, security_answer: Optional[str] = None, identifier: Optional[str] = None) -> tuple[Optional[str], str, Optional[Dict]]:
211
- """
212
- Tente d'enregistrer un nouvel utilisateur final pour un client donné.
213
- Retourne l'ID utilisateur final (str), un message (str), et les données utilisateur complètes (Dict) ou None.
214
- """
215
- email = email.lower().strip()
216
-
217
- # 1. Validation de base
218
- if not all([client_user_id, email, username, password]):
219
- return None, "Les champs Client ID, Email, Nom d'utilisateur et Mot de passe sont requis.", None
220
-
221
- if len(password) < 8:
222
- return None, "Le mot de passe est trop court (min 8 caractères).", None
223
-
224
- # 2. Vérification de l'unicité de l'utilisateur final par email (scopé par client)
225
- if get_end_user_by_email(client_user_id, email):
226
- return None, "Un utilisateur final avec cette adresse e-mail existe déjà pour ce client.", None
227
-
228
- # 3. Hachage des données
229
- password_hash = generate_password_hash(password)
230
- security_answer_hash = generate_password_hash(security_answer.lower()) if security_answer else None
231
-
232
- # 4. Création des données
233
- end_user_id = str(uuid.uuid4())
234
-
235
- new_end_user = {
236
- 'end_user_id': end_user_id,
237
- 'client_user_id': client_user_id, # ID du client principal pour le lien
238
- 'identifier': identifier or email,
239
- 'email': email,
240
- 'username': username,
241
- 'password_hash': password_hash,
242
- 'security_question': security_question,
243
- 'security_answer_hash': security_answer_hash,
244
- 'status': 'Active',
245
- 'metadata': '{}', # Initialiser les métadonnées
246
- 'date_creation': datetime.now().isoformat(),
247
- 'baserow_row_id': None,
248
- }
249
-
250
- # 5. Sauvegarde dans Baserow
251
- success = save_end_user_data(new_end_user, commit_msg=f"feat: Création de l'utilisateur final {end_user_id}")
252
-
253
- if success:
254
- return end_user_id, "Inscription de l'utilisateur final réussie.", new_end_user
255
- else:
256
- return None, "Erreur interne lors de l'enregistrement de l'utilisateur final.", None
257
-
258
- def login_end_user(client_user_id: str, email: str, password: str) -> tuple[Optional[str], str, Optional[Dict]]:
259
- """
260
- Tente de connecter un utilisateur final (End User) sous l'autorité d'un client principal (Primary User).
261
- Retourne l'ID utilisateur final (str), un message (str) et les données utilisateur (Dict).
262
- """
263
- email = email.lower().strip()
264
-
265
- # 1. Recherche de l'utilisateur final par email et client ID
266
- end_user = get_end_user_by_email(client_user_id, email)
267
-
268
- if end_user:
269
- # 2. Vérification du mot de passe
270
- if check_password_hash(end_user['password_hash'], password):
271
- # Succès de la connexion.
272
- return end_user['end_user_id'], "Connexion utilisateur final réussie.", end_user
273
-
274
- # Échec de l'authentification
275
- return None, "Email ou mot de passe utilisateur final invalide pour ce client.", None
276
-
277
- def reset_end_user_password_by_client(client_user_id: str, end_user_id: str, new_password: str) -> tuple[bool, str]:
278
- """
279
- Permet à un client principal (Primary User) de réinitialiser le mot de passe
280
- d'un de ses utilisateurs finaux (End User) par son ID (API client-side).
281
- """
282
- # 1. Validation du mot de passe
283
- if len(new_password) < 8:
284
- return False, "Le nouveau mot de passe est trop court (min 8 caractères)."
285
-
286
- # 2. Charger les données de l'utilisateur final, en s'assurant qu'il appartient bien au client.
287
- end_user_data = load_end_user_data(client_user_id, end_user_id)
288
-
289
- if not end_user_data:
290
- # L'utilisateur final n'existe pas ou n'est pas lié à ce client_user_id
291
- return False, "Utilisateur final introuvable ou non autorisé (n'appartient pas à ce client)."
292
-
293
- # 3. Hachage du nouveau mot de passe
294
- new_password_hash = generate_password_hash(new_password)
295
-
296
- # 4. Mise à jour des données
297
- end_user_data['password_hash'] = new_password_hash
298
-
299
- # 5. Sauvegarde des données
300
- success = save_end_user_data(end_user_data, commit_msg=f"action: Réinitialisation du mot de passe de l'utilisateur final {end_user_id} par le client {client_user_id}")
301
-
302
- if success:
303
- return True, "Mot de passe de l'utilisateur final réinitialisé avec succès."
304
- else:
305
- return False, "Erreur lors de la sauvegarde du nouveau mot de passe de l'utilisateur final."
306
-
307
-
308
- def update_user_profile(user_id: str, username: str, email: str, new_password: Optional[str] = None) -> tuple[bool, str]:
309
- """
310
- Met à jour le profil de l'utilisateur principal (client).
311
- user_id: L'UUID de l'utilisateur.
312
- username: Le nouveau nom d'utilisateur.
313
- email: Le nouvel email.
314
- new_password: Le nouveau mot de passe (si fourni et non vide).
315
- """
316
-
317
- # 1. Chargement des données existantes
318
- user_data = load_primary_user_data(user_id)
319
- if not user_data:
320
- return False, "Erreur critique : Utilisateur introuvable pour la mise à jour."
321
-
322
- original_email = user_data.get('email')
323
-
324
- # 2. Validation et mise à jour de l'email
325
- if email and email != original_email:
326
- # Vérification si le nouvel email n'est pas déjà utilisé par un autre utilisateur
327
- # Note: get_user_by_email retourne l'objet utilisateur, pas juste l'ID.
328
- existing_user_by_email = get_user_by_email(email)
329
-
330
- # S'il trouve un utilisateur ET que son ID ne correspond pas à l'utilisateur actuel,
331
- # l'email est déjà pris.
332
- if existing_user_by_email and existing_user_by_email.get('user_id') != user_id:
333
- return False, "Cet email est déjà utilisé par un autre compte."
334
-
335
- # Mise à jour de l'email si la validation passe
336
- user_data['email'] = email
337
-
338
- # 3. Mise à jour du nom d'utilisateur (pas de validation d'unicité assumée ici)
339
- user_data['username'] = username
340
-
341
- # 4. Mise à jour du mot de passe (si un nouveau est fourni)
342
- if new_password:
343
- if len(new_password) < 8:
344
- # Assurez-vous que cette limite est cohérente avec la fonction register_user
345
- return False, "Le nouveau mot de passe est trop court (min 8 caractères)."
346
-
347
- user_data['password_hash'] = generate_password_hash(new_password)
348
-
349
- # 5. Sauvegarde des données
350
- # Le row_id est stocké dans l'objet utilisateur après la première sauvegarde.
351
- baserow_row_id = user_data.get('baserow_row_id')
352
-
353
- if baserow_row_id is None:
354
- return False, "Erreur de configuration : ID de ligne Baserow manquant."
355
-
356
- if save_primary_user_data(user_data, commit_msg=f"feat: Mise à jour du profil utilisateur {user_id}"):
357
- return True, "Votre profil a été mis à jour avec succès."
358
- else:
359
  return False, "Une erreur s'est produite lors de la sauvegarde du profil."
 
1
+ # auth_backend.py
2
+
3
+ import json
4
+ import uuid
5
+ import secrets
6
+ import string
7
+ import sys # Nécessaire pour print(..., file=sys.stderr)
8
+ from datetime import datetime
9
+ from werkzeug.security import generate_password_hash, check_password_hash
10
+ # --- NOUVEL IMPORT: Migration vers Baserow ---
11
+ # Ces fonctions sont maintenant implémentées dans baserow_storage.py
12
+ from baserow_storage import (
13
+ # Fonctions de lecture/écriture pour les utilisateurs principaux (simulant l'ancienne API)
14
+ get_client_user_by_api_key,
15
+ load_primary_user_data,
16
+ save_primary_user_data,
17
+ # Fonctions de recherche indexées (nouvelles et plus efficaces)
18
+ get_user_by_email,
19
+ get_client_user_by_api_key,
20
+ # load_users_data est désormais obsolète et retiré.
21
+ )
22
+ from flask import session
23
+ from config import PLANS_CONFIG
24
+ from typing import Optional, Dict
25
+
26
+ # ----------------------------------------------------------------------
27
+ # --- Fonctions Utilitaires et de Configuration ---
28
+ # ----------------------------------------------------------------------
29
+
30
+ def get_plan_limit(plan: str) -> float:
31
+ """Retourne la limite de compte pour un plan donné."""
32
+ return PLANS_CONFIG.get(plan, {}).get("limit", PLANS_CONFIG["free"]["limit"])
33
+
34
+ def get_plan_details(plan_id: str) -> Optional[Dict]:
35
+ """Retourne les détails complets d'un plan à partir de son ID."""
36
+ return PLANS_CONFIG.get(plan_id)
37
+
38
+ def generate_api_key(length: int = 32) -> str:
39
+ """Génère une clé API sécurisée."""
40
+ chars = string.ascii_letters + string.digits
41
+ return ''.join(secrets.choice(chars) for _ in range(length))
42
+
43
+ # ----------------------------------------------------------------------
44
+ # --- Fonctions d'Authentification WEB (Primary Users) ---
45
+ # ----------------------------------------------------------------------
46
+
47
+ def register_user(username: str, email: str, password: str, confirm_password: str, security_question: str, security_answer: str) -> tuple[Optional[str], str, Optional[Dict]]:
48
+ """
49
+ Tente d'enregistrer un nouvel utilisateur principal.
50
+ Retourne l'ID utilisateur (str), un message (str), et les données utilisateur complètes (Dict) ou None.
51
+ """
52
+ email = email.lower().strip()
53
+
54
+ # Validation du formulaire
55
+ if not all([username, email, password, confirm_password, security_question, security_answer]):
56
+ # Retourne (ID, message, Data)
57
+ return None, "Tous les champs sont requis.", None
58
+
59
+ if password != confirm_password:
60
+ return None, "Les mots de passe ne correspondent pas.", None
61
+
62
+ if len(password) < 8:
63
+ return None, "Le mot de passe est trop court (min 8 caractères).", None
64
+
65
+ # 1. Vérification de l'existence de l'utilisateur par email
66
+ if get_user_by_email(email):
67
+ return None, "Un compte avec cette adresse e-mail existe déjà.", None
68
+
69
+ # 2. Hachage des données
70
+ password_hash = generate_password_hash(password)
71
+ security_answer_hash = generate_password_hash(security_answer.lower())
72
+
73
+ # 3. Création des données utilisateur
74
+ user_id = str(uuid.uuid4())
75
+ # Ligne optimisée: on génère les 5 clés en une seule liste
76
+ api_keys = [generate_api_key() for _ in range(5)]
77
+
78
+ new_user = {
79
+ 'user_id': user_id,
80
+ 'username': username,
81
+ 'email': email,
82
+ 'password_hash': password_hash,
83
+ # ASSIGNATION DES 5 CLÉS API AUX CHAMPS CORRESPONDANTS
84
+ 'api_key': api_keys[0],
85
+ 'api_key_2': api_keys[1],
86
+ 'api_key_3': api_keys[2],
87
+ 'api_key_4': api_keys[3],
88
+ 'api_key_5': api_keys[4],
89
+ 'security_question': security_question,
90
+ 'security_answer_hash': security_answer_hash,
91
+ 'plan_id': PLANS_CONFIG['free']['baserow_value'],
92
+ 'stripe_subscription_id': None, # Pas d'abonnement Stripe au début
93
+ 'date_creation': datetime.now().isoformat(),
94
+ 'date_plan_start': datetime.now().isoformat(),
95
+ 'api_calls_month': 0,
96
+ 'status': 'Active',
97
+ 'baserow_row_id': None, # Sera rempli par la fonction save_primary_user_data si c'est une création
98
+ # Note: D'autres champs pourraient être nécessaires ici, selon les besoins non-vus
99
+ }
100
+
101
+ # 4. Sauvegarde dans Baserow
102
+ # La fonction save_primary_user_data mettra à jour 'baserow_row_id' dans new_user si la création est réussie.
103
+ success = save_primary_user_data(new_user, commit_msg=f"feat: Création de l'utilisateur {user_id} ({email})")
104
+
105
+ if success:
106
+ # Retourne : ID utilisateur, message de succès, Dictionnaire utilisateur complet
107
+ return user_id, "Inscription réussie. Vous pouvez maintenant vous connecter.", new_user
108
+ else:
109
+ # Échec de l'écriture dans la BDD (Baserow)
110
+ # Retourne : None, message d'erreur, None
111
+ return None, "Erreur interne lors de l'enregistrement de l'utilisateur.", None
112
+
113
+
114
+ def login_user(username_or_email: str, password: str) -> tuple[Optional[str], str, Optional[Dict]]: # <-- NOUVEAU: Ajout de Optional[Dict] dans le type hint
115
+ """
116
+ Tente de connecter un utilisateur principal.
117
+ Retourne l'ID utilisateur (str), un message (str) et les données utilisateur (Dict).
118
+ """
119
+ username_or_email = username_or_email.lower().strip()
120
+
121
+ # 1. Recherche de l'utilisateur par email (Utilisation de la nouvelle fonction rapide Baserow)
122
+ user = get_user_by_email(username_or_email)
123
+
124
+ if user:
125
+ # 2. Vérification du mot de passe
126
+ if check_password_hash(user['password_hash'], password):
127
+ session['user_id'] = user['user_id']
128
+ # CORRECTION : Retourne 3 valeurs : ID, Message, Données Utilisateur
129
+ return user['user_id'], "Connexion réussie.", user
130
+
131
+ # CORRECTION : Retourne 3 valeurs : None ID, Message, None Data
132
+ return None, "Email/Nom d'utilisateur ou mot de passe invalide.", None
133
+
134
+
135
+ def get_user_by_id(user_id: str) -> Optional[Dict]:
136
+ """
137
+ Récupère un utilisateur principal par son ID.
138
+ Utilise la fonction load_primary_user_data (qui est get_user_by_id dans Baserow).
139
+ """
140
+ return load_primary_user_data(user_id)
141
+
142
+ def get_user_by_api_key(api_key: str) -> Optional[Dict]:
143
+ """
144
+ Récupère un utilisateur principal par sa Clé API (utilisé par le décorateur).
145
+ Utilise la nouvelle fonction indexée et rapide de Baserow.
146
+ """
147
+ return get_client_user_by_api_key(api_key)
148
+
149
+
150
+ def reset_password_via_security_question(username_or_email: str, question: str, answer: str, new_password: str) -> tuple[bool, str]:
151
+ """Réinitialise le mot de passe via la question de sécurité."""
152
+ username_or_email = username_or_email.lower().strip()
153
+
154
+ # Validation du mot de passe
155
+ if len(new_password) < 8:
156
+ return False, "Le nouveau mot de passe est trop court (min 8 caractères)."
157
+
158
+ # 1. Recherche de l'utilisateur
159
+ user = get_user_by_email(username_or_email)
160
+
161
+ if not user:
162
+ return False, "Utilisateur introuvable."
163
+
164
+ # 2. Vérification de la question/réponse
165
+ if user.get('security_question') != question:
166
+ return False, "Question de sécurité incorrecte."
167
+
168
+ if not check_password_hash(user.get('security_answer_hash', ''), answer.lower()):
169
+ return False, "Réponse de sécurité incorrecte."
170
+
171
+ # 3. Hachage du nouveau mot de passe
172
+ new_hashed_password = generate_password_hash(new_password)
173
+
174
+ # 4. Mise à jour et sauvegarde en BDD (Baserow)
175
+ user['password_hash'] = new_hashed_password
176
+
177
+ success = save_primary_user_data(user, commit_msg=f"feat: Réinitialisation MDP utilisateur {user['user_id']}")
178
+
179
+ if success:
180
+ return True, "Mot de passe réinitialisé avec succès."
181
+ else:
182
+ return False, "Erreur interne lors de la mise à jour du mot de passe."
183
+
184
+
185
+ def update_user_plan(user_id: str, new_plan_id: str, stripe_subscription_id: Optional[str]) -> bool:
186
+ """Met à jour le plan et l'ID d'abonnement Stripe pour un utilisateur."""
187
+
188
+ user = get_user_by_id(user_id)
189
+
190
+ if not user:
191
+ print(f"Erreur: Utilisateur {user_id} non trouvé pour la mise à jour du plan.", file=sys.stderr)
192
+ return False
193
+
194
+ user['plan_id'] = new_plan_id
195
+ user['stripe_subscription_id'] = stripe_subscription_id
196
+ user['date_plan_start'] = datetime.now().isoformat()
197
+
198
+ success = save_primary_user_data(user, commit_msg=f"feat: Mise à jour du plan pour {user_id} vers {new_plan_id}")
199
+
200
+ return success
201
+
202
+
203
+ def update_user_profile(user_id: str, username: str, email: str, new_password: Optional[str] = None) -> tuple[bool, str]:
204
+ """
205
+ Met à jour le profil de l'utilisateur principal (client).
206
+ user_id: L'UUID de l'utilisateur.
207
+ username: Le nouveau nom d'utilisateur.
208
+ email: Le nouvel email.
209
+ new_password: Le nouveau mot de passe (si fourni et non vide).
210
+ """
211
+
212
+ # 1. Chargement des données existantes
213
+ user_data = load_primary_user_data(user_id)
214
+ if not user_data:
215
+ return False, "Erreur critique : Utilisateur introuvable pour la mise à jour."
216
+
217
+ original_email = user_data.get('email')
218
+
219
+ # 2. Validation et mise à jour de l'email
220
+ if email and email != original_email:
221
+ # Vérification si le nouvel email n'est pas déjà utilisé par un autre utilisateur
222
+ # Note: get_user_by_email retourne l'objet utilisateur, pas juste l'ID.
223
+ existing_user_by_email = get_user_by_email(email)
224
+
225
+ # S'il trouve un utilisateur ET que son ID ne correspond pas à l'utilisateur actuel,
226
+ # l'email est déjà pris.
227
+ if existing_user_by_email and existing_user_by_email.get('user_id') != user_id:
228
+ return False, "Cet email est déjà utilisé par un autre compte."
229
+
230
+ # Mise à jour de l'email si la validation passe
231
+ user_data['email'] = email
232
+
233
+ # 3. Mise à jour du nom d'utilisateur (pas de validation d'unicité assumée ici)
234
+ user_data['username'] = username
235
+
236
+ # 4. Mise à jour du mot de passe (si un nouveau est fourni)
237
+ if new_password:
238
+ if len(new_password) < 8:
239
+ # Assurez-vous que cette limite est cohérente avec la fonction register_user
240
+ return False, "Le nouveau mot de passe est trop court (min 8 caractères)."
241
+
242
+ user_data['password_hash'] = generate_password_hash(new_password)
243
+
244
+ # 5. Sauvegarde des données
245
+ # Le row_id est stocké dans l'objet utilisateur après la première sauvegarde.
246
+ baserow_row_id = user_data.get('baserow_row_id')
247
+
248
+ if baserow_row_id is None:
249
+ return False, "Erreur de configuration : ID de ligne Baserow manquant."
250
+
251
+ if save_primary_user_data(user_data, commit_msg=f"feat: Mise à jour du profil utilisateur {user_id}"):
252
+ return True, "Votre profil a été mis à jour avec succès."
253
+ else:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
  return False, "Une erreur s'est produite lors de la sauvegarde du profil."