from fastapi import FastAPI, Request, status from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.trustedhost import TrustedHostMiddleware from fastapi.exceptions import RequestValidationError from starlette.exceptions import HTTPException as StarletteHTTPException import time import os import logging from contextlib import asynccontextmanager from typing import Dict, Any from routes import auth from utils.logger import setup_logger from utils.exceptions import APIException, handle_api_exception from models.user import AdminUser from models.coupon import Coupon from utils.auth import engine from init_db import initialize_database # Setup logging logger = setup_logger(__name__) # Application configuration class AppConfig: """Application configuration class""" APP_NAME = os.getenv("APP_NAME") VERSION = os.getenv("APP_VERSION") DEBUG = os.getenv("DEBUG", "false").lower() == "true" ENVIRONMENT = os.getenv("ENVIRONMENT", "development") # CORS settings - parse comma-separated string _cors_origins_str = os.getenv("CORS_ORIGINS", "") CORS_ORIGINS = [origin.strip() for origin in _cors_origins_str.split(",") if origin.strip()] if _cors_origins_str else [] # Trusted hosts for production _trusted_hosts_str = os.getenv("TRUSTED_HOSTS", "*") TRUSTED_HOSTS = [host.strip() for host in _trusted_hosts_str.split(",") if host.strip()] if _trusted_hosts_str != "*" else ["*"] # Application lifespan manager @asynccontextmanager async def lifespan(app: FastAPI): """Manage application startup and shutdown events""" # Startup logger.info( "Application starting up", extra={ "app_name": AppConfig.APP_NAME, "version": AppConfig.VERSION, "environment": AppConfig.ENVIRONMENT, "debug": AppConfig.DEBUG } ) # Ensure required directories exist ensure_directories() # Initialize database: create tables and default admin user try: initialize_database() except Exception as e: logger.error(f"Error initializing database: {e}") raise yield # Shutdown logger.info("Application shutting down") def ensure_directories(): """Ensure required directories exist""" directories = [ "translation_upload", "logs" ] for directory in directories: os.makedirs(directory, exist_ok=True) logger.debug(f"Ensured directory exists: {directory}") # Create FastAPI application with enterprise features app = FastAPI( title=AppConfig.APP_NAME, version=AppConfig.VERSION, description="Enterprise-grade Ebook Coupon Management System API", docs_url="/docs" if AppConfig.DEBUG else None, redoc_url="/redoc" if AppConfig.DEBUG else None, lifespan=lifespan ) # Get paths relative to backend/main.py BASE_DIR = os.path.dirname(__file__) PARENT_DIR = os.path.abspath(os.path.join(BASE_DIR, "..")) ADMIN_PANEL_DIR = os.path.join(PARENT_DIR, "admin-frontend") # Mount static files app.mount("/static", StaticFiles(directory=ADMIN_PANEL_DIR), name="static") # Setup templates templates = Jinja2Templates(directory=ADMIN_PANEL_DIR) # Add middleware for production readiness # NOTE: TrustedHostMiddleware disabled when behind reverse proxy (Traefik/Coolify) # The reverse proxy handles host validation # if AppConfig.ENVIRONMENT == "production": # # Trusted host middleware for production security # app.add_middleware( # TrustedHostMiddleware, # allowed_hosts=AppConfig.TRUSTED_HOSTS # ) # CORS middleware for cross-origin requests app.add_middleware( CORSMiddleware, allow_origins=AppConfig.CORS_ORIGINS, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Request timing and logging middleware @app.middleware("http") async def add_process_time_header(request: Request, call_next): """Add request processing time and logging""" start_time = time.time() # Generate request ID for tracking request_id = f"{int(start_time * 1000)}" request.state.request_id = request_id # Log incoming request logger.info( f"Incoming request: {request.method} {request.url.path}", extra={ "request_id": request_id, "method": request.method, "path": request.url.path, "client_ip": request.client.host, "user_agent": request.headers.get("user-agent", "") } ) try: response = await call_next(request) process_time = time.time() - start_time # Add headers for monitoring response.headers["X-Process-Time"] = f"{process_time:.4f}" response.headers["X-Request-ID"] = request_id # Log successful response logger.info( f"Request completed: {request.method} {request.url.path}", extra={ "request_id": request_id, "status_code": response.status_code, "process_time": process_time } ) return response except Exception as e: process_time = time.time() - start_time logger.error( f"Request failed: {request.method} {request.url.path}", extra={ "request_id": request_id, "error": str(e), "process_time": process_time }, exc_info=True ) raise # Exception handlers for proper error responses @app.exception_handler(APIException) async def api_exception_handler(request: Request, exc: APIException): """Handle custom API exceptions""" logger.warning( f"API Exception: {exc.detail}", extra={ "request_id": getattr(request.state, "request_id", "unknown"), "status_code": exc.status_code, "path": request.url.path } ) return JSONResponse( status_code=exc.status_code, content={ "success": False, "error": exc.detail, "error_code": exc.error_code, "timestamp": time.time(), "path": str(request.url.path) } ) @app.exception_handler(RequestValidationError) async def validation_exception_handler(request: Request, exc: RequestValidationError): """Handle validation errors""" # Safely extract error details try: error_details = [] for error in exc.errors(): safe_error = { "type": error.get("type", "unknown"), "loc": error.get("loc", []), "msg": str(error.get("msg", "Unknown error")), "input": str(error.get("input", "Unknown input")) } if "ctx" in error and error["ctx"]: safe_error["ctx"] = {k: str(v) for k, v in error["ctx"].items()} error_details.append(safe_error) except Exception: error_details = [{"type": "validation_error", "msg": "Request validation failed"}] logger.warning( "Validation error", extra={ "request_id": getattr(request.state, "request_id", "unknown"), "errors": error_details, "path": request.url.path } ) return JSONResponse( status_code=422, content={ "success": False, "error": "Validation Error", "error_code": "VALIDATION_ERROR", "detail": "Request validation failed", "timestamp": time.time(), "path": str(request.url.path), "details": error_details } ) @app.exception_handler(StarletteHTTPException) async def http_exception_handler(request: Request, exc: StarletteHTTPException): """Handle HTTP exceptions""" logger.warning( f"HTTP Exception: {exc.status_code}", extra={ "request_id": getattr(request.state, "request_id", "unknown"), "status_code": exc.status_code, "detail": exc.detail, "path": request.url.path } ) return JSONResponse( status_code=exc.status_code, content={ "success": False, "error": "HTTP Error", "detail": exc.detail, "timestamp": time.time(), "path": str(request.url.path) } ) @app.exception_handler(Exception) async def generic_exception_handler(request: Request, exc: Exception): """Handle generic exceptions""" logger.error( "Unhandled exception", extra={ "request_id": getattr(request.state, "request_id", "unknown"), "exception_type": type(exc).__name__, "exception_message": str(exc), "path": request.url.path }, exc_info=True ) return JSONResponse( status_code=500, content={ "success": False, "error": "Internal Server Error", "error_code": "INTERNAL_ERROR", "detail": "An unexpected error occurred", "timestamp": time.time(), "path": str(request.url.path) } ) # Health check endpoint @app.get("/health", tags=["Health"]) async def health_check() -> Dict[str, Any]: """Health check endpoint for monitoring""" from utils.auth import get_db from sqlalchemy import text # Check database connection db_status = "connected" try: db = next(get_db()) db.execute(text("SELECT 1")) db.close() except Exception as e: db_status = "disconnected" logger.error("Database health check failed", extra={"error": str(e)}) return { "status": "healthy" if db_status == "connected" else "unhealthy", "timestamp": time.time(), "version": AppConfig.VERSION, "environment": AppConfig.ENVIRONMENT, "database_status": db_status } # Include routers - auth.router handles / and /login HTML pages app.include_router(auth.router, prefix="/auth", tags=["Auth"]) app.include_router(auth.router, prefix="", tags=["Auth"]) # API info endpoint (moved from / to /api to avoid conflict with auth.router) @app.get("/api", tags=["API Info"]) async def api_info() -> Dict[str, Any]: """API information endpoint""" return { "message": AppConfig.APP_NAME, "version": AppConfig.VERSION, "environment": AppConfig.ENVIRONMENT, "docs_url": "/docs" if AppConfig.DEBUG else None, "health_check": "/health" }