diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..506e0f0 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +# Detect text files and normalize line endings in the index (cross‑platform). +* text=auto + +# YAML / CI: always LF so Linux runners and merges match Windows checkouts. +*.yml text eol=lf +*.yaml text eol=lf diff --git a/.gitignore b/.gitignore index 6932fd8..fe9544d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,3 @@ -# --- GLOBAL --- -.env -*.env -.env.* -!.env.example - # --- PYTHON BACKEND --- __pycache__/ diff --git a/README.md b/README.md index 1e6e5da..3a3e436 100644 --- a/README.md +++ b/README.md @@ -325,7 +325,7 @@ This project is the result of the collaboration with the **University of Jaén** | **Backend** | Mireya Cueto Garrido | [@MireyaCueto](https://github.com/MireyaCueto) | ### Direction -* **Proyect Supervisor:** Luis Martínez López +* **Project Supervisor:** Luis Martínez López --- diff --git a/backend/.env.example b/backend/.env.example index c6b9fe5..3e798fc 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,81 +1,26 @@ -# ============================================================ -# ENVIRONMENT -# ============================================================ -ENVIRONMENT=development +ENVIRONMENT=production DEBUG=false -# ============================================================ -# DATABASE / CACHE -# ============================================================ -DATABASE_URL=postgresql://postgres:postgres@db:5432/orcid_db +ORCID_CLIENT_ID=APP-XXXX +ORCID_CLIENT_SECRET= +ORCID_REDIRECT_URI=https://app.tudominio.com/callback +ORCID_OAUTH_STATE_ENABLED=true + +API_KEY_NAME=X-API-Key +API_KEY_VALUE= + +DATABASE_URL=postgresql://:@db:5432/orcid_db REDIS_URL=redis://redis:6379/0 -# ============================================================ -# BASE URL (uso interno del scheduler) -# ============================================================ -BASE_URL=http://localhost:8000/api +BASE_URL=https://api.tudominio.com/api -# ============================================================ -# CORS — lista blanca estricta separada por comas -# Nunca uses "*" si allow_credentials=true. -# ============================================================ -CORS_ALLOWED_ORIGINS=http://localhost:5173 +CORS_ALLOWED_ORIGINS=https://app.tudominio.com +TRUSTED_HOSTS=api.tudominio.com -# ============================================================ -# Trusted Hosts — anti Host-header injection (en prod, sé explícito) -# ============================================================ -TRUSTED_HOSTS=* - -# ============================================================ -# JWT (login ORCID) -# Genera un secreto fuerte: `openssl rand -base64 64` -# ============================================================ -JWT_SECRET=change_me_to_a_long_random_value_at_least_32_chars +JWT_SECRET= JWT_ALGORITHM=HS256 JWT_EXPIRES_MINUTES=720 JWT_ISSUER=orcid-sword-backend JWT_AUDIENCE=orcid-sword-frontend -# ============================================================ -# API key máquina-a-máquina (scheduler interno) -# Genera con: `python -c "import secrets;print(secrets.token_urlsafe(48))"` -# ============================================================ -API_KEY_NAME=X-API-Key -API_KEY_VALUE=replace_with_a_strong_random_value_min_24_chars - -# ============================================================ -# ORCID OAuth 3-legged (authorization code) -# ============================================================ -ORCID_CLIENT_ID=APP-XXXXXXXXXXXXXXXX -ORCID_CLIENT_SECRET=replace_me -ORCID_REDIRECT_URI=http://localhost:8000/api/auth/orcid/callback -ORCID_OAUTH_STATE_ENABLED=true - -# ============================================================ -# Rate limits (formato slowapi: "/") -# ============================================================ -RATE_LIMIT_DEFAULT=60/minute -RATE_LIMIT_AUTH=10/minute -RATE_LIMIT_SEARCH_ANON=5/minute -RATE_LIMIT_SEARCH_AUTH=30/minute -RATE_LIMIT_EXPORT=20/minute -RATE_LIMIT_SYNC=5/minute - -# ============================================================ -# Tope de tamaños (anti DoS) -# ============================================================ -MAX_ORCID_BATCH=25 -MAX_PUB_IDS_BATCH=500 -MAX_REQUEST_BODY_BYTES=1048576 - -# ============================================================ -# Documentación interactiva (deshabilita en producción si no es necesaria) -# ============================================================ -DOCS_ENABLED=true - -# ============================================================ -# HSTS -# ============================================================ -SECURITY_HSTS_SECONDS=31536000 -SECURITY_HSTS_INCLUDE_SUBDOMAINS=true -SECURITY_HSTS_PRELOAD=false +DOCS_ENABLED=false \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 96075b0..c45f304 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ services: container_name: orcid-backend restart: unless-stopped ports: - - "127.0.0.1:8072:8000" + - "0.0.0.0:8072:8000" env_file: - ./backend/.env environment: @@ -36,7 +36,7 @@ services: container_name: orcid-frontend restart: unless-stopped ports: - - "127.0.0.1:8073:5173" + - "0.0.0.0:8073:5173" depends_on: - backend env_file: @@ -75,4 +75,4 @@ services: - no-new-privileges:true volumes: - postgres_data: + postgres_data: \ No newline at end of file diff --git a/frontend/.dockerignore b/frontend/.dockerignore index 3e328fd..c0fa193 100644 --- a/frontend/.dockerignore +++ b/frontend/.dockerignore @@ -16,8 +16,9 @@ build/ .vite/ *.timestamp-* -# Secretos: docker-compose ya inyecta las variables vía `env_file`, -# no necesitamos copiarlos al filesystem de la imagen. +# Secretos: no subir `.env` al contexto de `docker build` (evita capas con claves). +# `env_file` en compose aplica al proceso en ejecución del servicio, no al paso +# `npm run build` del Dockerfile; las `VITE_*` deben estar disponibles ahí (ENV/ARG). .env .env.* !.env.example diff --git a/frontend/.env.example b/frontend/.env.example index 159ff5a..f61b7bf 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -1,45 +1,4 @@ -# URL base del backend FastAPI (sin barra final). -# -# En desarrollo puedes dejarlo en blanco y el proxy de Vite -# (ver vite.config.js) reenviará todo lo que cuelgue de /api al -# destino indicado en VITE_API_PROXY_TARGET. Esto evita problemas -# de CORS sin exponer el host del backend al navegador. -VITE_API_URL=http://localhost:8000/api - -# Solo para dev: destino al que el proxy de Vite reenvía las peticiones -# que empiecen por /api. Cambia a http://backend:8000 si ejecutas el -# frontend dentro de docker-compose. -VITE_API_PROXY_TARGET=http://localhost:8000 - -# Clave compartida con el backend. Se inyecta como header `X-API-Key` -# en TODAS las peticiones salientes (ver src/services/api.js). Debe -# coincidir con `API_KEY_VALUE` del .env del backend. -VITE_API_KEY=12ao.9-8a7b-4c&d-9e,f-?89abc - -# Pon "true" SOLO si el backend no está disponible y quieres trabajar -# con los fixtures de src/services/mocks.js. En producción debe estar a "false". -VITE_USE_MOCKS=false - -# ── Autenticación OAuth ORCID ──────────────────────────────────────────────── -# -# El flujo real es: -# 1. Frontend abre popup → GET /api/auth/orcid/authorize -# 2. Backend redirige a sandbox.orcid.org (o pub.orcid.org en producción) -# 3. Usuario se autentica en ORCID -# 4. ORCID redirige a ORCID_REDIRECT_URI (debe apuntar a esta app) -# 5. /auth/callback extrae el code y llama al backend para obtener el JWT -# -# Para que el callback vuelva al frontend, el backend necesita: -# ORCID_REDIRECT_URI=http://localhost:5173/callback -# (en backend/.env — debe coincidir con el redirect URI del app ORCID sandbox) -# En producción con ngrok u otro túnel, el formato sería: -# ORCID_REDIRECT_URI=https:///callback -# -# ── Modo bypass (solo desarrollo sin credenciales OAuth configuradas) ───────── -# Cuando está a "true", el botón "Iniciar sesión" genera un token simulado -# a partir del ORCID introducido en el campo de texto, sin abrir popup ni -# contactar al backend de auth. Útil para probar la UI autenticada -# (badges "Nuevo", botón "Descargar lo nuevo") sin OAuth real. -# ADVERTENCIA: el token simulado NO es válido en el backend, por lo que -# downloaded_by_me siempre será null (sin datos reales de "novedad"). -VITE_AUTH_BYPASS=false +VITE_API_URL=https://api.tudominio.com/api +VITE_API_PROXY_TARGET= +VITE_API_KEY= +VITE_USE_MOCKS=false \ No newline at end of file diff --git a/frontend/Dockerfile b/frontend/Dockerfile index b3e5b32..6b9fd82 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,10 +1,23 @@ -FROM node:20-alpine +# ── Build stage ──────────────────────────────────────────────── +FROM node:20-alpine AS builder WORKDIR /app COPY package.json package-lock.json ./ -RUN npm install +RUN npm ci COPY . . +# `env_file` en docker-compose solo inyecta variables en el contenedor al ARRANCAR +# (aquí nginx no usa Vite). `vite build` corre en esta fase y necesita `VITE_*` +# ya definidas: `.env` no entra en el contexto de build (.dockerignore). +ENV VITE_BASE_PATH=/orcid2sword/ +RUN npm run build -CMD ["npm", "run", "dev", "--", "--host"] +# ── Serve stage ──────────────────────────────────────────────── +FROM nginx:alpine + +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=builder /app/dist /app/dist + +EXPOSE 5173 +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/README.md b/frontend/README.md deleted file mode 100644 index a36934d..0000000 --- a/frontend/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# React + Vite - -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. - -Currently, two official plugins are available: - -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) - -## React Compiler - -The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). - -## Expanding the ESLint configuration - -If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. diff --git a/frontend/index.html b/frontend/index.html index 5120c51..a0881b0 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,6 +4,9 @@ + + + ORCID2SWORD diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..f1b2bc2 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,45 @@ +server { + listen 5173; + listen [::]:5173; + server_name _; + + # Evita que nginx fabrique `Location` absoluto con su puerto interno (5173) + # si en algún momento se emitiera una redirección. + absolute_redirect off; + port_in_redirect off; + + root /app/dist; + index index.html; + + # index.html sin caché → evita que persistan 301/HTML antiguos en clientes. + location = /index.html { + add_header Cache-Control "no-store" always; + } + + # Apache en Sinbad2 reescribe la URL pública `/orcid2sword/...` a un prefijo + # interno distinto (`/flintstones/...`) antes de llegar a este contenedor. + # Hay que quitar ese prefijo y servir desde dist/ igual que con `/orcid2sword/` + # (acceso directo al puerto 8073 sin pasar por Apache). + + location ^~ /flintstones/ { + rewrite ^/flintstones/(.*)$ /$1 break; + try_files $uri $uri/ /index.html; + } + + location = /flintstones { + try_files /index.html =404; + } + + location ^~ /orcid2sword/ { + rewrite ^/orcid2sword/(.*)$ /$1 break; + try_files $uri $uri/ /index.html; + } + + location = /orcid2sword { + try_files /index.html =404; + } + + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/frontend/src/components/layout/Footer.jsx b/frontend/src/components/layout/Footer.jsx index c8b7654..eb9b709 100644 --- a/frontend/src/components/layout/Footer.jsx +++ b/frontend/src/components/layout/Footer.jsx @@ -55,7 +55,7 @@ export default function Footer() { de Jaén Logo UJA diff --git a/frontend/src/index.css b/frontend/src/index.css index 704dfbe..de2e2b1 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -62,7 +62,7 @@ --color-error-text: #6E1111; /* Fonts */ - --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + --font-sans: "Segoe UI", -apple-system, BlinkMacSystemFont, "Inter", system-ui, Roboto, "Helvetica Neue", Arial, sans-serif; --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; } diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index d9e87bc..da21b70 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -1,13 +1,33 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import { BrowserRouter } from "react-router-dom"; - -import "./index.css"; import App from "./App.jsx"; +import "./index.css"; + +function resolveRouterBasename() { + const configured = import.meta.env.BASE_URL ?? "/"; + const withSlash = configured.endsWith("/") ? configured : `${configured}/`; + + if (withSlash === "/") { + return "/"; + } + + const { pathname } = window.location; + if (pathname === "/" || pathname === "") { + return "/"; + } + + const prefix = withSlash.replace(/\/$/, ""); + if (pathname === prefix || pathname.startsWith(`${prefix}/`)) { + return prefix; + } + + return "/"; +} createRoot(document.getElementById("root")).render( - + , diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 8183396..ec5c921 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -2,32 +2,22 @@ import { defineConfig, loadEnv } from 'vite' import react from '@vitejs/plugin-react' import tailwindcss from '@tailwindcss/vite' -// https://vite.dev/config/ 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 || '/' return { + base, plugins: [react(), tailwindcss()], server: { host: true, - // Needed for HTTPS tunnels like ngrok during OAuth callback flows. - // We allow all hosts in dev to avoid host-blocking when ngrok URL rotates. allowedHosts: true, port: 5173, proxy: { - // El backend agrupa todo bajo /api (researchers, export, …). - // Con un único prefijo evitamos tener que mantener una entrada - // por router cada vez que se añada un endpoint nuevo. - '/api': { - target: proxyTarget, - changeOrigin: true, - }, - '/health': { - target: proxyTarget, - changeOrigin: true, - }, + '/api': { target: proxyTarget, changeOrigin: true }, + '/health': { target: proxyTarget, changeOrigin: true }, }, }, } -}) +}) \ No newline at end of file