# 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/vendor (/admin/*, /vendor/*, /api/v1/admin/*) 3. Custom domain lookup (mybakery.lu -> STOREFRONT) 4. Vendor subdomain (wizamart.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/vendor_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 vendor shops) RESERVED_SUBDOMAINS = frozenset({"www", "admin", "api", "vendor", "portal"}) # Path patterns for each frontend type # Note: Order matters - more specific patterns should be checked first ADMIN_PATH_PREFIXES = ("/admin", "/api/v1/admin") VENDOR_PATH_PREFIXES = ("/vendor/", "/api/v1/vendor") # Note: /vendor/ not /vendors/ STOREFRONT_PATH_PREFIXES = ( "/storefront", "/api/v1/storefront", "/shop", # Legacy support "/api/v1/shop", # Legacy support "/vendors/", # Path-based vendor access ) PLATFORM_PATH_PREFIXES = ("/api/v1/platform",) @classmethod def detect( cls, host: str, path: str, has_vendor_context: bool = False, ) -> FrontendType: """ Detect frontend type from request. Args: host: Request host header (e.g., "oms.lu", "wizamart.oms.lu", "localhost:8000") path: Request path (e.g., "/admin/vendors", "/storefront/products") has_vendor_context: True if request.state.vendor 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_vendor_context": has_vendor_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.VENDOR_PATH_PREFIXES): logger.debug("[FRONTEND_DETECTOR] Detected VENDOR from path") return FrontendType.VENDOR 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.PLATFORM_PATH_PREFIXES): logger.debug("[FRONTEND_DETECTOR] Detected PLATFORM from path") return FrontendType.PLATFORM # 3. Vendor subdomain detection (wizamart.oms.lu) # If subdomain exists and is not reserved -> it's a vendor shop 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 vendor context) # If vendor is set but no storefront path -> still storefront if has_vendor_context: logger.debug( "[FRONTEND_DETECTOR] Detected STOREFRONT from vendor 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., 'wizamart' from 'wizamart.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_vendor(cls, host: str, path: str) -> bool: """Check if request targets vendor dashboard frontend.""" return cls.detect(host, path) == FrontendType.VENDOR @classmethod def is_storefront( cls, host: str, path: str, has_vendor_context: bool = False, ) -> bool: """Check if request targets storefront frontend.""" return cls.detect(host, path, has_vendor_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_vendor_context: bool = False) -> FrontendType: """ Convenience function to detect frontend type. Wrapper around FrontendDetector.detect() for simpler imports. """ return FrontendDetector.detect(host, path, has_vendor_context)