fix: update API key dependency handling in export endpoints and improve documentation for export URLs
This commit is contained in:
@@ -9,7 +9,7 @@ from app.core.config import settings
|
|||||||
from app.core.rate_limit import limiter
|
from app.core.rate_limit import limiter
|
||||||
from app.db.models import Publication, PublicationDownload, Researcher
|
from app.db.models import Publication, PublicationDownload, Researcher
|
||||||
from app.db.session import get_db
|
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.security.jwt import get_optional_current_researcher
|
||||||
from app.services.sword_generator import SWORDGenerator
|
from app.services.sword_generator import SWORDGenerator
|
||||||
from app.services.zip_generator import ZIPGenerator
|
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")
|
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:
|
def _record_downloads(db: Session, current: Researcher, pubs: Iterable[Publication]) -> None:
|
||||||
"""
|
"""
|
||||||
Inserta marcadores de descarga (researcher_id, publication_id).
|
Inserta marcadores de descarga (researcher_id, publication_id).
|
||||||
@@ -94,10 +89,9 @@ async def export_multiple_sword(
|
|||||||
request: Request,
|
request: Request,
|
||||||
pub_ids: List[UUID] = Body(..., min_length=1, max_length=settings.MAX_PUB_IDS_BATCH),
|
pub_ids: List[UUID] = Body(..., min_length=1, max_length=settings.MAX_PUB_IDS_BATCH),
|
||||||
db: Session = Depends(get_db),
|
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),
|
current: Researcher | None = Depends(get_optional_current_researcher),
|
||||||
):
|
):
|
||||||
_ensure_credentials(api_key, current)
|
|
||||||
_validate_pub_ids(pub_ids)
|
_validate_pub_ids(pub_ids)
|
||||||
|
|
||||||
pubs = db.query(Publication).filter(Publication.id.in_(pub_ids)).all()
|
pubs = db.query(Publication).filter(Publication.id.in_(pub_ids)).all()
|
||||||
@@ -124,10 +118,9 @@ async def export_researcher_sword(
|
|||||||
request: Request,
|
request: Request,
|
||||||
orcid_id: str = Path(min_length=19, max_length=19, pattern=ORCID_PATTERN),
|
orcid_id: str = Path(min_length=19, max_length=19, pattern=ORCID_PATTERN),
|
||||||
db: Session = Depends(get_db),
|
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),
|
current: Researcher | None = Depends(get_optional_current_researcher),
|
||||||
):
|
):
|
||||||
_ensure_credentials(api_key, current)
|
|
||||||
if not is_valid_orcid(orcid_id):
|
if not is_valid_orcid(orcid_id):
|
||||||
raise HTTPException(status_code=400, detail="Invalid ORCID iD")
|
raise HTTPException(status_code=400, detail="Invalid ORCID iD")
|
||||||
|
|
||||||
@@ -156,10 +149,9 @@ async def export_multiple_zip(
|
|||||||
request: Request,
|
request: Request,
|
||||||
pub_ids: List[UUID] = Body(..., min_length=1, max_length=settings.MAX_PUB_IDS_BATCH),
|
pub_ids: List[UUID] = Body(..., min_length=1, max_length=settings.MAX_PUB_IDS_BATCH),
|
||||||
db: Session = Depends(get_db),
|
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),
|
current: Researcher | None = Depends(get_optional_current_researcher),
|
||||||
):
|
):
|
||||||
_ensure_credentials(api_key, current)
|
|
||||||
_validate_pub_ids(pub_ids)
|
_validate_pub_ids(pub_ids)
|
||||||
|
|
||||||
pubs = db.query(Publication).filter(Publication.id.in_(pub_ids)).all()
|
pubs = db.query(Publication).filter(Publication.id.in_(pub_ids)).all()
|
||||||
@@ -186,10 +178,9 @@ async def export_researcher_zip(
|
|||||||
request: Request,
|
request: Request,
|
||||||
orcid_id: str = Path(min_length=19, max_length=19, pattern=ORCID_PATTERN),
|
orcid_id: str = Path(min_length=19, max_length=19, pattern=ORCID_PATTERN),
|
||||||
db: Session = Depends(get_db),
|
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),
|
current: Researcher | None = Depends(get_optional_current_researcher),
|
||||||
):
|
):
|
||||||
_ensure_credentials(api_key, current)
|
|
||||||
if not is_valid_orcid(orcid_id):
|
if not is_valid_orcid(orcid_id):
|
||||||
raise HTTPException(status_code=400, detail="Invalid ORCID iD")
|
raise HTTPException(status_code=400, detail="Invalid ORCID iD")
|
||||||
|
|
||||||
@@ -205,4 +196,4 @@ async def export_researcher_zip(
|
|||||||
if current:
|
if current:
|
||||||
_record_downloads(db, current, pubs)
|
_record_downloads(db, current, pubs)
|
||||||
|
|
||||||
return Response(content=zip_bytes, media_type="application/zip")
|
return Response(content=zip_bytes, media_type="application/zip")
|
||||||
@@ -17,10 +17,10 @@
|
|||||||
* - GET /researchers/search → buscador grupal (todo en uno)
|
* - GET /researchers/search → buscador grupal (todo en uno)
|
||||||
* - GET /researchers/search/{orcid_id} → buscador individual (todo en uno)
|
* - GET /researchers/search/{orcid_id} → buscador individual (todo en uno)
|
||||||
* - POST /researchers/{orcid_id}/sync → re-sync manual
|
* - POST /researchers/{orcid_id}/sync → re-sync manual
|
||||||
* - POST /export/sword/publications body=[ids] → SWORD XML de selección
|
* - 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
|
* - 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
|
* - 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
|
* - GET /export/zip/researcher/{orcid_id} → ZIP de todo el investigador (requiere X-API-Key)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -390,9 +390,9 @@ function exportSegmentFor(format) {
|
|||||||
* dato meramente informativo en los toasts de éxito; las descargas
|
* dato meramente informativo en los toasts de éxito; las descargas
|
||||||
* reales se disparan vía blob para poder forzar el download.
|
* reales se disparan vía blob para poder forzar el download.
|
||||||
*
|
*
|
||||||
* Ojo: estas URLs requieren `X-API-Key`, así que NO sirven como link
|
* El backend exige la cabecera `X-API-Key` (misma que `VITE_API_KEY` en el
|
||||||
* directo en una etiqueta `<a href>`; las exponemos para mostrarlas o
|
* front). No sirven como `<a href>` simple: hay que descargar con `fetch`
|
||||||
* loguearlas, no para navegar.
|
* (p. ej. `downloadExport`) o añadir la cabecera de otro modo.
|
||||||
*/
|
*/
|
||||||
export function getExportUrl(orcidId, format) {
|
export function getExportUrl(orcidId, format) {
|
||||||
const segment = exportSegmentFor(format);
|
const segment = exportSegmentFor(format);
|
||||||
@@ -420,6 +420,13 @@ export async function downloadExport(
|
|||||||
return { blob: null, url: getExportUrl(orcidId, format) };
|
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 segment = exportSegmentFor(format);
|
||||||
const ids =
|
const ids =
|
||||||
Array.isArray(publicationIds) && publicationIds.length > 0
|
Array.isArray(publicationIds) && publicationIds.length > 0
|
||||||
@@ -468,4 +475,4 @@ export async function downloadExport(
|
|||||||
}
|
}
|
||||||
const blob = await response.blob();
|
const blob = await response.blob();
|
||||||
return { blob, url };
|
return { blob, url };
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user