Replace all ~1,086 occurrences of Wizamart/wizamart/WIZAMART/WizaMart with Orion/orion/ORION across 184 files. This includes database identifiers, email addresses, domain references, R2 bucket names, DNS prefixes, encryption salt, Celery app name, config defaults, Docker configs, CI configs, documentation, seed data, and templates. Renames homepage-wizamart.html template to homepage-orion.html. Fixes duplicate file_pattern key in api.yaml architecture rule. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
208 lines
7.4 KiB
Python
208 lines
7.4 KiB
Python
# 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)
|