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:
278
docs/architecture/frontend-detection.md
Normal file
278
docs/architecture/frontend-detection.md
Normal file
@@ -0,0 +1,278 @@
|
||||
# Frontend Detection Architecture
|
||||
|
||||
This document describes the centralized frontend detection system that identifies which frontend (ADMIN, VENDOR, STOREFRONT, or PLATFORM) a request targets.
|
||||
|
||||
## Overview
|
||||
|
||||
The application serves multiple frontends from a single codebase:
|
||||
|
||||
| Frontend | Description | Example URLs |
|
||||
|----------|-------------|--------------|
|
||||
| **ADMIN** | Platform administration | `/admin/*`, `/api/v1/admin/*`, `admin.oms.lu/*` |
|
||||
| **VENDOR** | Vendor dashboard | `/vendor/*`, `/api/v1/vendor/*` |
|
||||
| **STOREFRONT** | Customer-facing shop | `/storefront/*`, `/vendors/*`, `wizamart.oms.lu/*` |
|
||||
| **PLATFORM** | Marketing pages | `/`, `/pricing`, `/about` |
|
||||
|
||||
The `FrontendDetector` class provides centralized, consistent detection of which frontend a request targets.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Components
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Request Processing │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 1. PlatformContextMiddleware → Sets request.state.platform │
|
||||
│ │
|
||||
│ 2. VendorContextMiddleware → Sets request.state.vendor │
|
||||
│ │
|
||||
│ 3. FrontendTypeMiddleware → Sets request.state.frontend_type│
|
||||
│ │ │
|
||||
│ └──→ Uses FrontendDetector.detect() │
|
||||
│ │
|
||||
│ 4. LanguageMiddleware → Uses frontend_type for language │
|
||||
│ │
|
||||
│ 5. ThemeContextMiddleware → Uses frontend_type for theming │
|
||||
│ │
|
||||
│ 6. FastAPI Router → Handles request │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `app/core/frontend_detector.py` | Centralized detection logic |
|
||||
| `middleware/frontend_type.py` | Middleware that sets `request.state.frontend_type` |
|
||||
| `app/modules/enums.py` | Defines `FrontendType` enum |
|
||||
|
||||
## FrontendType Enum
|
||||
|
||||
```python
|
||||
class FrontendType(str, Enum):
|
||||
PLATFORM = "platform" # Marketing pages (/, /pricing, /about)
|
||||
ADMIN = "admin" # Admin panel (/admin/*)
|
||||
VENDOR = "vendor" # Vendor dashboard (/vendor/*)
|
||||
STOREFRONT = "storefront" # Customer shop (/storefront/*, /vendors/*)
|
||||
```
|
||||
|
||||
## Detection Priority
|
||||
|
||||
The `FrontendDetector` uses the following priority order:
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
### Path Patterns
|
||||
|
||||
```python
|
||||
# Admin paths
|
||||
ADMIN_PATH_PREFIXES = ("/admin", "/api/v1/admin")
|
||||
|
||||
# Vendor dashboard paths
|
||||
VENDOR_PATH_PREFIXES = ("/vendor/", "/api/v1/vendor")
|
||||
|
||||
# Storefront paths
|
||||
STOREFRONT_PATH_PREFIXES = (
|
||||
"/storefront",
|
||||
"/api/v1/storefront",
|
||||
"/shop", # Legacy support
|
||||
"/api/v1/shop", # Legacy support
|
||||
"/vendors/", # Path-based vendor access
|
||||
)
|
||||
|
||||
# Platform paths
|
||||
PLATFORM_PATH_PREFIXES = ("/api/v1/platform",)
|
||||
```
|
||||
|
||||
### Reserved Subdomains
|
||||
|
||||
These subdomains are NOT treated as vendor storefronts:
|
||||
|
||||
```python
|
||||
RESERVED_SUBDOMAINS = {"www", "admin", "api", "vendor", "portal"}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### In Middleware/Routes
|
||||
|
||||
```python
|
||||
from middleware.frontend_type import get_frontend_type
|
||||
from app.modules.enums import FrontendType
|
||||
|
||||
@router.get("/some-route")
|
||||
async def some_route(request: Request):
|
||||
frontend_type = get_frontend_type(request)
|
||||
|
||||
if frontend_type == FrontendType.ADMIN:
|
||||
# Admin-specific logic
|
||||
pass
|
||||
elif frontend_type == FrontendType.STOREFRONT:
|
||||
# Storefront-specific logic
|
||||
pass
|
||||
```
|
||||
|
||||
### Direct Detection (without request)
|
||||
|
||||
```python
|
||||
from app.core.frontend_detector import FrontendDetector
|
||||
from app.modules.enums import FrontendType
|
||||
|
||||
# Full detection
|
||||
frontend_type = FrontendDetector.detect(
|
||||
host="wizamart.oms.lu",
|
||||
path="/products",
|
||||
has_vendor_context=True
|
||||
)
|
||||
# Returns: FrontendType.STOREFRONT
|
||||
|
||||
# Convenience methods
|
||||
if FrontendDetector.is_admin(host, path):
|
||||
# Admin logic
|
||||
pass
|
||||
|
||||
if FrontendDetector.is_storefront(host, path, has_vendor_context=True):
|
||||
# Storefront logic
|
||||
pass
|
||||
```
|
||||
|
||||
## Detection Scenarios
|
||||
|
||||
### Development Mode (localhost)
|
||||
|
||||
| Request | Host | Path | Frontend |
|
||||
|---------|------|------|----------|
|
||||
| Admin page | localhost | /admin/vendors | ADMIN |
|
||||
| Admin API | localhost | /api/v1/admin/users | ADMIN |
|
||||
| Vendor dashboard | localhost | /vendor/settings | VENDOR |
|
||||
| Vendor API | localhost | /api/v1/vendor/products | VENDOR |
|
||||
| Storefront | localhost | /storefront/products | STOREFRONT |
|
||||
| Storefront (path-based) | localhost | /vendors/wizamart/products | STOREFRONT |
|
||||
| Marketing | localhost | /pricing | PLATFORM |
|
||||
|
||||
### Production Mode (domains)
|
||||
|
||||
| Request | Host | Path | Frontend |
|
||||
|---------|------|------|----------|
|
||||
| Admin subdomain | admin.oms.lu | /dashboard | ADMIN |
|
||||
| Vendor subdomain | wizamart.oms.lu | /products | STOREFRONT |
|
||||
| Custom domain | mybakery.lu | /products | STOREFRONT |
|
||||
| Platform root | oms.lu | /pricing | PLATFORM |
|
||||
|
||||
## Migration from RequestContext
|
||||
|
||||
The previous `RequestContext` enum is deprecated. Here's the mapping:
|
||||
|
||||
| Old (RequestContext) | New (FrontendType) |
|
||||
|---------------------|-------------------|
|
||||
| `API` | Use `FrontendDetector.is_api_request()` + FrontendType |
|
||||
| `ADMIN` | `FrontendType.ADMIN` |
|
||||
| `VENDOR_DASHBOARD` | `FrontendType.VENDOR` |
|
||||
| `SHOP` | `FrontendType.STOREFRONT` |
|
||||
| `FALLBACK` | `FrontendType.PLATFORM` |
|
||||
|
||||
### Code Migration
|
||||
|
||||
**Before (deprecated):**
|
||||
```python
|
||||
from middleware.context import RequestContext, get_request_context
|
||||
|
||||
context = get_request_context(request)
|
||||
if context == RequestContext.SHOP:
|
||||
# Storefront logic
|
||||
pass
|
||||
```
|
||||
|
||||
**After:**
|
||||
```python
|
||||
from middleware.frontend_type import get_frontend_type
|
||||
from app.modules.enums import FrontendType
|
||||
|
||||
frontend_type = get_frontend_type(request)
|
||||
if frontend_type == FrontendType.STOREFRONT:
|
||||
# Storefront logic
|
||||
pass
|
||||
```
|
||||
|
||||
## Request State
|
||||
|
||||
After `FrontendTypeMiddleware` runs, the following is available:
|
||||
|
||||
```python
|
||||
request.state.frontend_type # FrontendType enum value
|
||||
```
|
||||
|
||||
This is used by:
|
||||
- `LanguageMiddleware` - to determine language resolution strategy
|
||||
- `ErrorRenderer` - to select appropriate error templates
|
||||
- `ExceptionHandler` - to redirect to correct login page
|
||||
- Route handlers - for frontend-specific logic
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Tests
|
||||
|
||||
Tests are located in:
|
||||
- `tests/unit/core/test_frontend_detector.py` - FrontendDetector tests
|
||||
- `tests/unit/middleware/test_frontend_type.py` - Middleware tests
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Run all frontend detection tests
|
||||
pytest tests/unit/core/test_frontend_detector.py tests/unit/middleware/test_frontend_type.py -v
|
||||
|
||||
# Run with coverage
|
||||
pytest tests/unit/core/test_frontend_detector.py tests/unit/middleware/test_frontend_type.py --cov=app.core.frontend_detector --cov=middleware.frontend_type
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### DO
|
||||
|
||||
1. **Use `get_frontend_type(request)`** in route handlers
|
||||
2. **Use `FrontendDetector.detect()`** when you have host/path but no request
|
||||
3. **Use convenience methods** like `is_admin()`, `is_storefront()` for boolean checks
|
||||
4. **Import from the correct location:**
|
||||
```python
|
||||
from app.modules.enums import FrontendType
|
||||
from middleware.frontend_type import get_frontend_type
|
||||
from app.core.frontend_detector import FrontendDetector
|
||||
```
|
||||
|
||||
### DON'T
|
||||
|
||||
1. **Don't use `RequestContext`** - it's deprecated
|
||||
2. **Don't duplicate path detection logic** - use FrontendDetector
|
||||
3. **Don't hardcode path patterns** in middleware - they're centralized in FrontendDetector
|
||||
4. **Don't check `request.state.context_type`** - use `request.state.frontend_type`
|
||||
|
||||
## Architecture Rules
|
||||
|
||||
These rules are enforced by `scripts/validate_architecture.py`:
|
||||
|
||||
| Rule | Description |
|
||||
|------|-------------|
|
||||
| MID-001 | Use FrontendDetector for frontend detection |
|
||||
| MID-002 | Don't hardcode path patterns in middleware |
|
||||
| MID-003 | Use FrontendType enum, not RequestContext |
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Middleware Stack](middleware.md) - Overall middleware architecture
|
||||
- [Request Flow](request-flow.md) - How requests are processed
|
||||
- [URL Routing](url-routing/overview.md) - URL structure and routing patterns
|
||||
- [Multi-Tenant Architecture](multi-tenant.md) - Tenant detection and isolation
|
||||
Reference in New Issue
Block a user