11import os
22import secrets
3+ from sqlite3 import IntegrityError
34
45from authlib .integrations .flask_client import OAuth
56from flask import current_app
@@ -20,6 +21,12 @@ def __init__(self):
2021 super ().__init__ (OrcidRepository ())
2122 self .client_id = self .get_orcid_client_id ()
2223 self .client_secret = self .get_orcid_client_secret ()
24+
25+ if not self .client_id or not self .client_secret :
26+ # This will be caught by the routes and shown as flash if you wrap service usage,
27+ # but right now you're creating it in before_app_request, so this would 500 early.
28+ current_app .logger .error ("ORCID_CLIENT_ID/ORCID_CLIENT_SECRET not configured" )
29+ # If you want to avoid raising here, set a flag and handle it in routes.
2330 self .oauth , self .orcid_client = self .configure_oauth (current_app )
2431
2532 def get_orcid_client_id (self ):
@@ -44,54 +51,67 @@ def configure_oauth(self, app):
4451 return oauth , orcid
4552
4653 def get_orcid_user_info (self , token ):
47- # Valida respuesta
48- resp = self .orcid_client .get ("https://orcid.org/oauth/userinfo" , token = token )
49- if not resp or resp .status_code != 200 :
50- current_app .logger .error ("ORCID userinfo fallo: %s" , getattr (resp , "text" , None ))
51- return None
54+ try :
55+ resp = self .orcid_client .get ("https://orcid.org/oauth/userinfo" , token = token )
56+ except Exception as exc :
57+ current_app .logger .exception ("ORCID userinfo request failed: %s" , exc )
58+ return None , "Could not reach ORCID. Please try again."
59+
60+ if not resp :
61+ current_app .logger .error ("ORCID userinfo empty response" )
62+ return None , "ORCID did not return user information. Please try again."
63+
64+ if resp .status_code != 200 :
65+ current_app .logger .error ("ORCID userinfo failed (%s): %s" , resp .status_code , getattr (resp , "text" , None ))
66+
67+ # Optional: special-case rate limiting
68+ if resp .status_code == 429 :
69+ return None , "ORCID is rate-limiting requests. Please try again in a minute."
70+
71+ return None , "ORCID user information request failed. Please try again."
72+
5273 data = resp .json () or {}
53- # 'sub' es el ORCID iD en OIDC
54- if "sub" not in data :
55- current_app .logger .error ("ORCID userinfo sin 'sub': %s" , data )
56- return None
57- return data
74+
75+ orcid_id = (data .get ("sub" ) or "" ).strip ()
76+ if not orcid_id :
77+ current_app .logger .error ("ORCID userinfo missing 'sub': %s" , data )
78+ return None , "ORCID did not provide an ORCID iD. Please try again."
79+
80+ return data , None
5881
5982 def get_or_create_user (self , user_info ):
6083 if not user_info :
61- return None
84+ return None , "Missing ORCID user information."
6285
6386 orcid_id = (user_info .get ("sub" ) or "" ).strip ()
6487 if not orcid_id :
65- return None
88+ return None , "Missing ORCID iD."
6689
67- given_name = user_info .get ("given_name" ) or ""
68- family_name = user_info .get ("family_name" ) or ""
69- affiliation = user_info .get ("affiliation" ) or "" # si no existe, queda ""
90+ given_name = ( user_info .get ("given_name" ) or "" ). strip ()
91+ family_name = ( user_info .get ("family_name" ) or "" ). strip ()
92+ affiliation = ( user_info .get ("affiliation" ) or "" ). strip ()
7093
7194 try :
72- # 1) ¿Existe registro ORCID?
95+ # 1) Existing ORCID link ?
7396 orcid_record = Orcid .query .filter_by (orcid_id = orcid_id ).first ()
74-
7597 if orcid_record :
7698 profile = UserProfile .query .get (orcid_record .profile_id )
7799 user = User .query .get (profile .user_id ) if profile else None
78100
79101 if user :
80- return user
102+ return user , None
81103
82- # Enlace roto: limpiamos y reconstruimos
83- if not profile or not user :
84- db .session .delete (orcid_record )
85- db .session .flush () # no commit aún, seguimos para recrear
104+ # Broken link: remove and recreate
105+ db .session .delete (orcid_record )
106+ db .session .flush ()
86107
87- # 2) Crear usuario + perfil + enlace ORCID
108+ # 2) Create new user + profile + ORCID link
88109 user = User (
89- # puedes guardar email si algún día lo pides a ORCID con más scope
90110 password = generate_password_hash (secrets .token_urlsafe (24 )),
91111 active = True ,
92112 )
93113 db .session .add (user )
94- db .session .flush () # para obtener user.id
114+ db .session .flush ()
95115
96116 profile = UserProfile (
97117 user_id = user .id ,
@@ -106,9 +126,27 @@ def get_or_create_user(self, user_info):
106126 db .session .add (orcid_record )
107127
108128 db .session .commit ()
109- return user
129+ return user , None
130+
131+ except IntegrityError as exc :
132+ # Typical race condition: two callbacks creating the same ORCID simultaneously
133+ current_app .logger .warning ("IntegrityError creating ORCID user (%s): %s" , orcid_id , exc )
134+ db .session .rollback ()
110135
111- except SQLAlchemyError as e :
112- current_app .logger .exception ("Error creando usuario ORCID: %s" , e )
136+ # Re-read and return the existing user if it was created in parallel
137+ try :
138+ orcid_record = Orcid .query .filter_by (orcid_id = orcid_id ).first ()
139+ if orcid_record :
140+ profile = UserProfile .query .get (orcid_record .profile_id )
141+ user = User .query .get (profile .user_id ) if profile else None
142+ if user :
143+ return user , None
144+ except Exception as reread_exc :
145+ current_app .logger .exception ("Failed to reread ORCID user after IntegrityError: %s" , reread_exc )
146+
147+ return None , "Could not create your account due to a concurrency issue. Please try again."
148+
149+ except SQLAlchemyError as exc :
150+ current_app .logger .exception ("Database error creating ORCID user (%s): %s" , orcid_id , exc )
113151 db .session .rollback ()
114- return None
152+ return None , "Could not create your account due to a database error. Please try again."
0 commit comments