From 8d29fb054dd03c1e3f33c97df7366bfcd41c3cd1 Mon Sep 17 00:00:00 2001 From: Alexis Date: Mon, 18 May 2026 11:10:27 +0200 Subject: [PATCH] fix: update API key dependency handling in export endpoints and improve documentation for export URLs --- backend/app/api/export.py | 21 ++++++--------------- frontend/src/services/api.js | 23 +++++++++++++++-------- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/backend/app/api/export.py b/backend/app/api/export.py index c3a9a6a..1c04c01 100644 --- a/backend/app/api/export.py +++ b/backend/app/api/export.py @@ -9,7 +9,7 @@ from app.core.config import settings from app.core.rate_limit import limiter from app.db.models import Publication, PublicationDownload, Researcher from app.db.session import get_db -from app.security.api_key import get_api_key_optional +from app.security.api_key import get_api_key from app.security.jwt import get_optional_current_researcher from app.services.sword_generator import SWORDGenerator from app.services.zip_generator import ZIPGenerator @@ -19,11 +19,6 @@ from app.utils.orcid_validator import ORCID_PATTERN, is_valid_orcid router = APIRouter(prefix="/export") -def _ensure_credentials(api_key: str | None, current: Researcher | None) -> None: - if not api_key and not current: - raise HTTPException(status_code=401, detail="Authentication required") - - def _record_downloads(db: Session, current: Researcher, pubs: Iterable[Publication]) -> None: """ Inserta marcadores de descarga (researcher_id, publication_id). @@ -94,10 +89,9 @@ async def export_multiple_sword( request: Request, pub_ids: List[UUID] = Body(..., min_length=1, max_length=settings.MAX_PUB_IDS_BATCH), db: Session = Depends(get_db), - api_key: str | None = Depends(get_api_key_optional), + _: str = Depends(get_api_key), current: Researcher | None = Depends(get_optional_current_researcher), ): - _ensure_credentials(api_key, current) _validate_pub_ids(pub_ids) pubs = db.query(Publication).filter(Publication.id.in_(pub_ids)).all() @@ -124,10 +118,9 @@ async def export_researcher_sword( request: Request, orcid_id: str = Path(min_length=19, max_length=19, pattern=ORCID_PATTERN), db: Session = Depends(get_db), - api_key: str | None = Depends(get_api_key_optional), + _: str = Depends(get_api_key), current: Researcher | None = Depends(get_optional_current_researcher), ): - _ensure_credentials(api_key, current) if not is_valid_orcid(orcid_id): raise HTTPException(status_code=400, detail="Invalid ORCID iD") @@ -156,10 +149,9 @@ async def export_multiple_zip( request: Request, pub_ids: List[UUID] = Body(..., min_length=1, max_length=settings.MAX_PUB_IDS_BATCH), db: Session = Depends(get_db), - api_key: str | None = Depends(get_api_key_optional), + _: str = Depends(get_api_key), current: Researcher | None = Depends(get_optional_current_researcher), ): - _ensure_credentials(api_key, current) _validate_pub_ids(pub_ids) pubs = db.query(Publication).filter(Publication.id.in_(pub_ids)).all() @@ -186,10 +178,9 @@ async def export_researcher_zip( request: Request, orcid_id: str = Path(min_length=19, max_length=19, pattern=ORCID_PATTERN), db: Session = Depends(get_db), - api_key: str | None = Depends(get_api_key_optional), + _: str = Depends(get_api_key), current: Researcher | None = Depends(get_optional_current_researcher), ): - _ensure_credentials(api_key, current) if not is_valid_orcid(orcid_id): raise HTTPException(status_code=400, detail="Invalid ORCID iD") @@ -205,4 +196,4 @@ async def export_researcher_zip( if current: _record_downloads(db, current, pubs) - return Response(content=zip_bytes, media_type="application/zip") + return Response(content=zip_bytes, media_type="application/zip") \ No newline at end of file diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 390987e..443cad2 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -17,10 +17,10 @@ * - GET /researchers/search → buscador grupal (todo en uno) * - GET /researchers/search/{orcid_id} → buscador individual (todo en uno) * - POST /researchers/{orcid_id}/sync → re-sync manual - * - POST /export/sword/publications body=[ids] → SWORD XML de selección - * - POST /export/zip/publications body=[ids] → ZIP de selección - * - GET /export/sword/researcher/{orcid_id} → SWORD XML de todo el investigador - * - GET /export/zip/researcher/{orcid_id} → ZIP de todo el investigador + * - POST /export/sword/publications body=[ids] → SWORD XML de selección (requiere X-API-Key) + * - POST /export/zip/publications body=[ids] → ZIP de selección (requiere X-API-Key) + * - GET /export/sword/researcher/{orcid_id} → SWORD XML de todo el investigador (requiere X-API-Key) + * - GET /export/zip/researcher/{orcid_id} → ZIP de todo el investigador (requiere X-API-Key) */ import { @@ -390,9 +390,9 @@ function exportSegmentFor(format) { * dato meramente informativo en los toasts de éxito; las descargas * reales se disparan vía blob para poder forzar el download. * - * Ojo: estas URLs requieren `X-API-Key`, así que NO sirven como link - * directo en una etiqueta ``; las exponemos para mostrarlas o - * loguearlas, no para navegar. + * El backend exige la cabecera `X-API-Key` (misma que `VITE_API_KEY` en el + * front). No sirven como `` simple: hay que descargar con `fetch` + * (p. ej. `downloadExport`) o añadir la cabecera de otro modo. */ export function getExportUrl(orcidId, format) { const segment = exportSegmentFor(format); @@ -420,6 +420,13 @@ export async function downloadExport( return { blob: null, url: getExportUrl(orcidId, format) }; } + if (!API_KEY) { + throw new ApiError( + "Configura VITE_API_KEY (debe coincidir con API_KEY_VALUE del backend): las exportaciones exigen la cabecera X-API-Key.", + { status: 401, payload: { missingApiKey: true } }, + ); + } + const segment = exportSegmentFor(format); const ids = Array.isArray(publicationIds) && publicationIds.length > 0 @@ -468,4 +475,4 @@ export async function downloadExport( } const blob = await response.blob(); return { blob, url }; -} +} \ No newline at end of file