This commit completes the migration to a fully module-driven architecture: ## Models Migration - Moved all domain models from models/database/ to their respective modules: - tenancy: User, Admin, Vendor, Company, Platform, VendorDomain, etc. - cms: MediaFile, VendorTheme - messaging: Email, VendorEmailSettings, VendorEmailTemplate - core: AdminMenuConfig - models/database/ now only contains Base and TimestampMixin (infrastructure) ## Schemas Migration - Moved all domain schemas from models/schema/ to their respective modules: - tenancy: company, vendor, admin, team, vendor_domain - cms: media, image, vendor_theme - messaging: email - models/schema/ now only contains base.py and auth.py (infrastructure) ## Routes Migration - Moved admin routes from app/api/v1/admin/ to modules: - menu_config.py -> core module - modules.py -> tenancy module - module_config.py -> tenancy module - app/api/v1/admin/ now only aggregates auto-discovered module routes ## Menu System - Implemented module-driven menu system with MenuDiscoveryService - Extended FrontendType enum: PLATFORM, ADMIN, VENDOR, STOREFRONT - Added MenuItemDefinition and MenuSectionDefinition dataclasses - Each module now defines its own menu items in definition.py - MenuService integrates with MenuDiscoveryService for template rendering ## Documentation - Updated docs/architecture/models-structure.md - Updated docs/architecture/menu-management.md - Updated architecture validation rules for new exceptions ## Architecture Validation - Updated MOD-019 rule to allow base.py in models/schema/ - Created core module exceptions.py and schemas/ directory - All validation errors resolved (only warnings remain) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
149 lines
4.6 KiB
Python
149 lines
4.6 KiB
Python
# middleware/theme_context.py
|
|
"""
|
|
Theme Context Middleware (Class-Based)
|
|
|
|
Injects vendor-specific theme into request context.
|
|
|
|
Class-based middleware provides:
|
|
- Better state management
|
|
- Easier testing
|
|
- Standard ASGI pattern
|
|
"""
|
|
|
|
import logging
|
|
|
|
from fastapi import Request
|
|
from sqlalchemy.orm import Session
|
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
|
|
from app.core.database import get_db
|
|
from app.modules.cms.models import VendorTheme
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ThemeContextManager:
|
|
"""Manages theme context for vendor shops."""
|
|
|
|
@staticmethod
|
|
def get_vendor_theme(db: Session, vendor_id: int) -> dict:
|
|
"""
|
|
Get theme configuration for vendor.
|
|
Returns default theme if no custom theme is configured.
|
|
"""
|
|
theme = (
|
|
db.query(VendorTheme)
|
|
.filter(VendorTheme.vendor_id == vendor_id, VendorTheme.is_active == True)
|
|
.first()
|
|
)
|
|
|
|
if theme:
|
|
return theme.to_dict()
|
|
|
|
# Return default theme
|
|
return ThemeContextManager.get_default_theme()
|
|
|
|
@staticmethod
|
|
def get_default_theme() -> dict:
|
|
"""Default theme configuration"""
|
|
return {
|
|
"theme_name": "default",
|
|
"colors": {
|
|
"primary": "#6366f1",
|
|
"secondary": "#8b5cf6",
|
|
"accent": "#ec4899",
|
|
"background": "#ffffff",
|
|
"text": "#1f2937",
|
|
"border": "#e5e7eb",
|
|
},
|
|
"fonts": {"heading": "Inter, sans-serif", "body": "Inter, sans-serif"},
|
|
"branding": {
|
|
"logo": None,
|
|
"logo_dark": None,
|
|
"favicon": None,
|
|
"banner": None,
|
|
},
|
|
"layout": {"style": "grid", "header": "fixed", "product_card": "modern"},
|
|
"social_links": {},
|
|
"custom_css": None,
|
|
"css_variables": {
|
|
"--color-primary": "#6366f1",
|
|
"--color-secondary": "#8b5cf6",
|
|
"--color-accent": "#ec4899",
|
|
"--color-background": "#ffffff",
|
|
"--color-text": "#1f2937",
|
|
"--color-border": "#e5e7eb",
|
|
"--font-heading": "Inter, sans-serif",
|
|
"--font-body": "Inter, sans-serif",
|
|
},
|
|
}
|
|
|
|
|
|
class ThemeContextMiddleware(BaseHTTPMiddleware):
|
|
"""
|
|
Middleware to inject theme context into request state.
|
|
|
|
Class-based middleware provides:
|
|
- Better state management
|
|
- Easier testing
|
|
- Standard ASGI pattern
|
|
|
|
Runs LAST in middleware chain (after vendor_context_middleware and context_middleware).
|
|
Depends on:
|
|
request.state.vendor (set by vendor_context_middleware)
|
|
|
|
Sets:
|
|
request.state.theme: Theme dictionary
|
|
"""
|
|
|
|
async def dispatch(self, request: Request, call_next):
|
|
"""
|
|
Load and inject theme context.
|
|
"""
|
|
# Only inject theme for shop pages (not admin or API)
|
|
if hasattr(request.state, "vendor") and request.state.vendor:
|
|
vendor = request.state.vendor
|
|
|
|
# Get database session
|
|
db_gen = get_db()
|
|
db = next(db_gen)
|
|
|
|
try:
|
|
# Get vendor theme
|
|
theme = ThemeContextManager.get_vendor_theme(db, vendor.id)
|
|
request.state.theme = theme
|
|
|
|
logger.debug(
|
|
"[THEME] Theme loaded for vendor",
|
|
extra={
|
|
"vendor_id": vendor.id,
|
|
"vendor_name": vendor.name,
|
|
"theme_name": theme.get("theme_name", "default"),
|
|
},
|
|
)
|
|
except Exception as e:
|
|
logger.error(
|
|
f"[THEME] Failed to load theme for vendor {vendor.id}: {e}",
|
|
exc_info=True,
|
|
)
|
|
# Fallback to default theme
|
|
request.state.theme = ThemeContextManager.get_default_theme()
|
|
finally:
|
|
db.close()
|
|
else:
|
|
# No vendor context, use default theme
|
|
request.state.theme = ThemeContextManager.get_default_theme()
|
|
logger.debug(
|
|
"[THEME] No vendor context, using default theme",
|
|
extra={"has_vendor": False},
|
|
)
|
|
|
|
# Continue processing
|
|
response = await call_next(request)
|
|
return response
|
|
|
|
|
|
def get_current_theme(request: Request) -> dict:
|
|
"""Helper function to get current theme from request state."""
|
|
return getattr(request.state, "theme", ThemeContextManager.get_default_theme())
|