feat: production deployment - full update with providers, admin, glossaries, pricing, tests

Major changes across backend, frontend, infrastructure:
- Provider system with model selection (Google, DeepL, OpenAI, Ollama, Google Cloud)
- Admin panel: user management, pricing, settings
- Glossary system with CSV import/export
- Subscription and tier quota management
- Security hardening (rate limiting, API key auth, path traversal fixes)
- Docker compose for dev, prod, and IONOS deployment
- Alembic migrations for new tables
- Frontend: dashboard, pricing page, landing page, i18n (en/fr)
- Test suite and verification scripts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Sepehr Ramezani
2026-04-25 15:01:47 +02:00
parent 2ba4fedfc8
commit 26bd096a06
1178 changed files with 136435 additions and 3047 deletions

100
main.py
View File

@@ -7,6 +7,18 @@ Story 3.5: API Versioning - All endpoints under /api/v1/ prefix
Story 3.6: Documentation OpenAPI (Swagger + ReDoc)
"""
# Story 6.6: Fail-fast on missing required env before any other imports (NFR10)
import sys
from config import config
_missing = config.validate_required_env()
if _missing:
msg = (
"Missing required env: " + ", ".join(_missing)
+ ". Set them in .env or environment. See .env.example."
)
print(msg, file=sys.stderr)
sys.exit(1)
from fastapi import (
FastAPI,
Request,
@@ -19,10 +31,7 @@ from fastapi.staticfiles import StaticFiles
from fastapi.exceptions import RequestValidationError
from contextlib import asynccontextmanager
from pathlib import Path
import logging
import os
from config import config
from translators import (
excel_translator,
word_translator,
@@ -60,26 +69,29 @@ from utils.exceptions import (
LanguageNotSupportedError,
DocumentProcessingError as UtilsDocumentProcessingError,
)
from core.logging import configure_logging, get_logger
logging.basicConfig(
level=getattr(logging, os.getenv("LOG_LEVEL", "INFO")),
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
# Configure structlog-based logging once at startup (single source of truth: config).
_json_logs = (
config.LOG_FORMAT.lower() == "json" or config.ENV == "production"
)
logger = logging.getLogger(__name__)
configure_logging(
json_logs=_json_logs,
log_level=config.LOG_LEVEL,
)
logger = get_logger(__name__)
rate_limit_config = RateLimitConfig(
requests_per_minute=int(os.getenv("RATE_LIMIT_PER_MINUTE", "30")),
requests_per_hour=int(os.getenv("RATE_LIMIT_PER_HOUR", "200")),
translations_per_minute=int(os.getenv("TRANSLATIONS_PER_MINUTE", "10")),
translations_per_hour=int(os.getenv("TRANSLATIONS_PER_HOUR", "50")),
max_concurrent_translations=int(os.getenv("MAX_CONCURRENT_TRANSLATIONS", "5")),
requests_per_minute=config.RATE_LIMIT_PER_MINUTE,
requests_per_hour=config.RATE_LIMIT_PER_HOUR,
translations_per_minute=config.TRANSLATIONS_PER_MINUTE,
translations_per_hour=config.TRANSLATIONS_PER_HOUR,
max_concurrent_translations=config.MAX_CONCURRENT_TRANSLATIONS,
)
rate_limit_manager = RateLimitManager(rate_limit_config)
cleanup_manager = create_cleanup_manager(config)
memory_monitor = MemoryMonitor(
max_memory_percent=float(os.getenv("MAX_MEMORY_PERCENT", "80"))
)
memory_monitor = MemoryMonitor(max_memory_percent=config.MAX_MEMORY_PERCENT)
health_checker = HealthChecker(cleanup_manager, memory_monitor)
@@ -350,21 +362,36 @@ app.add_middleware(ErrorHandlingMiddleware)
app.add_middleware(RequestLoggingMiddleware, log_body=False)
app.add_middleware(
SecurityHeadersMiddleware,
config={"enable_hsts": os.getenv("ENABLE_HSTS", "false").lower() == "true"},
config={"enable_hsts": config.ENABLE_HSTS},
)
app.add_middleware(RateLimitMiddleware, rate_limit_manager=rate_limit_manager)
_cors_env = os.getenv("CORS_ORIGINS", "")
# Local dev frontends often run on 3000, 3001, etc.; .env may list only one port and break the other.
_CORS_EXTRA_DEV_ORIGINS = [
"http://localhost:3000",
"http://localhost:3001",
"http://127.0.0.1:3000",
"http://127.0.0.1:3001",
]
_cors_env = config.CORS_ORIGINS_RAW
if _cors_env == "*" or not _cors_env:
logger.warning(
"CORS_ORIGINS not properly configured. Using permissive settings for development only!"
)
allowed_origins = ["*"]
else:
allowed_origins = [
origin.strip() for origin in _cors_env.split(",") if origin.strip()
]
logger.info(f"CORS configured for origins: {allowed_origins}")
allowed_origins = list(config.CORS_ORIGINS)
if config.ENV != "production":
for _o in _CORS_EXTRA_DEV_ORIGINS:
if _o not in allowed_origins:
allowed_origins.append(_o)
logger.info(
"CORS: non-production — localhost dev ports merged into allowed origins: %s",
allowed_origins,
)
else:
logger.info("CORS configured for origins: %s", allowed_origins)
app.add_middleware(
CORSMiddleware,
@@ -550,7 +577,7 @@ async def root():
@app.get("/health", tags=["Health"])
async def health_check():
"""Health check endpoint with detailed system status (Kubernetes liveness probe)"""
REDIS_URL = os.getenv("REDIS_URL", "")
from core.redis import get_redis_url, ping_sync
health_status = await health_checker.check_health()
status_code = 200 if health_status.get("status") == "healthy" else 503
@@ -559,7 +586,7 @@ async def health_check():
try:
from database.connection import check_db_connection
if check_db_connection():
if await check_db_connection():
db_status = {"status": "healthy"}
else:
db_status = {"status": "unhealthy"}
@@ -567,15 +594,9 @@ async def health_check():
db_status = {"status": "error", "error": str(e)}
redis_status = {"status": "not_configured"}
if REDIS_URL:
try:
import redis
redis_client = redis.from_url(REDIS_URL, decode_responses=True)
redis_client.ping()
redis_status = {"status": "healthy"}
except Exception as e:
redis_status = {"status": "unhealthy", "error": str(e)}
if get_redis_url():
ok, err = ping_sync()
redis_status = {"status": "healthy"} if ok else {"status": "unhealthy", "error": err or "ping failed"}
return JSONResponse(
status_code=status_code,
@@ -599,27 +620,24 @@ async def health_check():
@app.get("/ready", tags=["Health"])
async def readiness_check():
"""Kubernetes readiness probe - check if app can serve traffic"""
REDIS_URL = os.getenv("REDIS_URL", "")
from core.redis import get_redis_url, ping_sync
issues = []
try:
from database.connection import check_db_connection, DATABASE_URL
if DATABASE_URL:
if not check_db_connection():
if not await check_db_connection():
issues.append("database_unavailable")
except ImportError:
pass
except Exception as e:
issues.append(f"database_error: {str(e)}")
if REDIS_URL:
try:
import redis
redis_client = redis.from_url(REDIS_URL, decode_responses=True)
redis_client.ping()
except Exception:
if get_redis_url():
ok, _ = ping_sync()
if not ok:
issues.append("redis_unavailable")
if issues: