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

@@ -14,7 +14,8 @@ from fastapi import Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from middleware.context import RequestContext, get_request_context
from app.modules.enums import FrontendType
from middleware.frontend_type import get_frontend_type
logger = logging.getLogger(__name__)
@@ -65,11 +66,11 @@ class ErrorPageRenderer:
show_debug: bool = False,
) -> HTMLResponse:
"""
Render appropriate error page based on request context.
Render appropriate error page based on request frontend type.
Template Selection Priority:
1. Context-specific error page: {context}/errors/{status_code}.html
2. Context-specific generic: {context}/errors/generic.html
1. Frontend-specific error page: {frontend}/errors/{status_code}.html
2. Frontend-specific generic: {frontend}/errors/generic.html
3. Shared fallback error page: shared/{status_code}-fallback.html
4. Shared fallback generic: shared/generic-fallback.html
@@ -84,13 +85,13 @@ class ErrorPageRenderer:
Returns:
HTMLResponse with rendered error page
"""
# Get request context
context_type = get_request_context(request)
# Get frontend type
frontend_type = get_frontend_type(request)
# Prepare template data
template_data = ErrorPageRenderer._prepare_template_data(
request=request,
context_type=context_type,
frontend_type=frontend_type,
status_code=status_code,
error_code=error_code,
message=message,
@@ -104,16 +105,16 @@ class ErrorPageRenderer:
# Try to find appropriate template
template_path = ErrorPageRenderer._find_template(
context_type=context_type,
frontend_type=frontend_type,
status_code=status_code,
)
logger.info(
f"Rendering error page: {template_path} for {status_code} in {context_type.value} context",
f"Rendering error page: {template_path} for {status_code} in {frontend_type.value} frontend",
extra={
"status_code": status_code,
"error_code": error_code,
"context": context_type.value,
"frontend": frontend_type.value,
"template": template_path,
},
)
@@ -141,38 +142,37 @@ class ErrorPageRenderer:
@staticmethod
def _find_template(
context_type: RequestContext,
frontend_type: FrontendType,
status_code: int,
) -> str:
"""
Find appropriate error template based on context and status code.
Find appropriate error template based on frontend type and status code.
Priority:
1. {context}/errors/{status_code}.html
2. {context}/errors/generic.html
1. {frontend}/errors/{status_code}.html
2. {frontend}/errors/generic.html
3. shared/{status_code}-fallback.html
4. shared/generic-fallback.html
"""
templates_dir = ErrorPageRenderer.get_templates_dir()
# Map context type to folder name
context_folders = {
RequestContext.ADMIN: "admin",
RequestContext.VENDOR_DASHBOARD: "vendor",
RequestContext.SHOP: "shop",
RequestContext.API: "fallback", # API shouldn't get here, but just in case
RequestContext.FALLBACK: "fallback",
# Map frontend type to folder name
frontend_folders = {
FrontendType.ADMIN: "admin",
FrontendType.VENDOR: "vendor",
FrontendType.STOREFRONT: "storefront",
FrontendType.PLATFORM: "fallback", # Platform uses fallback templates
}
context_folder = context_folders.get(context_type, "fallback")
frontend_folder = frontend_folders.get(frontend_type, "fallback")
# Try context-specific status code template
specific_template = f"{context_folder}/errors/{status_code}.html"
# Try frontend-specific status code template
specific_template = f"{frontend_folder}/errors/{status_code}.html"
if (templates_dir / specific_template).exists():
return specific_template
# Try context-specific generic template
generic_template = f"{context_folder}/errors/generic.html"
# Try frontend-specific generic template
generic_template = f"{frontend_folder}/errors/generic.html"
if (templates_dir / generic_template).exists():
return generic_template
@@ -187,7 +187,7 @@ class ErrorPageRenderer:
@staticmethod
def _prepare_template_data(
request: Request,
context_type: RequestContext,
frontend_type: FrontendType,
status_code: int,
error_code: str,
message: str,
@@ -212,8 +212,8 @@ class ErrorPageRenderer:
# Only show to admins (we can check user role if available)
display_debug = show_debug and ErrorPageRenderer._is_admin_user(request)
# Get context-specific data
context_data = ErrorPageRenderer._get_context_data(request, context_type)
# Get frontend-specific data
frontend_data = ErrorPageRenderer._get_frontend_data(request, frontend_type)
return {
"status_code": status_code,
@@ -222,20 +222,20 @@ class ErrorPageRenderer:
"message": user_message,
"details": details or {},
"show_debug": display_debug,
"context_type": context_type.value,
"frontend_type": frontend_type.value,
"path": request.url.path,
**context_data,
**frontend_data,
}
@staticmethod
def _get_context_data(
request: Request, context_type: RequestContext
def _get_frontend_data(
request: Request, frontend_type: FrontendType
) -> dict[str, Any]:
"""Get context-specific data for error templates."""
"""Get frontend-specific data for error templates."""
data = {}
# Add vendor information if available (for shop context)
if context_type == RequestContext.SHOP:
# Add vendor information if available (for storefront frontend)
if frontend_type == FrontendType.STOREFRONT:
vendor = getattr(request.state, "vendor", None)
if vendor:
# Pass minimal vendor info for templates
@@ -261,7 +261,7 @@ class ErrorPageRenderer:
"custom_css": getattr(theme, "custom_css", None),
}
# Calculate base_url for shop links
# Calculate base_url for storefront links
vendor_context = getattr(request.state, "vendor_context", None)
access_method = (
vendor_context.get("detection_method", "unknown")
@@ -289,9 +289,9 @@ class ErrorPageRenderer:
This is a placeholder - implement based on your auth system.
"""
# TODO: Implement actual admin check based on JWT/session
# For now, check if we're in admin context
context_type = get_request_context(request)
return context_type == RequestContext.ADMIN
# For now, check if we're in admin frontend
frontend_type = get_frontend_type(request)
return frontend_type == FrontendType.ADMIN
@staticmethod
def _render_basic_html_fallback(