Merge pull request #10 from uja-dev-practices/feature/backend-v4

feat: implement ORCID OAuth callback and update environment configura…
This commit is contained in:
Mireya Cueto Garrido
2026-04-29 12:20:24 +02:00
committed by GitHub
4 changed files with 52 additions and 19 deletions
+30 -18
View File
@@ -1,5 +1,7 @@
import httpx import httpx
import os import os
from pathlib import Path
from dotenv import load_dotenv
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.responses import RedirectResponse from fastapi.responses import RedirectResponse
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
@@ -11,6 +13,10 @@ from app.security.jwt import create_access_token
from app.services.orcid_client import ORCIDClient from app.services.orcid_client import ORCIDClient
from app.utils.orcid_validator import is_valid_orcid from app.utils.orcid_validator import is_valid_orcid
# Asegura que al ejecutar `uvicorn` local también se carga `backend/.env`.
_ENV_PATH = Path(__file__).resolve().parents[2] / ".env"
load_dotenv(dotenv_path=_ENV_PATH, override=False)
router = APIRouter(prefix="/auth", tags=["auth"]) router = APIRouter(prefix="/auth", tags=["auth"])
@@ -29,25 +35,12 @@ def _orcid_redirect_uri() -> str:
return os.getenv("ORCID_REDIRECT_URI") or "http://localhost:8000/api/auth/orcid/callback" return os.getenv("ORCID_REDIRECT_URI") or "http://localhost:8000/api/auth/orcid/callback"
@router.get("/orcid/authorize") def _complete_oauth_login(*, code: str, db: Session) -> OrcidLoginResponseSchema:
def authorize_orcid():
""" """
Inicia el flujo OAuth 3-legged (authorization code) hacia ORCID. Completa el login OAuth:
""" 1) intercambio del `code` en ORCID (server-side)
client = ORCIDClient() 2) crea/actualiza el investigador
authorize_url = client.build_authorize_url( 3) emite nuestro JWT
redirect_uri=_orcid_redirect_uri(),
# Solo necesitamos el Authenticated iD del usuario.
scope="/authenticate",
)
return RedirectResponse(authorize_url)
@router.get("/orcid/callback", response_model=OrcidLoginResponseSchema)
def orcid_callback(code: str, db: Session = Depends(get_db)):
"""
Recibe el `code` devuelto por ORCID, lo intercambia por tokens en el servidor
y emite nuestro JWT solo para el ORCID autenticado por ORCID.
""" """
if not code: if not code:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Missing ORCID authorization code") raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Missing ORCID authorization code")
@@ -95,3 +88,22 @@ def orcid_callback(code: str, db: Session = Depends(get_db)):
token = create_access_token(subject=orcid_id, extra={"rid": str(researcher.id)}) token = create_access_token(subject=orcid_id, extra={"rid": str(researcher.id)})
return OrcidLoginResponseSchema(access_token=token) return OrcidLoginResponseSchema(access_token=token)
@router.get("/orcid/authorize")
def authorize_orcid():
"""
Inicia el flujo OAuth 3-legged (authorization code) hacia ORCID.
"""
client = ORCIDClient()
authorize_url = client.build_authorize_url(
redirect_uri=_orcid_redirect_uri(),
# Solo necesitamos el Authenticated iD del usuario.
scope="/authenticate",
)
return RedirectResponse(authorize_url)
@router.get("/orcid/callback", response_model=OrcidLoginResponseSchema)
def orcid_callback(code: str, db: Session = Depends(get_db)):
return _complete_oauth_login(code=code, db=db)
+14 -1
View File
@@ -1,10 +1,14 @@
from fastapi import FastAPI from fastapi import Depends, FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.orm import Session
from app.db.session import init_db from app.db.session import init_db
from app.db.session import get_db
from app.api.researchers import router as researchers_router from app.api.researchers import router as researchers_router
from app.api.export import router as export_router from app.api.export import router as export_router
from app.api.auth import router as auth_router from app.api.auth import router as auth_router
from app.api.auth import _complete_oauth_login
from app.schema.auth import OrcidLoginResponseSchema
from app.scheduler.sync_scheduler import start_scheduler from app.scheduler.sync_scheduler import start_scheduler
@@ -35,6 +39,15 @@ def health():
return {"status": "ok"} return {"status": "ok"}
@app.get("/callback", response_model=OrcidLoginResponseSchema)
def oauth_callback_root(code: str, db: Session = Depends(get_db)):
"""
Alias para probar redirect URIs como `https://127.0.0.1/callback` en local.
Intercambia el code con ORCID y emite el JWT.
"""
return _complete_oauth_login(code=code, db=db)
# --------------------------------------------------------- # ---------------------------------------------------------
# Registrar routers # Registrar routers
# --------------------------------------------------------- # ---------------------------------------------------------
+7
View File
@@ -1,7 +1,9 @@
import os import os
import urllib.parse import urllib.parse
from pathlib import Path
from typing import Any, Optional from typing import Any, Optional
from dotenv import load_dotenv
import httpx import httpx
TOKEN_URL_SANDBOX = "https://sandbox.orcid.org/oauth/token" TOKEN_URL_SANDBOX = "https://sandbox.orcid.org/oauth/token"
@@ -15,6 +17,11 @@ BASE_URL_SANDBOX = "https://pub.sandbox.orcid.org/v3.0"
class ORCIDClient: class ORCIDClient:
def __init__(self): def __init__(self):
# Asegura que al ejecutar `uvicorn` local también se carga `backend/.env`.
# (En docker `ORCID_REDIRECT_URI` y secretos llegan por env_file, así que esto no molesta.)
_env_path = Path(__file__).resolve().parents[2] / ".env"
load_dotenv(dotenv_path=_env_path, override=False)
self.client_id = os.getenv("ORCID_CLIENT_ID") self.client_id = os.getenv("ORCID_CLIENT_ID")
self.client_secret = os.getenv("ORCID_CLIENT_SECRET") self.client_secret = os.getenv("ORCID_CLIENT_SECRET")
self._token_cache: Optional[str] = None self._token_cache: Optional[str] = None
+1
View File
@@ -11,6 +11,7 @@ services:
environment: environment:
DATABASE_URL: postgresql://postgres:postgres@db:5432/orcid_db DATABASE_URL: postgresql://postgres:postgres@db:5432/orcid_db
REDIS_URL: redis://redis:6379/0 REDIS_URL: redis://redis:6379/0
ORCID_REDIRECT_URI: https://willfully-brunette-antennae.ngrok-free.dev/callback
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy