refactor: centralize frontend detection with FrontendDetector
Major architecture change to unify frontend detection: ## Problem Solved - Eliminated code duplication across 3 middleware files - Fixed incomplete path detection (now detects /api/v1/admin/*) - Unified on FrontendType enum (deprecates RequestContext) - Added request.state.frontend_type for all requests ## New Components - app/core/frontend_detector.py: Centralized FrontendDetector class - middleware/frontend_type.py: FrontendTypeMiddleware (replaces ContextMiddleware) - docs/architecture/frontend-detection.md: Complete architecture documentation ## Changes - main.py: Use FrontendTypeMiddleware instead of ContextMiddleware - middleware/context.py: Deprecated (kept for backwards compatibility) - middleware/platform_context.py: Use FrontendDetector.is_admin() - middleware/vendor_context.py: Use FrontendDetector.is_admin() - middleware/language.py: Use FrontendType instead of context_value - app/exceptions/handler.py: Use FrontendType.STOREFRONT - app/exceptions/error_renderer.py: Use FrontendType - Customer routes: Cookie path changed from /shop to /storefront ## Documentation - docs/architecture/frontend-detection.md: New comprehensive docs - docs/architecture/middleware.md: Updated for new system - docs/architecture/request-flow.md: Updated for FrontendType - docs/backend/middleware-reference.md: Updated API reference ## Tests - tests/unit/core/test_frontend_detector.py: 37 new tests - tests/unit/middleware/test_frontend_type.py: 11 new tests - tests/unit/middleware/test_context.py: Updated for compatibility Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,31 +1,52 @@
|
||||
# middleware/context_middleware.py
|
||||
# middleware/context.py
|
||||
"""
|
||||
Context Detection Middleware (Class-Based)
|
||||
DEPRECATED: This module is deprecated in favor of middleware/frontend_type.py
|
||||
|
||||
Detects the request context type (API, Admin, Vendor Dashboard, Shop, or Fallback)
|
||||
and injects it into request.state for use by error handlers and other components.
|
||||
The RequestContext enum and ContextMiddleware have been replaced by:
|
||||
- FrontendType enum (app/modules/enums.py)
|
||||
- FrontendTypeMiddleware (middleware/frontend_type.py)
|
||||
- FrontendDetector (app/core/frontend_detector.py)
|
||||
|
||||
MUST run AFTER vendor_context_middleware to have access to clean_path.
|
||||
MUST run BEFORE theme_context_middleware (which needs context_type).
|
||||
This file is kept for backwards compatibility during the migration period.
|
||||
All new code should use FrontendType and FrontendTypeMiddleware instead.
|
||||
|
||||
Class-based middleware provides:
|
||||
- Better state management
|
||||
- Easier testing
|
||||
- More organized code
|
||||
- Standard ASGI pattern
|
||||
Migration guide:
|
||||
- RequestContext.API -> Check with FrontendDetector.is_api_request()
|
||||
- RequestContext.ADMIN -> FrontendType.ADMIN
|
||||
- RequestContext.VENDOR_DASHBOARD -> FrontendType.VENDOR
|
||||
- RequestContext.SHOP -> FrontendType.STOREFRONT
|
||||
- RequestContext.FALLBACK -> FrontendType.PLATFORM (or handle API separately)
|
||||
|
||||
- get_request_context(request) -> get_frontend_type(request)
|
||||
- request.state.context_type -> request.state.frontend_type
|
||||
"""
|
||||
|
||||
import logging
|
||||
import warnings
|
||||
from enum import Enum
|
||||
|
||||
from fastapi import Request
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
|
||||
from app.modules.enums import FrontendType
|
||||
from middleware.frontend_type import get_frontend_type
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RequestContext(str, Enum):
|
||||
"""Request context types for the application."""
|
||||
"""
|
||||
DEPRECATED: Use FrontendType enum instead.
|
||||
|
||||
Request context types for the application.
|
||||
This enum is kept for backwards compatibility.
|
||||
|
||||
Migration:
|
||||
- API -> Use FrontendDetector.is_api_request() + FrontendType
|
||||
- ADMIN -> FrontendType.ADMIN
|
||||
- VENDOR_DASHBOARD -> FrontendType.VENDOR
|
||||
- SHOP -> FrontendType.STOREFRONT
|
||||
- FALLBACK -> FrontendType.PLATFORM
|
||||
"""
|
||||
|
||||
API = "api"
|
||||
ADMIN = "admin"
|
||||
@@ -34,169 +55,12 @@ class RequestContext(str, Enum):
|
||||
FALLBACK = "fallback"
|
||||
|
||||
|
||||
class ContextManager:
|
||||
"""Manages context detection for multi-area application."""
|
||||
|
||||
@staticmethod
|
||||
def detect_context(request: Request) -> RequestContext:
|
||||
"""
|
||||
Detect the request context type.
|
||||
|
||||
Priority order:
|
||||
1. API → /api/* paths (highest priority, always JSON)
|
||||
2. Admin → /admin/* paths or admin.* subdomain
|
||||
3. Vendor Dashboard → /vendor/* paths (vendor management area)
|
||||
4. Shop → Vendor storefront (custom domain, subdomain, or shop paths)
|
||||
5. Fallback → Unknown/generic context
|
||||
|
||||
CRITICAL: Uses clean_path (if available) instead of original path.
|
||||
This ensures correct context detection for path-based routing.
|
||||
|
||||
Args:
|
||||
request: FastAPI request object
|
||||
|
||||
Returns:
|
||||
RequestContext enum value
|
||||
"""
|
||||
# Use clean_path if available (extracted by vendor_context_middleware)
|
||||
# Falls back to original path if clean_path not set
|
||||
# This is critical for correct context detection with path-based routing
|
||||
path = getattr(request.state, "clean_path", request.url.path)
|
||||
|
||||
host = request.headers.get("host", "")
|
||||
|
||||
# Remove port from host if present
|
||||
if ":" in host:
|
||||
host = host.split(":")[0]
|
||||
|
||||
logger.debug(
|
||||
"[CONTEXT] Detecting context",
|
||||
extra={
|
||||
"original_path": request.url.path,
|
||||
"clean_path": getattr(request.state, "clean_path", "NOT SET"),
|
||||
"path_to_check": path,
|
||||
"host": host,
|
||||
},
|
||||
)
|
||||
|
||||
# 1. API context (highest priority)
|
||||
if path.startswith("/api/"):
|
||||
logger.debug("[CONTEXT] Detected as API", extra={"path": path})
|
||||
return RequestContext.API
|
||||
|
||||
# 2. Admin context
|
||||
if ContextManager._is_admin_context(request, host, path):
|
||||
logger.debug(
|
||||
"[CONTEXT] Detected as ADMIN", extra={"path": path, "host": host}
|
||||
)
|
||||
return RequestContext.ADMIN
|
||||
|
||||
# 3. Vendor Dashboard context (vendor management area)
|
||||
# Check both clean_path and original path for vendor dashboard
|
||||
original_path = request.url.path
|
||||
if ContextManager._is_vendor_dashboard_context(
|
||||
path
|
||||
) or ContextManager._is_vendor_dashboard_context(original_path):
|
||||
logger.debug(
|
||||
"[CONTEXT] Detected as VENDOR_DASHBOARD",
|
||||
extra={"path": path, "original_path": original_path},
|
||||
)
|
||||
return RequestContext.VENDOR_DASHBOARD
|
||||
|
||||
# 4. Shop context (vendor storefront)
|
||||
# Check if vendor context exists (set by vendor_context_middleware)
|
||||
if hasattr(request.state, "vendor") and request.state.vendor:
|
||||
# If we have a vendor and it's not admin or vendor dashboard, it's shop
|
||||
logger.debug(
|
||||
"[CONTEXT] Detected as SHOP (has vendor context)",
|
||||
extra={"vendor": request.state.vendor.name},
|
||||
)
|
||||
return RequestContext.SHOP
|
||||
|
||||
# Also check shop-specific paths
|
||||
if path.startswith("/shop/"):
|
||||
logger.debug("[CONTEXT] Detected as SHOP (from path)", extra={"path": path})
|
||||
return RequestContext.SHOP
|
||||
|
||||
# 5. Fallback for unknown contexts
|
||||
logger.debug("[CONTEXT] Detected as FALLBACK", extra={"path": path})
|
||||
return RequestContext.FALLBACK
|
||||
|
||||
@staticmethod
|
||||
def _is_admin_context(request: Request, host: str, path: str) -> bool:
|
||||
"""Check if request is in admin context."""
|
||||
# Admin subdomain (admin.platform.com)
|
||||
if host.startswith("admin."):
|
||||
return True
|
||||
|
||||
# Admin path (/admin/*)
|
||||
if path.startswith("/admin"):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _is_vendor_dashboard_context(path: str) -> bool:
|
||||
"""Check if request is in vendor dashboard context."""
|
||||
# Vendor dashboard paths (/vendor/{code}/*)
|
||||
# Note: This is the vendor management area, not the shop
|
||||
# Important: /vendors/{code}/shop/* should NOT match this
|
||||
if path.startswith("/vendor/") and not path.startswith("/vendors/"):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class ContextMiddleware(BaseHTTPMiddleware):
|
||||
"""
|
||||
Middleware to detect and inject request context into request.state.
|
||||
|
||||
Class-based middleware provides:
|
||||
- Better lifecycle management
|
||||
- Easier to test and extend
|
||||
- Standard ASGI pattern
|
||||
- Clear separation of concerns
|
||||
|
||||
Runs SECOND in middleware chain (after vendor_context_middleware).
|
||||
Depends on:
|
||||
request.state.clean_path (set by vendor_context_middleware)
|
||||
request.state.vendor (set by vendor_context_middleware)
|
||||
|
||||
Sets:
|
||||
request.state.context_type: RequestContext enum value
|
||||
"""
|
||||
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
"""
|
||||
Detect context and inject into request state.
|
||||
"""
|
||||
# Detect context
|
||||
context_type = ContextManager.detect_context(request)
|
||||
|
||||
# Inject into request state
|
||||
request.state.context_type = context_type
|
||||
|
||||
# Log context detection with full details
|
||||
logger.debug(
|
||||
f"[CONTEXT_MIDDLEWARE] Context detected: {context_type.value}",
|
||||
extra={
|
||||
"path": request.url.path,
|
||||
"clean_path": getattr(request.state, "clean_path", "NOT SET"),
|
||||
"host": request.headers.get("host", ""),
|
||||
"context": context_type.value,
|
||||
"has_vendor": hasattr(request.state, "vendor")
|
||||
and request.state.vendor is not None,
|
||||
},
|
||||
)
|
||||
|
||||
# Continue processing
|
||||
response = await call_next(request)
|
||||
return response
|
||||
|
||||
|
||||
def get_request_context(request: Request) -> RequestContext:
|
||||
"""
|
||||
DEPRECATED: Use get_frontend_type() from middleware.frontend_type instead.
|
||||
|
||||
Helper function to get current request context.
|
||||
This function maps FrontendType to RequestContext for backwards compatibility.
|
||||
|
||||
Args:
|
||||
request: FastAPI request object
|
||||
@@ -204,4 +68,33 @@ def get_request_context(request: Request) -> RequestContext:
|
||||
Returns:
|
||||
RequestContext enum value (defaults to FALLBACK if not set)
|
||||
"""
|
||||
return getattr(request.state, "context_type", RequestContext.FALLBACK)
|
||||
warnings.warn(
|
||||
"get_request_context() is deprecated. Use get_frontend_type() from "
|
||||
"middleware.frontend_type instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
# Get the new frontend_type
|
||||
frontend_type = get_frontend_type(request)
|
||||
|
||||
# Map FrontendType to RequestContext for backwards compatibility
|
||||
mapping = {
|
||||
FrontendType.ADMIN: RequestContext.ADMIN,
|
||||
FrontendType.VENDOR: RequestContext.VENDOR_DASHBOARD,
|
||||
FrontendType.STOREFRONT: RequestContext.SHOP,
|
||||
FrontendType.PLATFORM: RequestContext.FALLBACK,
|
||||
}
|
||||
|
||||
# Check if it's an API request
|
||||
if request.url.path.startswith("/api/"):
|
||||
return RequestContext.API
|
||||
|
||||
return mapping.get(frontend_type, RequestContext.FALLBACK)
|
||||
|
||||
|
||||
# ContextManager and ContextMiddleware are removed.
|
||||
# They have been replaced by FrontendDetector and FrontendTypeMiddleware.
|
||||
# Import from the new locations:
|
||||
# from app.core.frontend_detector import FrontendDetector
|
||||
# from middleware.frontend_type import FrontendTypeMiddleware, get_frontend_type
|
||||
|
||||
92
middleware/frontend_type.py
Normal file
92
middleware/frontend_type.py
Normal file
@@ -0,0 +1,92 @@
|
||||
# middleware/frontend_type.py
|
||||
"""
|
||||
Frontend Type Detection Middleware
|
||||
|
||||
Sets request.state.frontend_type for all requests using centralized FrontendDetector.
|
||||
|
||||
This middleware replaces the old ContextMiddleware and provides a unified way to
|
||||
detect which frontend (ADMIN, VENDOR, STOREFRONT, PLATFORM) is being accessed.
|
||||
|
||||
MUST run AFTER VendorContextMiddleware to have access to vendor context.
|
||||
MUST run BEFORE LanguageMiddleware (which needs frontend_type).
|
||||
|
||||
Sets:
|
||||
request.state.frontend_type: FrontendType enum value
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import Request
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
|
||||
from app.core.frontend_detector import FrontendDetector
|
||||
from app.modules.enums import FrontendType
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FrontendTypeMiddleware(BaseHTTPMiddleware):
|
||||
"""
|
||||
Middleware to detect and inject frontend type into request state.
|
||||
|
||||
Uses FrontendDetector for centralized, consistent detection across the app.
|
||||
|
||||
Runs AFTER VendorContextMiddleware in request chain.
|
||||
Depends on:
|
||||
request.state.vendor (optional, set by VendorContextMiddleware)
|
||||
request.state.clean_path (optional, set by VendorContextMiddleware)
|
||||
|
||||
Sets:
|
||||
request.state.frontend_type: FrontendType enum value
|
||||
"""
|
||||
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
"""Detect frontend type and inject into request state."""
|
||||
host = request.headers.get("host", "")
|
||||
# Use clean_path if available (from vendor_context_middleware), else original path
|
||||
path = getattr(request.state, "clean_path", None) or request.url.path
|
||||
|
||||
# Check if vendor context exists (set by VendorContextMiddleware)
|
||||
has_vendor_context = (
|
||||
hasattr(request.state, "vendor")
|
||||
and request.state.vendor is not None
|
||||
)
|
||||
|
||||
# Detect frontend type using centralized detector
|
||||
frontend_type = FrontendDetector.detect(
|
||||
host=host,
|
||||
path=path,
|
||||
has_vendor_context=has_vendor_context,
|
||||
)
|
||||
|
||||
# Store in request state
|
||||
request.state.frontend_type = frontend_type
|
||||
|
||||
# Log detection for debugging
|
||||
logger.debug(
|
||||
f"[FRONTEND_TYPE_MIDDLEWARE] Frontend type detected: {frontend_type.value}",
|
||||
extra={
|
||||
"path": request.url.path,
|
||||
"clean_path": getattr(request.state, "clean_path", "NOT SET"),
|
||||
"host": host,
|
||||
"frontend_type": frontend_type.value,
|
||||
"has_vendor": has_vendor_context,
|
||||
},
|
||||
)
|
||||
|
||||
# Continue processing
|
||||
response = await call_next(request)
|
||||
return response
|
||||
|
||||
|
||||
def get_frontend_type(request: Request) -> FrontendType:
|
||||
"""
|
||||
Helper function to get current frontend type from request.
|
||||
|
||||
Args:
|
||||
request: FastAPI request object
|
||||
|
||||
Returns:
|
||||
FrontendType enum value (defaults to PLATFORM if not set)
|
||||
"""
|
||||
return getattr(request.state, "frontend_type", FrontendType.PLATFORM)
|
||||
@@ -18,6 +18,7 @@ from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response
|
||||
|
||||
from app.modules.enums import FrontendType
|
||||
from app.utils.i18n import (
|
||||
DEFAULT_LANGUAGE,
|
||||
SUPPORTED_LANGUAGES,
|
||||
@@ -45,9 +46,8 @@ class LanguageMiddleware(BaseHTTPMiddleware):
|
||||
|
||||
async def dispatch(self, request: Request, call_next) -> Response:
|
||||
"""Process the request and set language."""
|
||||
# Get context type from previous middleware
|
||||
context_type = getattr(request.state, "context_type", None)
|
||||
context_value = context_type.value if context_type else None
|
||||
# Get frontend type from FrontendTypeMiddleware
|
||||
frontend_type = getattr(request.state, "frontend_type", None)
|
||||
|
||||
# Get vendor from previous middleware (if available)
|
||||
vendor = getattr(request.state, "vendor", None)
|
||||
@@ -59,13 +59,13 @@ class LanguageMiddleware(BaseHTTPMiddleware):
|
||||
accept_language = request.headers.get("accept-language")
|
||||
browser_language = parse_accept_language(accept_language)
|
||||
|
||||
# Resolve language based on context
|
||||
if context_value == "admin":
|
||||
# Resolve language based on frontend type
|
||||
if frontend_type == FrontendType.ADMIN:
|
||||
# Admin dashboard: English only (for now)
|
||||
# TODO: Implement admin language support later
|
||||
language = "en"
|
||||
|
||||
elif context_value == "vendor_dashboard":
|
||||
elif frontend_type == FrontendType.VENDOR:
|
||||
# Vendor dashboard
|
||||
user_preferred = self._get_user_language_from_token(request)
|
||||
vendor_dashboard = vendor.dashboard_language if vendor else None
|
||||
@@ -75,7 +75,7 @@ class LanguageMiddleware(BaseHTTPMiddleware):
|
||||
vendor_dashboard=vendor_dashboard,
|
||||
)
|
||||
|
||||
elif context_value == "shop":
|
||||
elif frontend_type == FrontendType.STOREFRONT:
|
||||
# Storefront
|
||||
customer_preferred = self._get_customer_language_from_token(request)
|
||||
vendor_storefront = vendor.storefront_language if vendor else None
|
||||
@@ -89,12 +89,12 @@ class LanguageMiddleware(BaseHTTPMiddleware):
|
||||
enabled_languages=enabled_languages,
|
||||
)
|
||||
|
||||
elif context_value == "api":
|
||||
# API requests: Use Accept-Language or cookie
|
||||
elif frontend_type == FrontendType.PLATFORM:
|
||||
# Platform marketing pages: Use cookie, browser, or default
|
||||
language = cookie_language or browser_language or DEFAULT_LANGUAGE
|
||||
|
||||
else:
|
||||
# Fallback: Use cookie, browser, or default
|
||||
# Fallback (API or unknown): Use Accept-Language or cookie
|
||||
language = cookie_language or browser_language or DEFAULT_LANGUAGE
|
||||
|
||||
# Validate language is supported
|
||||
@@ -109,13 +109,14 @@ class LanguageMiddleware(BaseHTTPMiddleware):
|
||||
"code": language,
|
||||
"cookie": cookie_language,
|
||||
"browser": browser_language,
|
||||
"context": context_value,
|
||||
"frontend_type": frontend_type.value if frontend_type else None,
|
||||
}
|
||||
|
||||
# Log language detection for debugging
|
||||
frontend_value = frontend_type.value if frontend_type else "unknown"
|
||||
logger.debug(
|
||||
f"Language detected: {language} "
|
||||
f"(context={context_value}, cookie={cookie_language}, browser={browser_language})"
|
||||
f"(frontend={frontend_value}, cookie={cookie_language}, browser={browser_language})"
|
||||
)
|
||||
|
||||
# Process request
|
||||
|
||||
@@ -22,6 +22,8 @@ from fastapi import Request
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.frontend_detector import FrontendDetector
|
||||
from app.modules.enums import FrontendType
|
||||
from app.modules.tenancy.models import Platform
|
||||
|
||||
# Note: We use pure ASGI middleware (not BaseHTTPMiddleware) to enable path rewriting
|
||||
@@ -61,7 +63,7 @@ class PlatformContextManager:
|
||||
host_without_port = host.split(":")[0] if ":" in host else host
|
||||
|
||||
# Skip platform detection for admin routes - admin is global
|
||||
if PlatformContextManager.is_admin_request(request):
|
||||
if FrontendDetector.is_admin(host, path):
|
||||
return None
|
||||
|
||||
# Method 1: Domain-based detection (production)
|
||||
@@ -208,17 +210,15 @@ class PlatformContextManager:
|
||||
|
||||
@staticmethod
|
||||
def is_admin_request(request: Request) -> bool:
|
||||
"""Check if request is for admin interface."""
|
||||
"""
|
||||
Check if request is for admin interface.
|
||||
|
||||
DEPRECATED: Use FrontendDetector.is_admin() instead.
|
||||
Kept for backwards compatibility.
|
||||
"""
|
||||
host = request.headers.get("host", "")
|
||||
path = request.url.path
|
||||
|
||||
if ":" in host:
|
||||
host = host.split(":")[0]
|
||||
|
||||
if host.startswith("admin."):
|
||||
return True
|
||||
|
||||
return path.startswith("/admin")
|
||||
return FrontendDetector.is_admin(host, path)
|
||||
|
||||
@staticmethod
|
||||
def is_static_file_request(request: Request) -> bool:
|
||||
@@ -299,7 +299,7 @@ class PlatformContextMiddleware:
|
||||
return
|
||||
|
||||
# Skip for admin requests
|
||||
if self._is_admin_request(path, host):
|
||||
if FrontendDetector.is_admin(host, path):
|
||||
scope["state"]["platform"] = None
|
||||
scope["state"]["platform_context"] = None
|
||||
scope["state"]["platform_clean_path"] = path
|
||||
@@ -427,11 +427,13 @@ class PlatformContextMiddleware:
|
||||
return "favicon.ico" in path_lower
|
||||
|
||||
def _is_admin_request(self, path: str, host: str) -> bool:
|
||||
"""Check if request is for admin interface."""
|
||||
host_without_port = host.split(":")[0] if ":" in host else host
|
||||
if host_without_port.startswith("admin."):
|
||||
return True
|
||||
return path.startswith("/admin")
|
||||
"""
|
||||
Check if request is for admin interface.
|
||||
|
||||
DEPRECATED: Use FrontendDetector.is_admin() instead.
|
||||
Kept for backwards compatibility.
|
||||
"""
|
||||
return FrontendDetector.is_admin(host, path)
|
||||
|
||||
|
||||
def get_current_platform(request: Request) -> Platform | None:
|
||||
|
||||
@@ -23,6 +23,7 @@ from starlette.middleware.base import BaseHTTPMiddleware
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.database import get_db
|
||||
from app.core.frontend_detector import FrontendDetector
|
||||
from app.modules.tenancy.models import Vendor
|
||||
from app.modules.tenancy.models import VendorDomain
|
||||
|
||||
@@ -194,22 +195,20 @@ class VendorContextManager:
|
||||
|
||||
@staticmethod
|
||||
def is_admin_request(request: Request) -> bool:
|
||||
"""Check if request is for admin interface."""
|
||||
"""
|
||||
Check if request is for admin interface.
|
||||
|
||||
DEPRECATED: Use FrontendDetector.is_admin() instead.
|
||||
Kept for backwards compatibility.
|
||||
"""
|
||||
host = request.headers.get("host", "")
|
||||
path = request.url.path
|
||||
|
||||
if ":" in host:
|
||||
host = host.split(":")[0]
|
||||
|
||||
if host.startswith("admin."):
|
||||
return True
|
||||
|
||||
return path.startswith("/admin")
|
||||
return FrontendDetector.is_admin(host, path)
|
||||
|
||||
@staticmethod
|
||||
def is_api_request(request: Request) -> bool:
|
||||
"""Check if request is for API endpoints."""
|
||||
return request.url.path.startswith("/api/")
|
||||
return FrontendDetector.is_api_request(request.url.path)
|
||||
|
||||
@staticmethod
|
||||
def is_shop_api_request(request: Request) -> bool:
|
||||
|
||||
Reference in New Issue
Block a user