Enable HTTPS production deployment on Sinbad2 via Apache reverse proxy.
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
__pycache__
|
||||
*.pyc
|
||||
.env
|
||||
.venv
|
||||
venv
|
||||
+11
-5
@@ -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
@@ -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"]
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user