Skip to content

Commit 9a05c88

Browse files
committed
fix: Fixes 500 ORCID bug. Closes #96
1 parent 635ce9d commit 9a05c88

File tree

3 files changed

+282
-50
lines changed

3 files changed

+282
-50
lines changed

app/modules/orcid/routes.py

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from flask import current_app, redirect, url_for
1+
from flask import current_app, flash, redirect, url_for
22
from flask_login import login_user
33

44
from app.modules.orcid import orcid_bp
@@ -7,19 +7,43 @@
77

88
@orcid_bp.before_app_request
99
def before_request():
10+
# If you keep this pattern for now, at least guarantee the service exists.
11+
# Consider moving OAuth client initialization to app factory later.
1012
current_app.orcid_service = OrcidService()
1113

1214

1315
@orcid_bp.route("/orcid/login")
1416
def login():
1517
redirect_uri = url_for("orcid.authorize", _external=True, _scheme="https")
16-
return current_app.orcid_service.orcid_client.authorize_redirect(redirect_uri)
18+
19+
try:
20+
return current_app.orcid_service.orcid_client.authorize_redirect(redirect_uri)
21+
except Exception as exc:
22+
current_app.logger.exception("ORCID authorize_redirect failed: %s", exc)
23+
flash("ORCID login failed. Please try again.", "danger")
24+
return redirect(url_for("auth.login")) # adjust to your login route
1725

1826

1927
@orcid_bp.route("/orcid/authorize")
2028
def authorize():
21-
token = current_app.orcid_service.orcid_client.authorize_access_token()
22-
user_info = current_app.orcid_service.get_orcid_user_info(token)
23-
user = current_app.orcid_service.get_or_create_user(user_info)
29+
try:
30+
token = current_app.orcid_service.orcid_client.authorize_access_token()
31+
except Exception as exc:
32+
# Covers state mismatch, missing session, provider errors, etc.
33+
current_app.logger.exception("ORCID authorize_access_token failed: %s", exc)
34+
flash("ORCID authorization failed (invalid session or provider error). Please try again.", "danger")
35+
return redirect(url_for("auth.login"))
36+
37+
user_info, err = current_app.orcid_service.get_orcid_user_info(token)
38+
if err:
39+
flash(err, "danger")
40+
return redirect(url_for("auth.login"))
41+
42+
user, err = current_app.orcid_service.get_or_create_user(user_info)
43+
if err:
44+
flash(err, "danger")
45+
return redirect(url_for("auth.login"))
46+
2447
login_user(user)
25-
return redirect("/")
48+
flash("Signed in with ORCID.", "success")
49+
return redirect(url_for("public.index")) # adjust to your home route

app/modules/orcid/services.py

Lines changed: 67 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22
import secrets
3+
from sqlite3 import IntegrityError
34

45
from authlib.integrations.flask_client import OAuth
56
from 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

Comments
 (0)