307 lines
10 KiB
Python
307 lines
10 KiB
Python
# 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 import ContextMiddleware
|
|
from middleware.theme_context import ThemeContextMiddleware
|
|
from middleware.logging 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)
|