middleware fix for path-based vendor url

This commit is contained in:
2025-11-09 18:47:53 +01:00
parent 79dfcab09f
commit adbcee4ce3
13 changed files with 2078 additions and 810 deletions

View File

@@ -1,14 +1,23 @@
# middleware/context_middleware.py
"""
Context Detection Middleware
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.
This middleware runs independently and complements vendor_context_middleware.
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__)
@@ -38,42 +47,68 @@ class ContextManager:
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
"""
path = request.url.path
# 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)
if ContextManager._is_vendor_dashboard_context(path):
logger.debug("[CONTEXT] Detected as VENDOR_DASHBOARD", extra={"path": 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
@@ -92,43 +127,59 @@ class ContextManager:
@staticmethod
def _is_vendor_dashboard_context(path: str) -> bool:
"""Check if request is in vendor dashboard context."""
# Vendor dashboard paths (/vendor/*)
# Vendor dashboard paths (/vendor/{code}/*)
# Note: This is the vendor management area, not the shop
if path.startswith("/vendor/"):
# Important: /vendors/{code}/shop/* should NOT match this
if path.startswith("/vendor/") and not path.startswith("/vendors/"):
return True
return False
async def context_middleware(request: Request, call_next):
class ContextMiddleware(BaseHTTPMiddleware):
"""
Middleware to detect and inject request context into request.state.
This should run AFTER vendor_context_middleware to have access to
vendor information if available.
Class-based middleware provides:
- Better lifecycle management
- Easier to test and extend
- Standard ASGI pattern
- Clear separation of concerns
Injects:
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
"""
# Detect context
context_type = ContextManager.detect_context(request)
# Inject into request state
request.state.context_type = context_type
async def dispatch(self, request: Request, call_next):
"""
Detect context and inject into request state.
"""
# Detect context
context_type = ContextManager.detect_context(request)
# Log context detection (debug level)
logger.debug(
f"[CONTEXT] Request context detected: {context_type.value}",
extra={
"path": request.url.path,
"host": request.headers.get("host", ""),
"context": context_type.value,
}
)
# Inject into request state
request.state.context_type = context_type
# Continue processing
response = await call_next(request)
return response
# 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: