feat: integrate PlatformContextMiddleware and update routes for multi-platform

Phase 2 implementation:
- Register PlatformContextMiddleware in main.py (runs before VendorContextMiddleware)
- Update VendorContextMiddleware to use platform_clean_path
- Update platform homepage route to use three-tier CMS resolution
- Update platform content page routes with platform context
- Update vendor root path handlers with platform_id support

Middleware execution order:
1. LoggingMiddleware
2. PlatformContextMiddleware (detect platform from domain/path)
3. VendorContextMiddleware (detect vendor, uses platform_clean_path)
4. ContextMiddleware
5. LanguageMiddleware
6. ThemeContextMiddleware

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-18 20:02:38 +01:00
parent 081f81af47
commit fe49008fef
2 changed files with 100 additions and 46 deletions

133
main.py
View File

@@ -70,6 +70,7 @@ from middleware.logging import LoggingMiddleware
from middleware.theme_context import ThemeContextMiddleware from middleware.theme_context import ThemeContextMiddleware
# Import REFACTORED class-based middleware # Import REFACTORED class-based middleware
from middleware.platform_context import PlatformContextMiddleware
from middleware.vendor_context import VendorContextMiddleware from middleware.vendor_context import VendorContextMiddleware
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -114,17 +115,19 @@ app.add_middleware(
# So we add them in REVERSE order of desired execution: # So we add them in REVERSE order of desired execution:
# #
# Desired execution order: # Desired execution order:
# 1. VendorContextMiddleware (detect vendor, extract clean_path) # 1. PlatformContextMiddleware (detect platform from domain/path)
# 2. ContextMiddleware (detect context using clean_path) # 2. VendorContextMiddleware (detect vendor, uses platform_clean_path)
# 3. LanguageMiddleware (detect language based on context) # 3. ContextMiddleware (detect context using clean_path)
# 4. ThemeContextMiddleware (load theme) # 4. LanguageMiddleware (detect language based on context)
# 5. LoggingMiddleware (log all requests) # 5. ThemeContextMiddleware (load theme)
# 6. LoggingMiddleware (log all requests)
# #
# Therefore we add them in REVERSE: # Therefore we add them in REVERSE:
# - Add ThemeContextMiddleware FIRST (runs LAST in request) # - Add ThemeContextMiddleware FIRST (runs LAST in request)
# - Add LanguageMiddleware SECOND (runs after context) # - Add LanguageMiddleware SECOND
# - Add ContextMiddleware THIRD # - Add ContextMiddleware THIRD
# - Add VendorContextMiddleware FOURTH # - Add VendorContextMiddleware FOURTH
# - Add PlatformContextMiddleware FIFTH
# - Add LoggingMiddleware LAST (runs FIRST for timing) # - Add LoggingMiddleware LAST (runs FIRST for timing)
# ============================================================================ # ============================================================================
@@ -148,19 +151,24 @@ app.add_middleware(LanguageMiddleware)
logger.info("Adding ContextMiddleware (detects context type using clean_path)") logger.info("Adding ContextMiddleware (detects context type using clean_path)")
app.add_middleware(ContextMiddleware) app.add_middleware(ContextMiddleware)
# Add vendor context middleware (runs first in request chain) # Add vendor context middleware (runs after platform context)
logger.info("Adding VendorContextMiddleware (detects vendor, extracts clean_path)") logger.info("Adding VendorContextMiddleware (detects vendor, uses platform_clean_path)")
app.add_middleware(VendorContextMiddleware) app.add_middleware(VendorContextMiddleware)
# Add platform context middleware (runs first in request chain, before vendor)
logger.info("Adding PlatformContextMiddleware (detects platform from domain/path)")
app.add_middleware(PlatformContextMiddleware)
logger.info("=" * 80) logger.info("=" * 80)
logger.info("MIDDLEWARE ORDER SUMMARY:") logger.info("MIDDLEWARE ORDER SUMMARY:")
logger.info(" Execution order (request →):") logger.info(" Execution order (request →):")
logger.info(" 1. LoggingMiddleware (timing)") logger.info(" 1. LoggingMiddleware (timing)")
logger.info(" 2. VendorContextMiddleware (vendor detection)") logger.info(" 2. PlatformContextMiddleware (platform detection)")
logger.info(" 3. ContextMiddleware (context detection)") logger.info(" 3. VendorContextMiddleware (vendor detection)")
logger.info(" 4. LanguageMiddleware (language detection)") logger.info(" 4. ContextMiddleware (context detection)")
logger.info(" 5. ThemeContextMiddleware (theme loading)") logger.info(" 5. LanguageMiddleware (language detection)")
logger.info(" 6. FastAPI Router") logger.info(" 6. ThemeContextMiddleware (theme loading)")
logger.info(" 7. FastAPI Router")
logger.info("=" * 80) logger.info("=" * 80)
# ======================================== # ========================================
@@ -331,6 +339,7 @@ async def vendor_root_path(
"""Handle vendor root path (e.g., /vendors/wizamart/)""" """Handle vendor root path (e.g., /vendors/wizamart/)"""
# Vendor should already be in request.state from middleware # Vendor should already be in request.state from middleware
vendor = getattr(request.state, "vendor", None) vendor = getattr(request.state, "vendor", None)
platform = getattr(request.state, "platform", None)
if not vendor: if not vendor:
raise HTTPException(status_code=404, detail=f"Vendor '{vendor_code}' not found") raise HTTPException(status_code=404, detail=f"Vendor '{vendor_code}' not found")
@@ -338,14 +347,17 @@ async def vendor_root_path(
from app.routes.shop_pages import get_shop_context from app.routes.shop_pages import get_shop_context
from app.services.content_page_service import content_page_service from app.services.content_page_service import content_page_service
# Try to find landing page # Get platform_id (use platform from context or default to 1 for OMS)
platform_id = platform.id if platform else 1
# Try to find landing page (with three-tier resolution)
landing_page = content_page_service.get_page_for_vendor( landing_page = content_page_service.get_page_for_vendor(
db, slug="landing", vendor_id=vendor.id, include_unpublished=False db, platform_id=platform_id, slug="landing", vendor_id=vendor.id, include_unpublished=False
) )
if not landing_page: if not landing_page:
landing_page = content_page_service.get_page_for_vendor( landing_page = content_page_service.get_page_for_vendor(
db, slug="home", vendor_id=vendor.id, include_unpublished=False db, platform_id=platform_id, slug="home", vendor_id=vendor.id, include_unpublished=False
) )
if landing_page: if landing_page:
@@ -373,29 +385,51 @@ async def platform_homepage(request: Request, db: Session = Depends(get_db)):
""" """
Platform homepage at localhost:8000 or platform.com Platform homepage at localhost:8000 or platform.com
Looks for CMS page with slug='platform_homepage' (vendor_id=NULL) Uses multi-platform CMS with three-tier resolution:
1. Platform marketing pages (is_platform_page=True)
2. Vendor default pages (fallback)
3. Vendor override pages
Falls back to default static template if not found. Falls back to default static template if not found.
""" """
from app.services.content_page_service import content_page_service from app.services.content_page_service import content_page_service
logger.debug("[PLATFORM] Homepage requested") logger.debug("[PLATFORM] Homepage requested")
# Try to load platform homepage from CMS # Get platform from middleware (multi-platform support)
homepage = content_page_service.get_page_for_vendor( platform = getattr(request.state, "platform", None)
db,
slug="platform_homepage",
vendor_id=None, # Platform-level page
include_unpublished=False,
)
# Load header and footer navigation if platform:
header_pages = content_page_service.list_pages_for_vendor( # Try to load platform homepage from CMS (platform marketing page)
db, vendor_id=None, header_only=True, include_unpublished=False homepage = content_page_service.get_platform_page(
) db,
platform_id=platform.id,
slug="home",
include_unpublished=False,
)
footer_pages = content_page_service.list_pages_for_vendor( # Also try platform_homepage slug for backwards compatibility
db, vendor_id=None, footer_only=True, include_unpublished=False if not homepage:
) homepage = content_page_service.get_platform_page(
db,
platform_id=platform.id,
slug="platform_homepage",
include_unpublished=False,
)
# Load header and footer navigation (platform marketing pages)
header_pages = content_page_service.list_platform_pages(
db, platform_id=platform.id, header_only=True, include_unpublished=False
)
footer_pages = content_page_service.list_platform_pages(
db, platform_id=platform.id, footer_only=True, include_unpublished=False
)
else:
# Fallback for when no platform context (shouldn't happen normally)
homepage = None
header_pages = []
footer_pages = []
# Get language from request state and build i18n context # Get language from request state and build i18n context
language = getattr(request.state, "language", "fr") language = getattr(request.state, "language", "fr")
@@ -438,7 +472,7 @@ async def platform_content_page(
""" """
Platform content pages: /about, /faq, /terms, /contact, etc. Platform content pages: /about, /faq, /terms, /contact, etc.
Loads content from CMS with slug (vendor_id=NULL for platform pages). Uses multi-platform CMS with three-tier resolution.
Returns 404 if page not found. Returns 404 if page not found.
This route MUST be defined LAST to avoid conflicts with other routes. This route MUST be defined LAST to avoid conflicts with other routes.
@@ -447,25 +481,32 @@ async def platform_content_page(
logger.debug(f"[PLATFORM] Content page requested: /{slug}") logger.debug(f"[PLATFORM] Content page requested: /{slug}")
# Load page from CMS # Get platform from middleware (multi-platform support)
page = content_page_service.get_page_for_vendor( platform = getattr(request.state, "platform", None)
if not platform:
logger.warning(f"[PLATFORM] No platform context for content page: {slug}")
raise HTTPException(status_code=404, detail=f"Page not found: {slug}")
# Load platform marketing page from CMS
page = content_page_service.get_platform_page(
db, db,
platform_id=platform.id,
slug=slug, slug=slug,
vendor_id=None, include_unpublished=False,
include_unpublished=False, # Platform pages only
) )
if not page: if not page:
logger.warning(f"[PLATFORM] Content page not found: {slug}") logger.warning(f"[PLATFORM] Content page not found: {slug}")
raise HTTPException(status_code=404, detail=f"Page not found: {slug}") raise HTTPException(status_code=404, detail=f"Page not found: {slug}")
# Load header and footer navigation # Load header and footer navigation (platform marketing pages)
header_pages = content_page_service.list_pages_for_vendor( header_pages = content_page_service.list_platform_pages(
db, vendor_id=None, header_only=True, include_unpublished=False db, platform_id=platform.id, header_only=True, include_unpublished=False
) )
footer_pages = content_page_service.list_pages_for_vendor( footer_pages = content_page_service.list_platform_pages(
db, vendor_id=None, footer_only=True, include_unpublished=False db, platform_id=platform.id, footer_only=True, include_unpublished=False
) )
logger.info(f"[PLATFORM] Rendering content page: {page.title} (/{slug})") logger.info(f"[PLATFORM] Rendering content page: {page.title} (/{slug})")
@@ -511,20 +552,24 @@ async def root(request: Request, db: Session = Depends(get_db)):
- If no vendor (platform root): Redirect to documentation - If no vendor (platform root): Redirect to documentation
""" """
vendor = getattr(request.state, "vendor", None) vendor = getattr(request.state, "vendor", None)
platform = getattr(request.state, "platform", None)
if vendor: if vendor:
# Vendor context detected - serve landing page # Vendor context detected - serve landing page
from app.services.content_page_service import content_page_service from app.services.content_page_service import content_page_service
# Try to find landing page (slug='landing' or 'home') # Get platform_id (use platform from context or default to 1 for OMS)
platform_id = platform.id if platform else 1
# Try to find landing page (slug='landing' or 'home') with three-tier resolution
landing_page = content_page_service.get_page_for_vendor( landing_page = content_page_service.get_page_for_vendor(
db, slug="landing", vendor_id=vendor.id, include_unpublished=False db, platform_id=platform_id, slug="landing", vendor_id=vendor.id, include_unpublished=False
) )
if not landing_page: if not landing_page:
# Try 'home' slug as fallback # Try 'home' slug as fallback
landing_page = content_page_service.get_page_for_vendor( landing_page = content_page_service.get_page_for_vendor(
db, slug="home", vendor_id=vendor.id, include_unpublished=False db, platform_id=platform_id, slug="home", vendor_id=vendor.id, include_unpublished=False
) )
if landing_page: if landing_page:

View File

@@ -9,6 +9,9 @@ Handles three routing modes:
3. Path-based (/vendor/vendor1/ or /vendors/vendor1/ → Vendor 1) 3. Path-based (/vendor/vendor1/ or /vendors/vendor1/ → Vendor 1)
Also extracts clean_path for nested routing patterns. Also extracts clean_path for nested routing patterns.
IMPORTANT: This middleware runs AFTER PlatformContextMiddleware.
Uses request.state.platform_clean_path when available (set by PlatformContextMiddleware).
""" """
import logging import logging
@@ -39,10 +42,14 @@ class VendorContextManager:
2. Subdomain (vendor1.platform.com) 2. Subdomain (vendor1.platform.com)
3. Path-based (/vendor/vendor1/ or /vendors/vendor1/) 3. Path-based (/vendor/vendor1/ or /vendors/vendor1/)
Uses platform_clean_path from PlatformContextMiddleware when available.
This path has the platform prefix stripped (e.g., /oms/vendors/foo → /vendors/foo).
Returns dict with vendor info or None if not found. Returns dict with vendor info or None if not found.
""" """
host = request.headers.get("host", "") host = request.headers.get("host", "")
path = request.url.path # Use platform_clean_path if available (set by PlatformContextMiddleware)
path = getattr(request.state, "platform_clean_path", None) or request.url.path
# Remove port from host if present (e.g., localhost:8000 -> localhost) # Remove port from host if present (e.g., localhost:8000 -> localhost)
if ":" in host: if ":" in host:
@@ -393,7 +400,9 @@ class VendorContextMiddleware(BaseHTTPMiddleware):
- More organized code - More organized code
- Standard ASGI pattern - Standard ASGI pattern
Runs FIRST in middleware chain. Runs AFTER PlatformContextMiddleware in the request chain.
Uses request.state.platform_clean_path for path-based vendor detection.
Sets: Sets:
request.state.vendor: Vendor object request.state.vendor: Vendor object
request.state.vendor_context: Detection metadata request.state.vendor_context: Detection metadata