Files
orion/app/core/frontend_detector.py
Samir Boulahtit aad18c27ab
Some checks failed
CI / ruff (push) Successful in 11s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has started running
refactor: remove all backward compatibility code across 70 files
Clean up 28 backward compatibility instances identified in the codebase.
The app is not live, so all shims are replaced with the target architecture:

- Remove legacy Inventory.location column (use bin_location exclusively)
- Remove dashboard _extract_metric_value helper (use flat metrics dict)
- Remove legacy stat field duplicates (total_stores, total_imports, etc.)
- Remove 13 re-export shims and class aliases across modules
- Remove module-enabling JSON fallback (use PlatformModule junction table)
- Remove menu_to_legacy_format() conversion (return dataclasses directly)
- Remove title/description from MarketplaceProductBase schema
- Clean billing convenience method docstrings
- Clean test fixtures and backward-compat comments
- Add PlatformModule seeding to init_production.py

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 13:20:29 +01:00

198 lines
7.1 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.omsflow.lu)
2. Path-based admin/store (/admin/*, /store/*, /api/v1/admin/*)
3. Custom domain lookup (mybakery.lu -> STOREFRONT)
4. Store subdomain (orion.omsflow.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., "omsflow.lu", "orion.omsflow.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.omsflow.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.omsflow.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.omsflow.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/")