Support HTTPS deployment under /generadorexamenesllm behind reverse proxy.

This updates frontend base-path routing, same-origin API proxying, and deployment defaults/docs so the app works correctly through the Sinbad2 Apache ProxyPass setup.
This commit is contained in:
Mireya Cueto Garrido
2026-06-02 13:10:52 +02:00
parent d7f9ae8841
commit 944482b96c
10 changed files with 76 additions and 18 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ API_KEY=change-me-in-production-min-16-chars
DATABASE_URL=postgresql+psycopg://genexamenes:genexamenes@db:5432/genexamenes DATABASE_URL=postgresql+psycopg://genexamenes:genexamenes@db:5432/genexamenes
# --- CORS (orígenes del frontend, separados por coma) --- # --- CORS (orígenes del frontend, separados por coma) ---
ALLOWED_ORIGINS=http://sinbad2.ujaen.es,http://sinbad2.ujaen.es:8075 ALLOWED_ORIGINS=https://sinbad2.ujaen.es,http://sinbad2.ujaen.es,http://sinbad2.ujaen.es:8075
# --- Rate limiting y tamaño de petición --- # --- Rate limiting y tamaño de petición ---
RATE_LIMIT_REQUESTS=60 RATE_LIMIT_REQUESTS=60
+1 -1
View File
@@ -10,7 +10,7 @@ class Settings(BaseSettings):
api_prefix: str = "" api_prefix: str = ""
api_key: str = Field(min_length=16) api_key: str = Field(min_length=16)
database_url: str = "postgresql+psycopg://genexamenes:genexamenes@localhost:5432/genexamenes" database_url: str = "postgresql+psycopg://genexamenes:genexamenes@localhost:5432/genexamenes"
allowed_origins: str = "http://sinbad2.ujaen.es,http://sinbad2.ujaen.es:8075" allowed_origins: str = "https://sinbad2.ujaen.es,http://sinbad2.ujaen.es,http://sinbad2.ujaen.es:8075"
rate_limit_requests: int = Field(default=60, ge=1) rate_limit_requests: int = Field(default=60, ge=1)
rate_limit_window_seconds: int = Field(default=60, ge=1) rate_limit_window_seconds: int = Field(default=60, ge=1)
max_request_bytes: int = Field(default=1_048_576, ge=1_024) max_request_bytes: int = Field(default=1_048_576, ge=1_024)
+3 -2
View File
@@ -7,7 +7,7 @@ services:
environment: environment:
DATABASE_URL: postgresql+psycopg://genexamenes:genexamenes@db:5432/genexamenes DATABASE_URL: postgresql+psycopg://genexamenes:genexamenes@db:5432/genexamenes
# Sobrescribe backend/.env con el origen público del frontend en despliegue. # Sobrescribe backend/.env con el origen público del frontend en despliegue.
ALLOWED_ORIGINS: ${ALLOWED_ORIGINS:-http://sinbad2.ujaen.es,http://sinbad2.ujaen.es:8075} ALLOWED_ORIGINS: ${ALLOWED_ORIGINS:-https://sinbad2.ujaen.es,http://sinbad2.ujaen.es,http://sinbad2.ujaen.es:8075}
LLM_BASE_URL: LLM_BASE_URL:
LLM_MODEL: qwen3.5:35b LLM_MODEL: qwen3.5:35b
LLM_TIMEOUT_SECONDS: "180" LLM_TIMEOUT_SECONDS: "180"
@@ -24,7 +24,8 @@ services:
build: build:
context: ./frontend context: ./frontend
args: args:
VITE_API_URL: ${VITE_API_URL:-http://sinbad2.ujaen.es:8074} VITE_APP_BASE_PATH: ${VITE_APP_BASE_PATH:-/generadorexamenesllm/}
VITE_API_URL: ${VITE_API_URL:-}
VITE_GOOGLE_CLIENT_ID: ${VITE_GOOGLE_CLIENT_ID:-} VITE_GOOGLE_CLIENT_ID: ${VITE_GOOGLE_CLIENT_ID:-}
ports: ports:
- "${FRONTEND_PORT:-8075}:80" - "${FRONTEND_PORT:-8075}:80"
+7 -2
View File
@@ -1,5 +1,10 @@
# URL base del backend (accesible desde el navegador) # Base pública de la app cuando va tras proxy (debe terminar en /)
VITE_API_URL=http://sinbad2.ujaen.es:8074 # Ejemplo producción UJA: /generadorexamenesllm/
VITE_APP_BASE_PATH=/generadorexamenesllm/
# URL base del backend (accesible desde el navegador).
# Si se deja vacía, usa la misma base de la app (recomendado tras proxy HTTPS).
VITE_API_URL=
# (Opcional) Client ID de Google para "Iniciar sesión con Google". # (Opcional) Client ID de Google para "Iniciar sesión con Google".
# Debe coincidir con GOOGLE_CLIENT_ID del backend. # Debe coincidir con GOOGLE_CLIENT_ID del backend.
+3 -1
View File
@@ -2,8 +2,10 @@
FROM node:20-alpine AS build FROM node:20-alpine AS build
WORKDIR /app WORKDIR /app
ARG VITE_API_URL=http://sinbad2.ujaen.es:8074 ARG VITE_APP_BASE_PATH=/generadorexamenesllm/
ARG VITE_API_URL=
ARG VITE_GOOGLE_CLIENT_ID= ARG VITE_GOOGLE_CLIENT_ID=
ENV VITE_APP_BASE_PATH=$VITE_APP_BASE_PATH
ENV VITE_API_URL=$VITE_API_URL ENV VITE_API_URL=$VITE_API_URL
ENV VITE_GOOGLE_CLIENT_ID=$VITE_GOOGLE_CLIENT_ID ENV VITE_GOOGLE_CLIENT_ID=$VITE_GOOGLE_CLIENT_ID
+21 -2
View File
@@ -38,7 +38,8 @@ npm run dev # http://sinbad2.ujaen.es:8075
| Variable | Descripción | | Variable | Descripción |
| ----------------------- | -------------------------------------------------------- | | ----------------------- | -------------------------------------------------------- |
| `VITE_API_URL` | URL base del backend (por defecto `http://sinbad2.ujaen.es:8074`). | | `VITE_APP_BASE_PATH` | Base pública de la SPA (debe terminar en `/`). En producción UJA: `/generadorexamenesllm/`. |
| `VITE_API_URL` | URL base de la API. Si se deja vacía, usa la misma base pública de la app. |
| `VITE_GOOGLE_CLIENT_ID` | (Opcional) Client ID de Google. Si está vacío, se oculta el botón de Google. | | `VITE_GOOGLE_CLIENT_ID` | (Opcional) Client ID de Google. Si está vacío, se oculta el botón de Google. |
> Las variables `VITE_*` se incrustan en el build, por lo que apuntan al backend > Las variables `VITE_*` se incrustan en el build, por lo que apuntan al backend
@@ -60,9 +61,27 @@ El `docker-compose.yml` de la raíz construye el frontend con un build multi-sta
docker compose up --build docker compose up --build
``` ```
Las variables `VITE_API_URL` y `VITE_GOOGLE_CLIENT_ID` pueden pasarse como Las variables `VITE_APP_BASE_PATH`, `VITE_API_URL` y `VITE_GOOGLE_CLIENT_ID` pueden pasarse como
variables de entorno al ejecutar `docker compose`. variables de entorno al ejecutar `docker compose`.
## Despliegue HTTPS en subruta (Sinbad2)
Para ejecutarlo detrás de Apache en `https://sinbad2.ujaen.es/deckofcars` con:
```apache
ProxyPass /generadorexamenesllm http://host.docker.internal:8075/
ProxyPassReverse /generadorexamenesllm http://host.docker.internal:8075/
```
usa estos valores de build:
```env
VITE_APP_BASE_PATH=/generadorexamenesllm/
VITE_API_URL=
```
Con esto, el frontend sirve assets/rutas bajo `/generadorexamenesllm` y consume la API por HTTPS en la misma base (sin mixed content).
## Manejo de errores ## Manejo de errores
Todas las respuestas de error del backend siguen el formato Todas las respuestas de error del backend siguen el formato
+25
View File
@@ -4,6 +4,31 @@ server {
root /usr/share/nginx/html; root /usr/share/nginx/html;
index index.html; index index.html;
# Backend API dentro de la misma base HTTPS (evita mixed content).
location /auth/ {
proxy_pass http://backend:8074/auth/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /exam/ {
proxy_pass http://backend:8074/exam/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location = /health {
proxy_pass http://backend:8074/health;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# SPA: cualquier ruta desconocida sirve index.html (React Router). # SPA: cualquier ruta desconocida sirve index.html (React Router).
location / { location / {
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html;
+1 -1
View File
@@ -1,7 +1,7 @@
import axios from "axios"; import axios from "axios";
export const API_URL = export const API_URL =
import.meta.env.VITE_API_URL?.replace(/\/$/, "") || "http://sinbad2.ujaen.es:8074"; import.meta.env.VITE_API_URL?.replace(/\/$/, "") || import.meta.env.BASE_URL.replace(/\/$/, "");
const TOKEN_KEY = "genex_token"; const TOKEN_KEY = "genex_token";
+1 -1
View File
@@ -8,7 +8,7 @@ import "./index.css";
ReactDOM.createRoot(document.getElementById("root")).render( ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode> <React.StrictMode>
<BrowserRouter> <BrowserRouter basename={import.meta.env.BASE_URL}>
<ToastProvider> <ToastProvider>
<AuthProvider> <AuthProvider>
<App /> <App />
+13 -7
View File
@@ -1,10 +1,16 @@
import { defineConfig } from "vite"; import { defineConfig, loadEnv } from "vite";
import react from "@vitejs/plugin-react"; import react from "@vitejs/plugin-react";
export default defineConfig({ export default defineConfig(({ mode }) => {
plugins: [react()], const env = loadEnv(mode, process.cwd(), "");
server: { const base = env.VITE_APP_BASE_PATH || "/";
host: true,
port: 8075, return {
}, base,
plugins: [react()],
server: {
host: true,
port: 8075,
},
};
}); });