Enable HTTPS production deployment on Sinbad2 via Apache reverse proxy.

This commit is contained in:
Mireya Cueto Garrido
2026-06-03 10:41:02 +02:00
parent 31be326f2c
commit cccbe15275
22 changed files with 264 additions and 28 deletions
+5
View File
@@ -0,0 +1,5 @@
__pycache__
*.pyc
.env
.venv
venv
+11 -5
View File
@@ -16,10 +16,10 @@
GOOGLE_CLIENT_ID=tu-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=tu-client-secret
# URI a la que Google redirige tras el login (debe estar registrada en Google Cloud)
# Con Docker: suele ser el frontend (8071) porque /api hace proxy al backend
# Con Docker local: el frontend (8071) hace proxy de /api al backend
GOOGLE_REDIRECT_URI=http://localhost:8071/api/auth/google/callback
# Producción (añade la misma URI en Google Console → Credenciales OAuth):
# GOOGLE_REDIRECT_URI=http://tu-servidor:8071/api/auth/google/callback
# Producción Sinbad2 (HTTPS vía Apache, prefijo /deckofcards):
# GOOGLE_REDIRECT_URI=https://sinbad2.ujaen.es/deckofcards/api/auth/google/callback
# Clave para firmar los JWT (usa algo largo y aleatorio en producción)
SECRET_KEY=cambia-esta-clave-en-produccion
@@ -27,8 +27,14 @@ SECRET_KEY=cambia-esta-clave-en-produccion
# URL del frontend a la que se redirige tras el login con Google
# Con docker-compose en local: http://localhost:8071
# Con Vite directo en local: http://localhost:5173
# En producción: https://tu-dominio.com
FRONTEND_URL=http://localhost:5173
# Producción Sinbad2: https://sinbad2.ujaen.es/deckofcards
FRONTEND_URL=http://localhost:8071
# Entorno y seguridad HTTPS (producción en Sinbad2)
# ENVIRONMENT=production
# CORS_ALLOWED_ORIGINS=https://sinbad2.ujaen.es
# TRUSTED_HOSTS=sinbad2.ujaen.es,backend,localhost
# SECURITY_HSTS_SECONDS=31536000
# Verificación de email por código numérico
# El destinatario puede ser cualquier proveedor (Gmail, Hotmail, Outlook, etc.).
+12 -1
View File
@@ -1,4 +1,4 @@
FROM python:3.10-slim
FROM python:3.10-slim AS base
WORKDIR /app
@@ -8,4 +8,15 @@ RUN pip install --no-cache-dir -r requirements.txt
COPY . .
FROM base AS development
CMD ["uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
FROM base AS production
CMD ["uvicorn", "api.main:app", \
"--host", "0.0.0.0", \
"--port", "8000", \
"--proxy-headers", \
"--forwarded-allow-ips", "*", \
"--no-server-header"]
View File
+41
View File
@@ -0,0 +1,41 @@
import os
from functools import lru_cache
def _split_csv(value: str | None) -> list[str]:
if not value:
return []
return [item.strip() for item in value.split(",") if item.strip()]
class Settings:
ENVIRONMENT: str = os.getenv("ENVIRONMENT", "development")
SECURITY_HSTS_SECONDS: int = int(os.getenv("SECURITY_HSTS_SECONDS", "31536000"))
@property
def is_production(self) -> bool:
return self.ENVIRONMENT.strip().lower() == "production"
@property
def cors_allowed_origins(self) -> list[str]:
configured = _split_csv(os.getenv("CORS_ALLOWED_ORIGINS"))
if configured:
return configured
if self.is_production:
frontend = os.getenv("FRONTEND_URL", "").rstrip("/")
return [frontend] if frontend else []
return ["*"]
@property
def trusted_hosts(self) -> list[str]:
configured = _split_csv(os.getenv("TRUSTED_HOSTS"))
if configured:
return configured
if self.is_production:
return ["sinbad2.ujaen.es"]
return ["*"]
@lru_cache
def get_settings() -> Settings:
return Settings()
+29 -8
View File
@@ -1,7 +1,12 @@
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from contextlib import asynccontextmanager
from starlette.middleware.trustedhost import TrustedHostMiddleware
from api.config.settings import get_settings
from api.database.mongodb import db
from api.middleware.security_headers import SecurityHeadersMiddleware
# Routers
from api.routers.test_mongo import router as test_mongo_router
@@ -20,15 +25,31 @@ from api.routers.google_auth import router as google_auth_router
async def lifespan(app: FastAPI):
yield
settings = get_settings()
app = FastAPI(lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.add_middleware(SecurityHeadersMiddleware, settings=settings)
if settings.is_production:
app.add_middleware(
TrustedHostMiddleware,
allowed_hosts=settings.trusted_hosts,
)
cors_origins = settings.cors_allowed_origins
cors_kwargs = {
"allow_methods": ["*"],
"allow_headers": ["*"],
}
if cors_origins == ["*"]:
cors_kwargs["allow_origins"] = ["*"]
cors_kwargs["allow_credentials"] = False
else:
cors_kwargs["allow_origins"] = cors_origins
cors_kwargs["allow_credentials"] = True
app.add_middleware(CORSMiddleware, **cors_kwargs)
app.include_router(test_mongo_router, prefix="/api")
app.include_router(value_router, prefix="/api/criteria/doc")
View File
@@ -0,0 +1,24 @@
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import Response
from api.config.settings import Settings, get_settings
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
def __init__(self, app, settings: Settings | None = None):
super().__init__(app)
self._settings = settings or get_settings()
async def dispatch(self, request: Request, call_next) -> Response:
response = await call_next(request)
response.headers.setdefault("X-Content-Type-Options", "nosniff")
response.headers.setdefault("X-Frame-Options", "DENY")
response.headers.setdefault("Referrer-Policy", "strict-origin-when-cross-origin")
if request.url.scheme == "https" or self._settings.is_production:
hsts = f"max-age={self._settings.SECURITY_HSTS_SECONDS}"
response.headers.setdefault("Strict-Transport-Security", hsts)
return response