# main.py """ Wizamart FastAPI Application Multi-tenant e-commerce marketplace platform with: - Three deployment modes (subdomain, custom domain, path-based) - Three interfaces (platform administration (admin), vendor dashboard (vendor), customer shop (shop)) - Comprehensive exception handling - Middleware stack for context injection """ import sys import io # Fix Windows console encoding issues (must be at the very top) if sys.platform == 'win32': sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') import logging from datetime import datetime, timezone from pathlib import Path from fastapi import Depends, FastAPI, HTTPException, Request, Response from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import HTMLResponse, RedirectResponse, FileResponse from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from sqlalchemy import text from sqlalchemy.orm import Session from app.api.main import api_router # Import page routers from app.routes import admin_pages, vendor_pages, shop_pages from app.core.config import settings from app.core.database import get_db from app.core.lifespan import lifespan from app.exceptions.handler import setup_exception_handlers from app.exceptions import ServiceUnavailableException # Import REFACTORED class-based middleware from middleware.vendor_context import VendorContextMiddleware from middleware.context_middleware import ContextMiddleware from middleware.theme_context import ThemeContextMiddleware from middleware.logging_middleware import LoggingMiddleware logger = logging.getLogger(__name__) # Get the project root directory (where main.py is located) BASE_DIR = Path(__file__).resolve().parent STATIC_DIR = BASE_DIR / "static" TEMPLATES_DIR = BASE_DIR / "app" / "templates" # FastAPI app with lifespan app = FastAPI( title=settings.project_name, description=settings.description, version=settings.version, lifespan=lifespan, ) # Configure Jinja2 Templates templates = Jinja2Templates(directory=str(TEMPLATES_DIR)) # Setup custom exception handlers (unified approach) setup_exception_handlers(app) # Add CORS middleware app.add_middleware( CORSMiddleware, allow_origins=settings.allowed_hosts, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # ============================================================================ # MIDDLEWARE REGISTRATION (CORRECTED ORDER!) # ============================================================================ # # IMPORTANT: Middleware execution order with BaseHTTPMiddleware: # # When using app.add_middleware or wrapping with BaseHTTPMiddleware, # the LAST added middleware runs FIRST (LIFO - Last In, First Out). # # So we add them in REVERSE order of desired execution: # # Desired execution order: # 1. VendorContextMiddleware (detect vendor, extract clean_path) # 2. ContextMiddleware (detect context using clean_path) # 3. ThemeContextMiddleware (load theme) # 4. LoggingMiddleware (log all requests) # # Therefore we add them in REVERSE: # - Add ThemeContextMiddleware FIRST (runs LAST in request) # - Add ContextMiddleware SECOND # - Add VendorContextMiddleware THIRD # - Add LoggingMiddleware LAST (runs FIRST for timing) # ============================================================================ logger.info("=" * 80) logger.info("MIDDLEWARE REGISTRATION") logger.info("=" * 80) # Add logging middleware (runs first for timing, logs all requests/responses) logger.info("Adding LoggingMiddleware (runs first for request timing)") app.add_middleware(LoggingMiddleware) # Add theme context middleware (runs last in request chain) logger.info("Adding ThemeContextMiddleware (detects and loads theme)") app.add_middleware(ThemeContextMiddleware) # Add context detection middleware (runs after vendor context extraction) logger.info("Adding ContextMiddleware (detects context type using clean_path)") app.add_middleware(ContextMiddleware) # Add vendor context middleware (runs first in request chain) logger.info("Adding VendorContextMiddleware (detects vendor, extracts clean_path)") app.add_middleware(VendorContextMiddleware) logger.info("=" * 80) logger.info("MIDDLEWARE ORDER SUMMARY:") logger.info(" Execution order (request →):") logger.info(" 1. LoggingMiddleware (timing)") logger.info(" 2. VendorContextMiddleware (vendor detection)") logger.info(" 3. ContextMiddleware (context detection)") logger.info(" 4. ThemeContextMiddleware (theme loading)") logger.info(" 5. FastAPI Router") logger.info("=" * 80) # ======================================== # MOUNT STATIC FILES - Use absolute path # ======================================== if STATIC_DIR.exists(): app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static") logger.info(f"Mounted static files from: {STATIC_DIR}") else: logger.warning(f"Static directory not found at {STATIC_DIR}") # ======================================== # Include API router (JSON endpoints at /api/*) app.include_router(api_router, prefix="/api") # ============================================================================ # FAVICON ROUTES (Must be registered BEFORE page routers) # ============================================================================ def serve_favicon() -> Response: """ Serve favicon with caching headers. Checks multiple possible locations for the favicon. """ possible_paths = [ STATIC_DIR / "favicon.ico", STATIC_DIR / "images" / "favicon.ico", STATIC_DIR / "assets" / "favicon.ico", ] for favicon_path in possible_paths: if favicon_path.exists(): return FileResponse( favicon_path, media_type="image/x-icon", headers={ "Cache-Control": "public, max-age=86400", # Cache for 1 day } ) return Response(status_code=204) @app.get("/favicon.ico", include_in_schema=False) async def favicon(): """Serve favicon from root path.""" return serve_favicon() @app.get("/vendor/favicon.ico", include_in_schema=False) async def vendor_favicon(): """Handle vendor-prefixed favicon requests.""" return serve_favicon() # ============================================================================ # HTML PAGE ROUTES (Jinja2 Templates) # ============================================================================ # Include HTML page routes (these return rendered templates, not JSON) logger.info("=" * 80) logger.info("ROUTE REGISTRATION") logger.info("=" * 80) # Admin pages logger.info("Registering admin page routes: /admin/*") app.include_router( admin_pages.router, prefix="/admin", tags=["admin-pages"], include_in_schema=False ) # Vendor management pages (dashboard, products, orders, etc.) logger.info("Registering vendor page routes: /vendor/{code}/*") app.include_router( vendor_pages.router, prefix="/vendor", tags=["vendor-pages"], include_in_schema=False ) # Customer shop pages - Register at TWO prefixes: # 1. /shop/* (for subdomain/custom domain modes) # 2. /vendors/{code}/shop/* (for path-based development mode) logger.info("Registering shop page routes:") logger.info(" - /shop/* (subdomain/custom domain mode)") logger.info(" - /vendors/{code}/shop/* (path-based development mode)") app.include_router( shop_pages.router, prefix="/shop", tags=["shop-pages"], include_in_schema=False ) app.include_router( shop_pages.router, prefix="/vendors/{vendor_code}/shop", tags=["shop-pages"], include_in_schema=False ) logger.info("=" * 80) # Log all registered routes logger.info("=" * 80) logger.info("REGISTERED ROUTES SUMMARY") logger.info("=" * 80) for route in app.routes: if hasattr(route, 'methods') and hasattr(route, 'path'): methods = ', '.join(route.methods) if route.methods else 'N/A' logger.info(f" {methods:<10} {route.path:<60}") logger.info("=" * 80) # ============================================================================ # API ROUTES (JSON Responses) # ============================================================================ # Public Routes (no authentication required) @app.get("/", include_in_schema=False) async def root(): """Redirect root to documentation""" return RedirectResponse(url="/documentation") @app.get("/health") def health_check(db: Session = Depends(get_db)): """Health check endpoint""" try: # Test database connection db.execute(text("SELECT 1")) return { "status": "healthy", "timestamp": datetime.now(timezone.utc), "message": f"{settings.project_name} v{settings.version}", "docs": { "swagger": "/docs", "redoc": "/redoc", "openapi": "/openapi.json", "complete": "/documentation", }, "features": [ "Multi-tenant architecture with vendor isolation", "JWT Authentication with role-based access control", "Marketplace product import and curation", "Vendor catalog management", "Product-based inventory tracking", "Stripe Connect payment processing", ], "supported_marketplaces": [ "Letzshop", ], "deployment_modes": [ "Subdomain-based (production): vendor.platform.com", "Custom domain (production): customvendordomain.com", "Path-based (development): /vendors/vendorname/ or /vendor/vendorname/", ], "auth_required": "Most endpoints require Bearer token authentication", } except Exception as e: logger.error(f"Health check failed: {e}") raise ServiceUnavailableException("Service unhealthy") @app.get("/documentation", response_class=HTMLResponse, include_in_schema=False) async def documentation(): """Redirect to documentation""" if settings.debug: return RedirectResponse(url="http://localhost:8001") return RedirectResponse(url=settings.documentation_url) if __name__ == "__main__": import uvicorn uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)