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:
+30
-18
@@ -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
@@ -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
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user