# middleware/context_middleware.py """ Context Detection Middleware (Class-Based) Detects the request context type (API, Admin, Vendor Dashboard, Shop, or Fallback) and injects it into request.state for use by error handlers and other components. MUST run AFTER vendor_context_middleware to have access to clean_path. MUST run BEFORE theme_context_middleware (which needs context_type). Class-based middleware provides: - Better state management - Easier testing - More organized code - Standard ASGI pattern """ import logging from enum import Enum from fastapi import Request from starlette.middleware.base import BaseHTTPMiddleware logger = logging.getLogger(__name__) class RequestContext(str, Enum): """Request context types for the application.""" API = "api" ADMIN = "admin" VENDOR_DASHBOARD = "vendor" SHOP = "shop" FALLBACK = "fallback" class ContextManager: """Manages context detection for multi-area application.""" @staticmethod def detect_context(request: Request) -> RequestContext: """ Detect the request context type. Priority order: 1. API → /api/* paths (highest priority, always JSON) 2. Admin → /admin/* paths or admin.* subdomain 3. Vendor Dashboard → /vendor/* paths (vendor management area) 4. Shop → Vendor storefront (custom domain, subdomain, or shop paths) 5. Fallback → Unknown/generic context CRITICAL: Uses clean_path (if available) instead of original path. This ensures correct context detection for path-based routing. Args: request: FastAPI request object Returns: RequestContext enum value """ # Use clean_path if available (extracted by vendor_context_middleware) # Falls back to original path if clean_path not set # This is critical for correct context detection with path-based routing path = getattr(request.state, "clean_path", request.url.path) host = request.headers.get("host", "") # Remove port from host if present if ":" in host: host = host.split(":")[0] logger.debug( "[CONTEXT] Detecting context", extra={ "original_path": request.url.path, "clean_path": getattr(request.state, "clean_path", "NOT SET"), "path_to_check": path, "host": host, }, ) # 1. API context (highest priority) if path.startswith("/api/"): logger.debug("[CONTEXT] Detected as API", extra={"path": path}) return RequestContext.API # 2. Admin context if ContextManager._is_admin_context(request, host, path): logger.debug( "[CONTEXT] Detected as ADMIN", extra={"path": path, "host": host} ) return RequestContext.ADMIN # 3. Vendor Dashboard context (vendor management area) # Check both clean_path and original path for vendor dashboard original_path = request.url.path if ContextManager._is_vendor_dashboard_context( path ) or ContextManager._is_vendor_dashboard_context(original_path): logger.debug( "[CONTEXT] Detected as VENDOR_DASHBOARD", extra={"path": path, "original_path": original_path}, ) return RequestContext.VENDOR_DASHBOARD # 4. Shop context (vendor storefront) # Check if vendor context exists (set by vendor_context_middleware) if hasattr(request.state, "vendor") and request.state.vendor: # If we have a vendor and it's not admin or vendor dashboard, it's shop logger.debug( "[CONTEXT] Detected as SHOP (has vendor context)", extra={"vendor": request.state.vendor.name}, ) return RequestContext.SHOP # Also check shop-specific paths if path.startswith("/shop/"): logger.debug("[CONTEXT] Detected as SHOP (from path)", extra={"path": path}) return RequestContext.SHOP # 5. Fallback for unknown contexts logger.debug("[CONTEXT] Detected as FALLBACK", extra={"path": path}) return RequestContext.FALLBACK @staticmethod def _is_admin_context(request: Request, host: str, path: str) -> bool: """Check if request is in admin context.""" # Admin subdomain (admin.platform.com) if host.startswith("admin."): return True # Admin path (/admin/*) if path.startswith("/admin"): return True return False @staticmethod def _is_vendor_dashboard_context(path: str) -> bool: """Check if request is in vendor dashboard context.""" # Vendor dashboard paths (/vendor/{code}/*) # Note: This is the vendor management area, not the shop # Important: /vendors/{code}/shop/* should NOT match this if path.startswith("/vendor/") and not path.startswith("/vendors/"): return True return False class ContextMiddleware(BaseHTTPMiddleware): """ Middleware to detect and inject request context into request.state. Class-based middleware provides: - Better lifecycle management - Easier to test and extend - Standard ASGI pattern - Clear separation of concerns Runs SECOND in middleware chain (after vendor_context_middleware). Depends on: request.state.clean_path (set by vendor_context_middleware) request.state.vendor (set by vendor_context_middleware) Sets: request.state.context_type: RequestContext enum value """ async def dispatch(self, request: Request, call_next): """ Detect context and inject into request state. """ # Detect context context_type = ContextManager.detect_context(request) # Inject into request state request.state.context_type = context_type # Log context detection with full details logger.debug( f"[CONTEXT_MIDDLEWARE] Context detected: {context_type.value}", extra={ "path": request.url.path, "clean_path": getattr(request.state, "clean_path", "NOT SET"), "host": request.headers.get("host", ""), "context": context_type.value, "has_vendor": hasattr(request.state, "vendor") and request.state.vendor is not None, }, ) # Continue processing response = await call_next(request) return response def get_request_context(request: Request) -> RequestContext: """ Helper function to get current request context. Args: request: FastAPI request object Returns: RequestContext enum value (defaults to FALLBACK if not set) """ return getattr(request.state, "context_type", RequestContext.FALLBACK)