# middleware/theme_context.py """ Theme Context Middleware (Class-Based) Injects store-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 StoreTheme logger = logging.getLogger(__name__) class ThemeContextManager: """Manages theme context for store shops.""" @staticmethod def get_store_theme(db: Session, store_id: int) -> dict: """ Get theme configuration for store. Returns default theme if no custom theme is configured. """ theme = ( db.query(StoreTheme) .filter(StoreTheme.store_id == store_id, StoreTheme.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 store_context_middleware and context_middleware). Depends on: request.state.store (set by store_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, "store") and request.state.store: store = request.state.store # Get database session db_gen = get_db() db = next(db_gen) try: # Get store theme theme = ThemeContextManager.get_store_theme(db, store.id) request.state.theme = theme logger.debug( "[THEME] Theme loaded for store", extra={ "store_id": store.id, "store_name": store.name, "theme_name": theme.get("theme_name", "default"), }, ) except Exception as e: logger.error( f"[THEME] Failed to load theme for store {store.id}: {e}", exc_info=True, ) # Fallback to default theme request.state.theme = ThemeContextManager.get_default_theme() finally: db.close() else: # No store context, use default theme request.state.theme = ThemeContextManager.get_default_theme() logger.debug( "[THEME] No store context, using default theme", extra={"has_store": 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())