fix: update frontend API key handling and improve export documentation

This commit is contained in:
Alexis
2026-05-19 09:52:50 +02:00
parent 8d29fb054d
commit 59eda988d2
8 changed files with 79 additions and 42 deletions
+2 -1
View File
@@ -1,4 +1,5 @@
VITE_API_URL=https://api.tudominio.com/api
VITE_API_PROXY_TARGET=
VITE_API_KEY=<misma API_KEY_VALUE backend si la usas desde frontend>
# Debe coincidir con API_KEY_VALUE del backend. La inyecta el proxy (Vite/nginx).
VITE_API_KEY=
VITE_USE_MOCKS=false
+1 -1
View File
@@ -20,4 +20,4 @@ COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=builder /app/dist /app/dist
EXPOSE 5173
CMD ["nginx", "-g", "daemon off;"]
CMD ["/bin/sh", "-c", "sed -i \"s|__API_KEY__|${API_KEY_VALUE:-$VITE_API_KEY}|g\" /etc/nginx/conf.d/default.conf && exec nginx -g 'daemon off;'"]
+2
View File
@@ -49,6 +49,8 @@ server {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Sustituido al arrancar el contenedor (ver CMD en Dockerfile).
proxy_set_header X-API-Key __API_KEY__;
}
}
+12 -24
View File
@@ -10,17 +10,18 @@
* (p. ej. `http://localhost:8000/api`). Si se deja vacío, las
* peticiones se hacen contra `/api` y las redirige el proxy de
* Vite (ver `vite.config.js`).
* - `VITE_API_KEY`: clave compartida con el backend, se manda en el
* header `X-API-Key` de TODAS las peticiones.
* - `VITE_API_KEY` / `API_KEY_VALUE`: opcional en el navegador. En
* producción y dev la inyecta el proxy (nginx / Vite). Solo hace falta
* en el bundle si el front llama al backend sin proxy intermedio.
*
* Contrato actual del backend (todo bajo `/api`):
* - 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 (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)
* - POST /export/sword/publications body=[ids] → SWORD XML (API key o JWT)
* - POST /export/zip/publications body=[ids] → ZIP (API key o JWT)
* - GET /export/sword/researcher/{orcid_id} → SWORD XML (API key o JWT)
* - GET /export/zip/researcher/{orcid_id} → ZIP (API key o JWT)
*/
import {
@@ -35,6 +36,7 @@ import {
const BASE_URL = (import.meta.env.VITE_API_URL
? import.meta.env.VITE_API_URL
: `${import.meta.env.BASE_URL}api`).replace(/\/$/, "");
// Fallback solo si no hay proxy que inyecte la cabecera (p. ej. VITE_API_URL absoluta).
const API_KEY = import.meta.env.VITE_API_KEY ?? "";
const USE_MOCKS = import.meta.env.VITE_USE_MOCKS === "true";
@@ -81,16 +83,10 @@ export class ApiError extends Error {
}
/**
* Construye la cabecera base que llevan TODAS las peticiones (incluidas
* las descargas de blob). Incluye X-API-Key siempre y, si existe un JWT
* en localStorage, también Authorization: Bearer <token>.
* Cabeceras base del navegador. El proxy (nginx/Vite) inyecta X-API-Key en
* rutas /api; aquí solo la añadimos como fallback directo al backend.
*/
function buildAuthHeaders(extra = {}) {
if (!API_KEY && import.meta.env.DEV) {
console.warn(
"[api] VITE_API_KEY no está definida; las peticiones serán rechazadas por el backend.",
);
}
const token = localStorage.getItem("orcid_auth_token");
return {
Accept: "application/json",
@@ -390,9 +386,8 @@ function exportSegmentFor(format) {
* dato meramente informativo en los toasts de éxito; las descargas
* reales se disparan vía blob para poder forzar el download.
*
* El backend exige la cabecera `X-API-Key` (misma que `VITE_API_KEY` en el
* front). No sirven como `<a href>` simple: hay que descargar con `fetch`
* (p. ej. `downloadExport`) o añadir la cabecera de otro modo.
* Requiere API key (inyectada por proxy) o JWT. No sirve como `<a href>` simple;
* usar `downloadExport` para POST con IDs o Bearer opcional.
*/
export function getExportUrl(orcidId, format) {
const segment = exportSegmentFor(format);
@@ -420,13 +415,6 @@ 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
+18 -6
View File
@@ -6,6 +6,19 @@ export default defineConfig(({ mode }) => {
const env = loadEnv(mode, import.meta.dirname, '')
const proxyTarget = env.VITE_API_PROXY_TARGET || 'http://localhost:8000'
const base = env.VITE_BASE_PATH || '/'
const proxyApiKey = env.API_KEY_VALUE || env.VITE_API_KEY || ''
const proxyCommon = {
target: proxyTarget,
changeOrigin: true,
...(proxyApiKey && {
configure: (proxy) => {
proxy.on('proxyReq', (proxyReq) => {
proxyReq.setHeader('X-API-Key', proxyApiKey)
})
},
}),
}
return {
base,
@@ -15,16 +28,15 @@ export default defineConfig(({ mode }) => {
allowedHosts: true,
port: 5173,
proxy: {
'/api': { target: proxyTarget, changeOrigin: true },
'/api': proxyCommon,
'/health': { target: proxyTarget, changeOrigin: true },
...(base !== '/' && {
[`${base}api`]: {
target: proxyTarget,
changeOrigin: true,
rewrite: (path) => path.replace(new RegExp(`^${base}api`), '/api')
}
...proxyCommon,
rewrite: (path) => path.replace(new RegExp(`^${base}api`), '/api'),
},
}),
},
},
}
})
})