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

@@ -120,35 +120,35 @@ Injects: request.state.vendor = <Vendor object>
**Note on Path-Based Routing:** Previous implementations used a `PathRewriteMiddleware` to rewrite paths at runtime. This has been replaced with **double router mounting** in `main.py`, where shop routes are registered twice with different prefixes (`/shop` and `/vendors/{vendor_code}/shop`). This approach is simpler and uses FastAPI's native routing capabilities.
### 3. Context Detection Middleware
### 3. Frontend Type Detection Middleware
**Purpose**: Determine the type/context of the request
**Purpose**: Determine which frontend the request targets
**What it does**:
- Analyzes the request path (using clean_path)
- Determines which interface is being accessed:
- `API` - `/api/*` paths
- `ADMIN` - `/admin/*` paths or `admin.*` subdomain
- `VENDOR_DASHBOARD` - `/vendor/*` paths (management area)
- `SHOP` - Storefront pages (has vendor + not admin/vendor/API)
- `FALLBACK` - Unknown context
- Injects `request.state.context_type`
- Uses centralized `FrontendDetector` class for all detection logic
- Determines which frontend is being accessed:
- `ADMIN` - `/admin/*`, `/api/v1/admin/*` paths or `admin.*` subdomain
- `VENDOR` - `/vendor/*`, `/api/v1/vendor/*` paths (management area)
- `STOREFRONT` - Customer shop pages (`/storefront/*`, `/vendors/*`, vendor subdomains)
- `PLATFORM` - Marketing pages (`/`, `/pricing`, `/about`)
- Injects `request.state.frontend_type` (FrontendType enum)
**Detection Rules**:
**Detection Priority** (handled by `FrontendDetector`):
```python
if path.startswith("/api/"):
context = API
elif path.startswith("/admin/") or host.startswith("admin."):
context = ADMIN
elif path.startswith("/vendor/"):
context = VENDOR_DASHBOARD
elif request.state.vendor exists:
context = SHOP
else:
context = FALLBACK
1. Admin subdomain (admin.oms.lu) ADMIN
2. Path-based detection:
- /admin/* or /api/v1/admin/* ADMIN
- /vendor/* or /api/v1/vendor/* VENDOR
- /storefront/*, /shop/*, /vendors/* STOREFRONT
- /api/v1/platform/* PLATFORM
3. Vendor subdomain (wizamart.oms.lu) STOREFRONT
4. Vendor context set by middleware STOREFRONT
5. Default PLATFORM
```
**Why it's useful**: Error handlers and templates adapt based on context
**Why it's useful**: Error handlers, templates, and language detection adapt based on frontend type
**See**: [Frontend Detection Architecture](frontend-detection.md) for complete details
### 4. Theme Context Middleware
@@ -221,15 +221,18 @@ Each middleware file contains one primary class or a tightly related set of clas
class LoggingMiddleware(BaseHTTPMiddleware):
"""Request/response logging middleware"""
# middleware/context.py
class ContextManager: # Business logic
class ContextMiddleware: # ASGI wrapper
class RequestContext(Enum): # Related enum
# middleware/frontend_type.py
class FrontendTypeMiddleware: # ASGI wrapper for frontend detection
# Uses FrontendDetector from app/core/frontend_detector.py
# middleware/auth.py
class AuthManager: # Authentication logic
```
> **Note**: The old `middleware/context.py` with `ContextMiddleware` and `RequestContext` is deprecated.
> Use `FrontendTypeMiddleware` and `FrontendType` enum instead.
> See [Frontend Detection Architecture](frontend-detection.md) for migration guide.
#### One Test File Per Component
Follow the Single Responsibility Principle - each test file tests exactly one component:
@@ -368,8 +371,8 @@ async def get_products(request: Request):
}
</style>
{# Access context #}
{% if request.state.context_type.value == "admin" %}
{# Access frontend type #}
{% if request.state.frontend_type.value == "admin" %}
<div class="admin-badge">Admin Mode</div>
{% endif %}
```
@@ -394,11 +397,11 @@ async def get_products(request: Request):
↓ Sets: request.state.vendor_id = 1
↓ Sets: request.state.clean_path = "/shop/products"
3. ContextDetectionMiddleware
Analyzes path: "/shop/products"
↓ Has vendor: Yes
Not admin/api/vendor dashboard
↓ Sets: request.state.context_type = RequestContext.SHOP
3. FrontendTypeMiddleware
Uses FrontendDetector with path: "/shop/products"
↓ Has vendor context: Yes
Detects storefront frontend
↓ Sets: request.state.frontend_type = FrontendType.STOREFRONT
4. ThemeContextMiddleware
↓ Loads theme for vendor_id = 1
@@ -430,10 +433,10 @@ Each middleware component handles errors gracefully:
- If database error: Logs error, allows request to continue
- Fallback: Request proceeds without vendor context
### ContextDetectionMiddleware
### FrontendTypeMiddleware
- If clean_path missing: Uses original path
- If vendor missing: Defaults to FALLBACK context
- Always sets a context_type (never None)
- If vendor missing: Defaults to PLATFORM frontend type
- Always sets a frontend_type (never None)
### ThemeContextMiddleware
- If vendor missing: Skips theme loading
@@ -479,8 +482,10 @@ Middleware is registered in `main.py`:
# Add in REVERSE order (LIFO execution)
app.add_middleware(LoggingMiddleware)
app.add_middleware(ThemeContextMiddleware)
app.add_middleware(ContextDetectionMiddleware)
app.add_middleware(LanguageMiddleware)
app.add_middleware(FrontendTypeMiddleware)
app.add_middleware(VendorContextMiddleware)
app.add_middleware(PlatformContextMiddleware)
```
**Note**: FastAPI's `add_middleware` executes in **reverse order** (Last In, First Out)