# main.py 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 from middleware.theme_context import theme_context_middleware from middleware.vendor_context import vendor_context_middleware 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=["*"], ) # Add vendor context middleware (must be after CORS) app.middleware("http")(vendor_context_middleware) # Add theme context middleware (must be after vendor context) app.middleware("http")(theme_context_middleware) # ======================================== # MOUNT STATIC FILES - Use absolute path # ======================================== if STATIC_DIR.exists(): app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static") 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 favicon locations (in priority order) possible_paths = [ STATIC_DIR / "favicon.ico", STATIC_DIR / "images" / "favicon.ico", STATIC_DIR / "assets" / "favicon.ico", ] # Find first existing favicon 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 } ) # No favicon found - return 204 No Content 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) # Admin pages app.include_router( admin_pages.router, prefix="/admin", tags=["admin-pages"], include_in_schema=False # Don't show HTML pages in API docs ) # Vendor pages app.include_router( vendor_pages.router, prefix="/vendor", tags=["vendor-pages"], include_in_schema=False # Don't show HTML pages in API docs ) # Shop pages app.include_router( shop_pages.router, prefix="/shop", tags=["shop-pages"], include_in_schema=False # Don't show HTML pages in API docs ) # ============================================================================ # 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", "Path-based (development): /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)