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:
2026-02-03 16:15:19 +01:00
parent e77535e2cd
commit b769f5a047
17 changed files with 1393 additions and 915 deletions

View File

@@ -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: