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
# Import REFACTORED class-based middleware
from middleware.platform_context import PlatformContextMiddleware
from middleware.vendor_context import VendorContextMiddleware
logger = logging.getLogger(__name__)
@@ -114,17 +115,19 @@ app.add_middleware(
# 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. LanguageMiddleware (detect language based on context)
# 4. ThemeContextMiddleware (load theme)
# 5. LoggingMiddleware (log all requests)
# 1. PlatformContextMiddleware (detect platform from domain/path)
# 2. VendorContextMiddleware (detect vendor, uses platform_clean_path)
# 3. ContextMiddleware (detect context using clean_path)
# 4. LanguageMiddleware (detect language based on context)
# 5. ThemeContextMiddleware (load theme)
# 6. LoggingMiddleware (log all requests)
#
# Therefore we add them in REVERSE:
# - Add ThemeContextMiddleware FIRST (runs LAST in request)
# - Add LanguageMiddleware SECOND (runs after context)
# - Add LanguageMiddleware SECOND
# - Add ContextMiddleware THIRD
# - Add VendorContextMiddleware FOURTH
# - Add PlatformContextMiddleware FIFTH
# - 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)")
app.add_middleware(ContextMiddleware)
# Add vendor context middleware (runs first in request chain)
logger.info("Adding VendorContextMiddleware (detects vendor, extracts clean_path)")
# Add vendor context middleware (runs after platform context)
logger.info("Adding VendorContextMiddleware (detects vendor, uses platform_clean_path)")
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("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. LanguageMiddleware (language detection)")
logger.info(" 5. ThemeContextMiddleware (theme loading)")
logger.info(" 6. FastAPI Router")
logger.info(" 2. PlatformContextMiddleware (platform detection)")
logger.info(" 3. VendorContextMiddleware (vendor detection)")
logger.info(" 4. ContextMiddleware (context detection)")
logger.info(" 5. LanguageMiddleware (language detection)")
logger.info(" 6. ThemeContextMiddleware (theme loading)")
logger.info(" 7. FastAPI Router")
logger.info("=" * 80)
# ========================================
@@ -331,6 +339,7 @@ async def vendor_root_path(
"""Handle vendor root path (e.g., /vendors/wizamart/)"""
# Vendor should already be in request.state from middleware
vendor = getattr(request.state, "vendor", None)
platform = getattr(request.state, "platform", None)
if not vendor:
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.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(
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:
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:
@@ -373,29 +385,51 @@ async def platform_homepage(request: Request, db: Session = Depends(get_db)):
"""
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.
"""
from app.services.content_page_service import content_page_service
logger.debug("[PLATFORM] Homepage requested")
# Try to load platform homepage from CMS
homepage = content_page_service.get_page_for_vendor(
db,
slug="platform_homepage",
vendor_id=None, # Platform-level page
include_unpublished=False,
)
# Get platform from middleware (multi-platform support)
platform = getattr(request.state, "platform", None)
# Load header and footer navigation
header_pages = content_page_service.list_pages_for_vendor(
db, vendor_id=None, header_only=True, include_unpublished=False
)
if platform:
# Try to load platform homepage from CMS (platform marketing page)
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(
db, vendor_id=None, footer_only=True, include_unpublished=False
)
# Also try platform_homepage slug for backwards compatibility
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
language = getattr(request.state, "language", "fr")
@@ -438,7 +472,7 @@ async def platform_content_page(
"""
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.
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}")
# Load page from CMS
page = content_page_service.get_page_for_vendor(
# Get platform from middleware (multi-platform support)
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,
platform_id=platform.id,
slug=slug,
vendor_id=None,
include_unpublished=False, # Platform pages only
include_unpublished=False,
)
if not page:
logger.warning(f"[PLATFORM] Content page not found: {slug}")
raise HTTPException(status_code=404, detail=f"Page not found: {slug}")
# Load header and footer navigation
header_pages = content_page_service.list_pages_for_vendor(
db, vendor_id=None, header_only=True, 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_pages_for_vendor(
db, vendor_id=None, footer_only=True, include_unpublished=False
footer_pages = content_page_service.list_platform_pages(
db, platform_id=platform.id, footer_only=True, include_unpublished=False
)
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
"""
vendor = getattr(request.state, "vendor", None)
platform = getattr(request.state, "platform", None)
if vendor:
# Vendor context detected - serve landing page
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(
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:
# Try 'home' slug as fallback
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:

View File

@@ -9,6 +9,9 @@ Handles three routing modes:
3. Path-based (/vendor/vendor1/ or /vendors/vendor1/ → Vendor 1)
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
@@ -39,10 +42,14 @@ class VendorContextManager:
2. Subdomain (vendor1.platform.com)
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.
"""
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)
if ":" in host:
@@ -393,7 +400,9 @@ class VendorContextMiddleware(BaseHTTPMiddleware):
- More organized code
- 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:
request.state.vendor: Vendor object
request.state.vendor_context: Detection metadata