# app/core/frontend_detector.py """ Centralized Frontend Detection Single source of truth for detecting which frontend type a request targets. Handles both development (path-based) and production (domain-based) routing. Detection priority: 1. Admin subdomain (admin.oms.lu) 2. Path-based admin/store (/admin/*, /store/*, /api/v1/admin/*) 3. Custom domain lookup (mybakery.lu -> STOREFRONT) 4. Store subdomain (orion.oms.lu -> STOREFRONT) 5. Storefront paths (/storefront/*, /api/v1/storefront/*) 6. Default to PLATFORM (marketing pages) This module unifies frontend detection that was previously duplicated across: - middleware/platform_context.py - middleware/store_context.py - middleware/context.py All middleware and routes should use FrontendDetector for frontend detection. """ import logging from app.modules.enums import FrontendType logger = logging.getLogger(__name__) class FrontendDetector: """ Centralized frontend detection for dev and prod modes. Provides consistent detection of frontend type from request characteristics. All path/domain detection logic should be centralized here. """ # Reserved subdomains (not store shops) RESERVED_SUBDOMAINS = frozenset({"www", "admin", "api", "store", "portal"}) # Path patterns for each frontend type # Note: Order matters - more specific patterns should be checked first ADMIN_PATH_PREFIXES = ("/admin", "/api/v1/admin") STORE_PATH_PREFIXES = ("/store/", "/api/v1/store") # Note: /store/ not /stores/ STOREFRONT_PATH_PREFIXES = ( "/storefront", "/api/v1/storefront", "/stores/", # Path-based store access ) MERCHANT_PATH_PREFIXES = ("/merchants", "/api/v1/merchants") PLATFORM_PATH_PREFIXES = ("/api/v1/platform",) @classmethod def detect( cls, host: str, path: str, has_store_context: bool = False, ) -> FrontendType: """ Detect frontend type from request. Args: host: Request host header (e.g., "oms.lu", "orion.oms.lu", "localhost:8000") path: Request path (e.g., "/admin/stores", "/storefront/products") has_store_context: True if request.state.store is set (from middleware) Returns: FrontendType enum value """ host = cls._strip_port(host) subdomain = cls._get_subdomain(host) logger.debug( "[FRONTEND_DETECTOR] Detecting frontend type", extra={ "host": host, "path": path, "subdomain": subdomain, "has_store_context": has_store_context, }, ) # 1. Admin subdomain (admin.oms.lu) if subdomain == "admin": logger.debug("[FRONTEND_DETECTOR] Detected ADMIN from subdomain") return FrontendType.ADMIN # 2. Path-based detection (works for dev and prod) # Check in priority order if cls._matches_any(path, cls.ADMIN_PATH_PREFIXES): logger.debug("[FRONTEND_DETECTOR] Detected ADMIN from path") return FrontendType.ADMIN if cls._matches_any(path, cls.MERCHANT_PATH_PREFIXES): logger.debug("[FRONTEND_DETECTOR] Detected MERCHANT from path") return FrontendType.MERCHANT # Check storefront BEFORE store since /api/v1/storefront starts with /api/v1/store if cls._matches_any(path, cls.STOREFRONT_PATH_PREFIXES): logger.debug("[FRONTEND_DETECTOR] Detected STOREFRONT from path") return FrontendType.STOREFRONT if cls._matches_any(path, cls.STORE_PATH_PREFIXES): logger.debug("[FRONTEND_DETECTOR] Detected STORE from path") return FrontendType.STORE if cls._matches_any(path, cls.PLATFORM_PATH_PREFIXES): logger.debug("[FRONTEND_DETECTOR] Detected PLATFORM from path") return FrontendType.PLATFORM # 3. Store subdomain detection (orion.oms.lu) # If subdomain exists and is not reserved -> it's a store storefront if subdomain and subdomain not in cls.RESERVED_SUBDOMAINS: logger.debug( f"[FRONTEND_DETECTOR] Detected STOREFRONT from subdomain: {subdomain}" ) return FrontendType.STOREFRONT # 4. Custom domain detection (handled by middleware setting store context) # If store is set but no storefront path -> still storefront if has_store_context: logger.debug( "[FRONTEND_DETECTOR] Detected STOREFRONT from store context" ) return FrontendType.STOREFRONT # 5. Default: PLATFORM (marketing pages like /, /pricing, /about) logger.debug("[FRONTEND_DETECTOR] Defaulting to PLATFORM") return FrontendType.PLATFORM @classmethod def _strip_port(cls, host: str) -> str: """Remove port from host if present (e.g., localhost:8000 -> localhost).""" return host.split(":")[0] if ":" in host else host @classmethod def _get_subdomain(cls, host: str) -> str | None: """ Extract subdomain from host (e.g., 'orion' from 'orion.oms.lu'). Returns None for localhost, IP addresses, or root domains. Handles special case of admin.localhost for development. """ if host in ("localhost", "127.0.0.1"): return None parts = host.split(".") # Handle localhost subdomains (e.g., admin.localhost) if len(parts) == 2 and parts[1] == "localhost": return parts[0].lower() if len(parts) >= 3: # subdomain.domain.tld return parts[0].lower() return None @classmethod def _matches_any(cls, path: str, prefixes: tuple[str, ...]) -> bool: """Check if path starts with any of the given prefixes.""" return any(path.startswith(prefix) for prefix in prefixes) # ========================================================================= # Convenience methods for specific frontend types # ========================================================================= @classmethod def is_admin(cls, host: str, path: str) -> bool: """Check if request targets admin frontend.""" return cls.detect(host, path) == FrontendType.ADMIN @classmethod def is_store(cls, host: str, path: str) -> bool: """Check if request targets store dashboard frontend.""" return cls.detect(host, path) == FrontendType.STORE @classmethod def is_storefront( cls, host: str, path: str, has_store_context: bool = False, ) -> bool: """Check if request targets storefront frontend.""" return cls.detect(host, path, has_store_context) == FrontendType.STOREFRONT @classmethod def is_platform(cls, host: str, path: str) -> bool: """Check if request targets platform marketing frontend.""" return cls.detect(host, path) == FrontendType.PLATFORM @classmethod def is_api_request(cls, path: str) -> bool: """Check if request is for API endpoints (any frontend's API).""" return path.startswith("/api/") # Convenience function for backwards compatibility def get_frontend_type(host: str, path: str, has_store_context: bool = False) -> FrontendType: """ Convenience function to detect frontend type. Wrapper around FrontendDetector.detect() for simpler imports. """ return FrontendDetector.detect(host, path, has_store_context)