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

@@ -147,27 +147,25 @@ app.include_router(shop_pages.router, prefix="/vendors/{vendor_code}/shop")
**Detection Logic**:
```python
host = request.headers.get("host", "")
path = request.state.clean_path # "/shop/products"
has_vendor = hasattr(request.state, 'vendor') and request.state.vendor
if path.startswith("/api/"):
context = RequestContext.API
elif path.startswith("/admin/"):
context = RequestContext.ADMIN
elif path.startswith("/vendor/"):
context = RequestContext.VENDOR_DASHBOARD
elif hasattr(request.state, 'vendor') and request.state.vendor:
context = RequestContext.SHOP # ← Our example
else:
context = RequestContext.FALLBACK
# FrontendDetector handles all detection logic centrally
frontend_type = FrontendDetector.detect(host, path, has_vendor)
# Returns: FrontendType.STOREFRONT # ← Our example
request.state.context_type = context
request.state.frontend_type = frontend_type
```
**Request State After**:
```python
request.state.context_type = RequestContext.SHOP
request.state.frontend_type = FrontendType.STOREFRONT
```
> **Note**: Detection logic is centralized in `app/core/frontend_detector.py`.
> See [Frontend Detection Architecture](frontend-detection.md) for details.
### 6. ThemeContextMiddleware
**What happens**:
@@ -363,7 +361,7 @@ sequenceDiagram
Context->>Router: Route request
Router->>Handler: Call API handler
Handler->>DB: Query products
DB-->>Handler: Product data
DB-->>Handler: Product data
Handler-->>Router: JSON response
Router-->>Client: {products: [...]}
```
@@ -390,7 +388,7 @@ sequenceDiagram
Context->>Theme: Pass request
Note over Theme: Skip theme<br/>(No vendor)
Theme->>Router: Route request
Router->>Handler: Call handler
Router->>Handler: Call handler
Handler->>Template: Render admin template
Template-->>Client: Admin HTML page
```
@@ -423,7 +421,7 @@ sequenceDiagram
Context->>Theme: Pass request
Theme->>DB: Query theme
DB-->>Theme: Theme config
Note over Theme: Set theme in request.state
Note over Theme: Set theme in request.state
Theme->>Router: Route request
Router->>Handler: Call handler
Handler->>DB: Query products for vendor
@@ -450,12 +448,12 @@ After VendorContextMiddleware:
{
vendor: <Vendor: Wizamart>,
vendor_id: 1,
clean_path: "/shop/products",
clean_path: "/shop/products",
frontend_type: FrontendType.STOREFRONT
}
After ThemeContextMiddleware:
{
{
vendor: <Vendor: Wizamart>,
vendor_id: 1,
clean_path: "/shop/products",
@@ -463,7 +461,7 @@ After ThemeContextMiddleware:
theme: {
primary_color: "#3B82F6",
secondary_color: "#10B981",
logo_url: "/static/vendors/wizamart/logo.png",
logo_url: "/static/vendors/wizamart/logo.png",
custom_css: "..."
}
}
@@ -481,7 +479,7 @@ Typical request timings:
| - ThemeContextMiddleware | 2ms | 1% |
| Database Queries | 15ms | 10% |
| Business Logic | 50ms | 35% |
| Template Rendering | 75ms | 52% |
| Template Rendering | 75ms | 52% |
| **Total** | **145ms** | **100%** |
## Error Handling in Flow
@@ -550,7 +548,7 @@ async def debug_state(request: Request):
"has_theme": bool(getattr(request.state, 'theme', None))
}
```
### Check Middleware Order
In `main.py`, middleware registration order is critical:
@@ -562,7 +560,8 @@ In `main.py`, middleware registration order is critical:
app.add_middleware(LanguageMiddleware) # Runs fifth
app.add_middleware(FrontendTypeMiddleware) # Runs fourth
app.add_middleware(VendorContextMiddleware) # Runs second
app.add_middleware(ThemeContextMiddleware) # Runs fifth
app.add_middleware(ContextDetectionMiddleware) # Runs fourth
```
app.add_middleware(LanguageMiddleware) # Runs fifth
app.add_middleware(FrontendTypeMiddleware) # Runs fourth
app.add_middleware(VendorContextMiddleware) # Runs second
```