@@ -214,48 +214,42 @@ async def health_check() -> HealthCheckResponse:
214214# Consent callback endpoint
215215@app .get ("/consent" , tags = ["OAuth" ])
216216async def consent_callback (
217+ request : Request ,
217218 code : str = Query (..., description = "Authorization code from Enedis" ),
218- state : str = Query (... , description = "State parameter containing user_id " ),
219+ state : str = Query (None , description = "State parameter (ignored - user identified via JWT) " ),
219220 usage_point_id : str = Query (None , description = "Usage point ID from Enedis" ),
220221 db : AsyncSession = Depends (get_db ),
221222) -> RedirectResponse :
222- """Handle consent redirect from Enedis and redirect to frontend dashboard"""
223+ """Handle consent redirect from Enedis and redirect to frontend dashboard.
224+
225+ The user is identified via their JWT token (from cookie or localStorage).
226+ The PDL(s) from Enedis are automatically added to the authenticated user's account.
227+ """
223228 # Frontend URL from settings
224229 frontend_url = f"{ settings .FRONTEND_URL } /dashboard"
225230
226231 logger .info ("=" * 60 )
227232 logger .debug ("[CONSENT] ===== DEBUT CALLBACK ENEDIS =====" )
228- logger .debug (f"[CONSENT] Code reçu: { code [:20 ]} ..." )
233+ logger .debug (f"[CONSENT] Code reçu: { code [:20 ]} ..." if code else "[CONSENT] Pas de code" )
229234 logger .debug (f"[CONSENT] State reçu: { state } " )
230235 logger .debug (f"[CONSENT] Usage Point ID reçu: { usage_point_id } " )
231236 logger .debug (f"[CONSENT] Frontend URL: { frontend_url } " )
232- logger .debug (f"[CONSENT] Redirect URI configuré: { settings .ENEDIS_REDIRECT_URI } " )
233237 logger .info ("=" * 60 )
234238
235239 try :
236- # Parse state - Enedis may return it with format "user_id:usage_point_id" or just "user_id"
237- # Extract only the user_id part (before the colon if present)
238- if ":" in state :
239- user_id = state .split (":" )[0 ].strip ()
240- else :
241- user_id = state .strip ()
242-
243- # Log for debugging
244- logger .debug (f"[CONSENT] User ID extrait du state: { user_id } " )
240+ # Get the authenticated user from JWT token
241+ from .middleware .auth import get_current_user_optional
245242
246- # Verify user exists
247- result = await db .execute (select (User ).where (User .id == user_id ))
248- user = result .scalar_one_or_none ()
243+ user = await get_current_user_optional (request , db )
249244
250245 if not user :
251- # Log all users for debugging
252- all_users = await db .execute (select (User ))
253- user_ids = [u .id for u in all_users .scalars ().all ()]
254- logger .error (f"[CONSENT] User not found with ID: { user_id } " )
255- logger .debug (f"[CONSENT] Available users: { user_ids } " )
256- # Redirect to frontend dashboard with error
257- error_msg = f"user_not_found (looking for: { user_id [:8 ]} ...)"
258- return RedirectResponse (url = f"{ frontend_url } ?consent_error={ error_msg } " )
246+ logger .error ("[CONSENT] ✗ Utilisateur non authentifié - redirection vers login" )
247+ # Redirect to login with return URL
248+ return_url = f"/oauth/callback?code={ code } &usage_point_id={ usage_point_id } " if usage_point_id else f"/oauth/callback?code={ code } "
249+ return RedirectResponse (url = f"{ settings .FRONTEND_URL } /login?redirect={ return_url } " )
250+
251+ user_id = user .id
252+ logger .info (f"[CONSENT] ✓ Utilisateur authentifié: { user .email } (ID: { user_id } )" )
259253
260254 # Just create PDL - token will be managed globally via Client Credentials
261255 logger .debug ("[CONSENT] ===== TRAITEMENT DU PDL =====" )
@@ -283,61 +277,76 @@ async def consent_callback(
283277 logger .warning (f"[CONSENT] ⚠ PDL ignoré (pas d'ID): { up } " )
284278 continue
285279
286- # Check if PDL already exists
280+ # Check if PDL already exists (globally)
287281 from .models import PDL
288282
289- result = await db .execute (select (PDL ).where (PDL .user_id == user_id , PDL . usage_point_id == usage_point_id ))
290- existing_pdl = result .scalar_one_or_none ()
283+ result = await db .execute (select (PDL ).where (PDL .usage_point_id == usage_point_id ))
284+ existing_pdl = result .scalars (). first ()
291285
292- if not existing_pdl :
293- # Create new PDL
286+ if existing_pdl :
287+ # PDL already exists - reject via consent flow
288+ # Admin must use the manual "Add PDL (admin)" button instead
289+ logger .warning (f"[CONSENT] ⚠ PDL { usage_point_id } existe déjà (user_id: { existing_pdl .user_id } ) - refusé" )
290+ return RedirectResponse (
291+ url = f"{ frontend_url } ?consent_error=pdl_already_exists&pdl={ usage_point_id } "
292+ )
293+
294+ # Create new PDL with race condition handling
295+ try :
294296 new_pdl = PDL (user_id = user_id , usage_point_id = usage_point_id )
295297 db .add (new_pdl )
296- await db .flush () # Flush to get the ID
298+ await db .flush () # Flush to get the ID - will raise IntegrityError if duplicate
297299 created_count += 1
298300 logger .info (f"[CONSENT] ✓ PDL créé: { usage_point_id } " )
299-
300- # Try to fetch contract info automatically
301- try :
302- from .adapters import enedis_adapter
303- from .routers .enedis import get_valid_token
304-
305- access_token = await get_valid_token (usage_point_id , user , db )
306- if access_token :
307- contract_data = await enedis_adapter .get_contract (usage_point_id , access_token )
308-
309- if (
310- contract_data
311- and "customer" in contract_data
312- and "usage_points" in contract_data ["customer" ]
313- ):
314- usage_points = contract_data ["customer" ]["usage_points" ]
315- if usage_points and len (usage_points ) > 0 :
316- usage_point = usage_points [0 ]
317-
318- if "contracts" in usage_point :
319- contract = usage_point ["contracts" ]
320-
321- if "subscribed_power" in contract :
322- power_str = str (contract ["subscribed_power" ])
323- new_pdl .subscribed_power = int (
324- power_str .replace ("kVA" , "" ).replace (" " , "" ).strip ()
325- )
326- print (
327- f"[CONSENT] ✓ Puissance souscrite récupérée: { new_pdl .subscribed_power } kVA"
328- )
329-
330- if "offpeak_hours" in contract :
331- offpeak = contract ["offpeak_hours" ]
332- if isinstance (offpeak , str ):
333- new_pdl .offpeak_hours = {"default" : offpeak }
334- elif isinstance (offpeak , dict ):
335- new_pdl .offpeak_hours = offpeak
336- logger .info (f"[CONSENT] ✓ Heures creuses récupérées: { new_pdl .offpeak_hours } " )
337- except Exception as e :
338- logger .warning (f"[CONSENT] ⚠ Impossible de récupérer les infos du contrat: { e } " )
339- else :
340- logger .debug (f"[CONSENT] PDL existe déjà: { usage_point_id } " )
301+ except Exception as e :
302+ # Handle race condition: another request created the PDL between our check and insert
303+ if "UNIQUE constraint failed" in str (e ) or "duplicate key" in str (e ).lower ():
304+ logger .warning (f"[CONSENT] ⚠ PDL { usage_point_id } créé par requête concurrente - abandon silencieux" )
305+ # Don't redirect with error - let the first request handle it
306+ # Just return empty response to avoid double redirect
307+ from fastapi .responses import Response
308+ return Response (status_code = 204 ) # No Content - browser will ignore
309+ raise # Re-raise other exceptions
310+
311+ # Try to fetch contract info automatically
312+ try :
313+ from .adapters import enedis_adapter
314+ from .routers .enedis import get_valid_token
315+
316+ access_token = await get_valid_token (usage_point_id , user , db )
317+ if access_token :
318+ contract_data = await enedis_adapter .get_contract (usage_point_id , access_token )
319+
320+ if (
321+ contract_data
322+ and "customer" in contract_data
323+ and "usage_points" in contract_data ["customer" ]
324+ ):
325+ usage_points_data = contract_data ["customer" ]["usage_points" ]
326+ if usage_points_data and len (usage_points_data ) > 0 :
327+ usage_point_data = usage_points_data [0 ]
328+
329+ if "contracts" in usage_point_data :
330+ contract = usage_point_data ["contracts" ]
331+
332+ if "subscribed_power" in contract :
333+ power_str = str (contract ["subscribed_power" ])
334+ new_pdl .subscribed_power = int (
335+ power_str .replace ("kVA" , "" ).replace (" " , "" ).strip ()
336+ )
337+ logger .info (
338+ f"[CONSENT] ✓ Puissance souscrite récupérée: { new_pdl .subscribed_power } kVA"
339+ )
340+
341+ if "offpeak_hours" in contract :
342+ offpeak = contract ["offpeak_hours" ]
343+ if isinstance (offpeak , str ):
344+ new_pdl .offpeak_hours = {"default" : offpeak }
345+ elif isinstance (offpeak , dict ):
346+ new_pdl .offpeak_hours = offpeak
347+ logger .info (f"[CONSENT] ✓ Heures creuses récupérées: { new_pdl .offpeak_hours } " )
348+ except Exception as e :
349+ logger .warning (f"[CONSENT] ⚠ Impossible de récupérer les infos du contrat: { e } " )
341350
342351 await db .commit ()
343352 logger .info ("[CONSENT] ✓ Commit effectué en base de données" )
0 commit comments