199 lines
6.7 KiB
Python
199 lines
6.7 KiB
Python
# 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 starlette.middleware.base import BaseHTTPMiddleware
|
|
from fastapi import Request
|
|
|
|
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(
|
|
f"[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)
|