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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user