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>
584 lines
18 KiB
Markdown
584 lines
18 KiB
Markdown
# Middleware Stack
|
|
|
|
The middleware stack is the backbone of the multi-tenant system, handling tenant detection, context injection, and theme loading for all requests.
|
|
|
|
## Overview
|
|
|
|
The application uses a custom middleware stack that processes **every request** regardless of whether it's:
|
|
- REST API calls (`/api/*`)
|
|
- Admin interface pages (`/admin/*`)
|
|
- Vendor dashboard pages (`/vendor/*`)
|
|
- Shop pages (`/shop/*` or custom domains)
|
|
|
|
This middleware layer is **system-wide** and enables the multi-tenant architecture to function seamlessly.
|
|
|
|
## Middleware Components
|
|
|
|
### 1. Platform Context Middleware
|
|
|
|
**Purpose**: Detect which platform (OMS, Loyalty, Main) the request is for
|
|
|
|
**What it does**:
|
|
- Detects platform from:
|
|
- Custom domain (e.g., `oms.lu`, `loyalty.lu`)
|
|
- Path prefix in development (e.g., `/platforms/oms/`, `/platforms/loyalty/`)
|
|
- Default to `main` platform for localhost without prefix
|
|
- Rewrites path for platform-prefixed requests (strips `/platforms/{code}/`)
|
|
- Queries database to find platform by domain or code
|
|
- Injects platform object into `request.state.platform`
|
|
|
|
**URL Detection Logic**:
|
|
```
|
|
Request arrives
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────┐
|
|
│ Production domain? (oms.lu, etc.) │
|
|
└─────────────────────────────────────┘
|
|
│ YES → Use that platform
|
|
│
|
|
▼ NO (localhost)
|
|
┌─────────────────────────────────────┐
|
|
│ Path starts with /platforms/{code}? │
|
|
└─────────────────────────────────────┘
|
|
│ YES → Strip prefix, use platform
|
|
│ /platforms/oms/pricing → /pricing
|
|
│
|
|
▼ NO
|
|
┌─────────────────────────────────────┐
|
|
│ Use 'main' platform (DEFAULT) │
|
|
│ Path unchanged │
|
|
└─────────────────────────────────────┘
|
|
```
|
|
|
|
**Example**:
|
|
```
|
|
Request: https://localhost:9999/platforms/oms/pricing
|
|
↓
|
|
Middleware detects: platform_code = "oms"
|
|
↓
|
|
Rewrites path: /platforms/oms/pricing → /pricing
|
|
↓
|
|
Queries database: SELECT * FROM platforms WHERE code = 'oms'
|
|
↓
|
|
Injects: request.state.platform = <Platform object>
|
|
```
|
|
|
|
**Why it's critical**: Without this, the system wouldn't know which platform's content to serve
|
|
|
|
**Configuration**: Runs BEFORE VendorContextMiddleware (sets platform context first)
|
|
|
|
### 2. Logging Middleware
|
|
|
|
**Purpose**: Request/response logging and performance monitoring
|
|
|
|
**What it does**:
|
|
- Logs every incoming request with method, path, and client IP
|
|
- Measures request processing time
|
|
- Logs response status codes
|
|
- Adds `X-Process-Time` header with processing duration
|
|
- Logs errors with stack traces
|
|
|
|
**Example Log Output**:
|
|
```
|
|
INFO Request: GET /admin/dashboard from 192.168.1.100
|
|
INFO Response: 200 for GET /admin/dashboard (0.143s)
|
|
```
|
|
|
|
**Configuration**: Runs first to capture full request timing
|
|
|
|
### 2. Vendor Context Middleware
|
|
|
|
**Purpose**: Detect which vendor's shop the request is for (multi-tenant core)
|
|
|
|
**What it does**:
|
|
- Detects vendor from:
|
|
- Custom domain (e.g., `customdomain.com`)
|
|
- Subdomain (e.g., `vendor1.platform.com`)
|
|
- Path prefix (e.g., `/vendor/vendor1/` or `/vendors/vendor1/`)
|
|
- Queries database to find vendor by domain or code
|
|
- Injects vendor object into `request.state.vendor`
|
|
- Extracts "clean path" (path without vendor prefix)
|
|
- Sets `request.state.clean_path` for routing
|
|
|
|
**Example**:
|
|
```
|
|
Request: https://wizamart.platform.com/shop/products
|
|
↓
|
|
Middleware detects: vendor_code = "wizamart"
|
|
↓
|
|
Queries database: SELECT * FROM vendors WHERE code = 'wizamart'
|
|
↓
|
|
Injects: request.state.vendor = <Vendor object>
|
|
request.state.vendor_id = 1
|
|
request.state.clean_path = "/shop/products"
|
|
```
|
|
|
|
**Why it's critical**: Without this, the system wouldn't know which vendor's data to show
|
|
|
|
**See**: [Multi-Tenant System](multi-tenant.md) for routing modes
|
|
|
|
**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. Frontend Type Detection Middleware
|
|
|
|
**Purpose**: Determine which frontend the request targets
|
|
|
|
**What it does**:
|
|
- 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 Priority** (handled by `FrontendDetector`):
|
|
```python
|
|
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, templates, and language detection adapt based on frontend type
|
|
|
|
**See**: [Frontend Detection Architecture](frontend-detection.md) for complete details
|
|
|
|
### 4. Theme Context Middleware
|
|
|
|
**Purpose**: Load vendor-specific theme settings
|
|
|
|
**What it does**:
|
|
- Checks if request has a vendor (from VendorContextMiddleware)
|
|
- Queries database for vendor's theme settings
|
|
- Injects theme configuration into `request.state.theme`
|
|
- Provides default theme if vendor has no custom theme
|
|
|
|
**Theme Data Structure**:
|
|
```python
|
|
{
|
|
"primary_color": "#3B82F6",
|
|
"secondary_color": "#10B981",
|
|
"logo_url": "/static/vendors/wizamart/logo.png",
|
|
"favicon_url": "/static/vendors/wizamart/favicon.ico",
|
|
"custom_css": "/* vendor-specific styles */"
|
|
}
|
|
```
|
|
|
|
**Why it's needed**: Each vendor shop can have custom branding
|
|
|
|
## Naming Conventions
|
|
|
|
### Middleware File Organization
|
|
|
|
All middleware components follow a consistent naming pattern for maintainability and clarity.
|
|
|
|
#### File Naming: Simple Nouns Without Redundant Suffixes
|
|
|
|
**Pattern**: `{purpose}.py` (no "_middleware" suffix)
|
|
|
|
```
|
|
✅ Good:
|
|
middleware/logging.py
|
|
middleware/context.py
|
|
middleware/auth.py
|
|
|
|
❌ Avoid:
|
|
middleware/logging_middleware.py
|
|
middleware/context_middleware.py
|
|
middleware/auth_middleware.py
|
|
```
|
|
|
|
**Rationale**:
|
|
- Keeps names concise and consistent
|
|
- Follows Django, Flask, and FastAPI conventions
|
|
- Makes imports cleaner: `from middleware.logging import LoggingMiddleware`
|
|
- Reduces redundancy (the `middleware/` directory already indicates the purpose)
|
|
|
|
#### Test File Naming: Mirror the Source File
|
|
|
|
Test files directly mirror the middleware filename with a `test_` prefix:
|
|
|
|
```
|
|
middleware/logging.py → tests/unit/middleware/test_logging.py
|
|
middleware/context.py → tests/unit/middleware/test_context.py
|
|
middleware/auth.py → tests/unit/middleware/test_auth.py
|
|
middleware/vendor_context.py → tests/unit/middleware/test_vendor_context.py
|
|
```
|
|
|
|
#### One Component Per File
|
|
|
|
Each middleware file contains one primary class or a tightly related set of classes:
|
|
|
|
```python
|
|
# middleware/logging.py
|
|
class LoggingMiddleware(BaseHTTPMiddleware):
|
|
"""Request/response logging middleware"""
|
|
|
|
# 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:
|
|
|
|
```
|
|
✅ Good:
|
|
tests/unit/middleware/test_logging.py # Tests only LoggingMiddleware
|
|
tests/unit/middleware/test_context.py # Tests only ContextManager/Middleware
|
|
tests/unit/middleware/test_decorators.py # Tests only rate_limit decorator
|
|
|
|
❌ Avoid:
|
|
tests/unit/middleware/test_all_middleware.py # Tests multiple components
|
|
tests/unit/middleware/test_combined.py # Violates SRP
|
|
```
|
|
|
|
**Benefits**:
|
|
- Easy to locate tests for specific components
|
|
- Clear test organization and maintainability
|
|
- Follows unit testing best practices
|
|
- Simplifies test debugging and updates
|
|
|
|
#### Import Convention
|
|
|
|
When importing middleware components, use explicit imports:
|
|
|
|
```python
|
|
# ✅ Preferred - Explicit and clear
|
|
from middleware.logging import LoggingMiddleware
|
|
from middleware.context import ContextManager, RequestContext
|
|
from middleware.auth import AuthManager
|
|
|
|
# ❌ Avoid - Less clear
|
|
from middleware import logging_middleware
|
|
from middleware import context_middleware
|
|
```
|
|
|
|
**See**: [Complete Naming Conventions Guide](../development/naming-conventions.md) for project-wide standards.
|
|
|
|
## Middleware Execution Order
|
|
|
|
### The Stack (First to Last)
|
|
|
|
```mermaid
|
|
graph TD
|
|
A[Client Request] --> B[1. LoggingMiddleware]
|
|
B --> C[2. PlatformContextMiddleware]
|
|
C --> D[3. VendorContextMiddleware]
|
|
D --> E[4. ContextDetectionMiddleware]
|
|
E --> F[5. ThemeContextMiddleware]
|
|
F --> G[6. FastAPI Router]
|
|
G --> H[Route Handler]
|
|
H --> I[Response]
|
|
I --> J[Client]
|
|
```
|
|
|
|
### Why This Order Matters
|
|
|
|
**Critical Dependencies**:
|
|
|
|
1. **LoggingMiddleware first**
|
|
- Needs to wrap everything to measure total time
|
|
- Must log errors from all other middleware
|
|
|
|
2. **PlatformContextMiddleware second**
|
|
- Must run before VendorContextMiddleware (sets platform context)
|
|
- Rewrites path for `/platforms/{code}/` prefixed requests
|
|
- Sets `request.state.platform` for downstream middleware
|
|
|
|
3. **VendorContextMiddleware third**
|
|
- Uses rewritten path from PlatformContextMiddleware
|
|
- Must run before ContextDetectionMiddleware (provides vendor and clean_path)
|
|
- Must run before ThemeContextMiddleware (provides vendor_id)
|
|
|
|
4. **ContextDetectionMiddleware fourth**
|
|
- Uses clean_path from VendorContextMiddleware
|
|
- Provides context_type for ThemeContextMiddleware
|
|
|
|
5. **ThemeContextMiddleware last**
|
|
- Depends on vendor from VendorContextMiddleware
|
|
- Depends on context_type from ContextDetectionMiddleware
|
|
|
|
**Breaking this order will break the application!**
|
|
|
|
**Note:** Path-based routing (e.g., `/vendors/{code}/shop/*`) is handled by double router mounting in `main.py`, not by middleware. Platform path-based routing (e.g., `/platforms/oms/`) IS handled by PlatformContextMiddleware which rewrites the path.
|
|
|
|
## Request State Variables
|
|
|
|
Middleware components inject these variables into `request.state`:
|
|
|
|
| Variable | Set By | Type | Used By | Description |
|
|
|----------|--------|------|---------|-------------|
|
|
| `platform` | PlatformContextMiddleware | Platform | Routes, Content | Current platform object (main, oms, loyalty) |
|
|
| `platform_context` | PlatformContextMiddleware | dict | Routes | Platform detection details (method, paths) |
|
|
| `vendor` | VendorContextMiddleware | Vendor | Theme, Templates | Current vendor object |
|
|
| `vendor_id` | VendorContextMiddleware | int | Queries, Theme | Current vendor ID |
|
|
| `clean_path` | VendorContextMiddleware | str | Context | Path without vendor prefix (for context detection) |
|
|
| `context_type` | ContextDetectionMiddleware | RequestContext | Theme, Error handlers | Request context enum |
|
|
| `theme` | ThemeContextMiddleware | dict | Templates | Vendor theme config |
|
|
|
|
### Using in Route Handlers
|
|
|
|
```python
|
|
from fastapi import Request
|
|
|
|
@app.get("/shop/products")
|
|
async def get_products(request: Request):
|
|
# Access vendor
|
|
vendor = request.state.vendor
|
|
vendor_id = request.state.vendor_id
|
|
|
|
# Access context
|
|
context = request.state.context_type
|
|
|
|
# Access theme
|
|
theme = request.state.theme
|
|
|
|
# Use in queries
|
|
products = db.query(Product).filter(
|
|
Product.vendor_id == vendor_id
|
|
).all()
|
|
|
|
return {"vendor": vendor.name, "products": products}
|
|
```
|
|
|
|
### Using in Templates
|
|
|
|
```jinja2
|
|
{# Access vendor #}
|
|
<h1>{{ request.state.vendor.name }}</h1>
|
|
|
|
{# Access theme #}
|
|
<style>
|
|
:root {
|
|
--primary-color: {{ request.state.theme.primary_color }};
|
|
--secondary-color: {{ request.state.theme.secondary_color }};
|
|
}
|
|
</style>
|
|
|
|
{# Access frontend type #}
|
|
{% if request.state.frontend_type.value == "admin" %}
|
|
<div class="admin-badge">Admin Mode</div>
|
|
{% endif %}
|
|
```
|
|
|
|
## Request Flow Example
|
|
|
|
### Example: Shop Product Page Request
|
|
|
|
**URL**: `https://wizamart.myplatform.com/shop/products`
|
|
|
|
**Middleware Processing**:
|
|
|
|
```
|
|
1. LoggingMiddleware
|
|
↓ Starts timer
|
|
↓ Logs: "Request: GET /shop/products from 192.168.1.100"
|
|
|
|
2. VendorContextMiddleware
|
|
↓ Detects subdomain: "wizamart"
|
|
↓ Queries DB: vendor = get_vendor_by_code("wizamart")
|
|
↓ Sets: request.state.vendor = <Vendor: Wizamart>
|
|
↓ Sets: request.state.vendor_id = 1
|
|
↓ Sets: request.state.clean_path = "/shop/products"
|
|
|
|
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
|
|
↓ Sets: request.state.theme = {...theme config...}
|
|
|
|
5. FastAPI Router
|
|
↓ Matches route: @app.get("/shop/products")
|
|
↓ Calls handler function
|
|
|
|
6. Route Handler
|
|
↓ Accesses: request.state.vendor_id
|
|
↓ Queries: products WHERE vendor_id = 1
|
|
↓ Renders template with vendor data
|
|
|
|
8. Response
|
|
↓ Returns HTML with vendor theme
|
|
|
|
9. LoggingMiddleware (response phase)
|
|
↓ Logs: "Response: 200 for GET /shop/products (0.143s)"
|
|
↓ Adds header: X-Process-Time: 0.143
|
|
```
|
|
|
|
## Error Handling in Middleware
|
|
|
|
Each middleware component handles errors gracefully:
|
|
|
|
### VendorContextMiddleware
|
|
- If vendor not found: Sets `request.state.vendor = None`
|
|
- If database error: Logs error, allows request to continue
|
|
- Fallback: Request proceeds without vendor context
|
|
|
|
### FrontendTypeMiddleware
|
|
- If clean_path missing: Uses original path
|
|
- If vendor missing: Defaults to PLATFORM frontend type
|
|
- Always sets a frontend_type (never None)
|
|
|
|
### ThemeContextMiddleware
|
|
- If vendor missing: Skips theme loading
|
|
- If theme query fails: Uses default theme
|
|
- If no theme exists: Returns empty theme dict
|
|
|
|
**Design Philosophy**: Middleware should never crash the application. Degrade gracefully.
|
|
|
|
## Performance Considerations
|
|
|
|
### Database Queries
|
|
|
|
**Per Request**:
|
|
- 1 query in VendorContextMiddleware (vendor lookup) - cached by DB
|
|
- 1 query in ThemeContextMiddleware (theme lookup) - cached by DB
|
|
|
|
**Total**: ~2 DB queries per request
|
|
|
|
**Optimization Opportunities**:
|
|
- Implement Redis caching for vendor lookups
|
|
- Cache theme data in memory
|
|
- Use connection pooling (already enabled)
|
|
|
|
### Memory Usage
|
|
|
|
Minimal per-request overhead:
|
|
- Small objects stored in `request.state`
|
|
- No global state maintained
|
|
- Garbage collected after response
|
|
|
|
### Latency
|
|
|
|
Typical overhead: **< 5ms** per request
|
|
- Vendor lookup: ~2ms
|
|
- Theme lookup: ~2ms
|
|
- Context detection: <1ms
|
|
|
|
## Configuration
|
|
|
|
Middleware is registered in `main.py`:
|
|
|
|
```python
|
|
# Add in REVERSE order (LIFO execution)
|
|
app.add_middleware(LoggingMiddleware)
|
|
app.add_middleware(ThemeContextMiddleware)
|
|
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)
|
|
|
|
## Testing Middleware
|
|
|
|
### Unit Testing
|
|
|
|
Test each middleware component in isolation:
|
|
|
|
```python
|
|
from middleware.vendor_context import VendorContextManager
|
|
|
|
def test_vendor_detection_subdomain():
|
|
# Mock request
|
|
request = create_mock_request(host="wizamart.platform.com")
|
|
|
|
# Test detection
|
|
manager = VendorContextManager()
|
|
vendor = manager.detect_vendor_from_subdomain(request)
|
|
|
|
assert vendor.code == "wizamart"
|
|
```
|
|
|
|
### Integration Testing
|
|
|
|
Test the full middleware stack:
|
|
|
|
```python
|
|
def test_shop_request_flow(client):
|
|
response = client.get(
|
|
"/shop/products",
|
|
headers={"Host": "wizamart.platform.com"}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert "Wizamart" in response.text
|
|
```
|
|
|
|
**See**: [Testing Guide](../testing/testing-guide.md)
|
|
|
|
## Debugging Middleware
|
|
|
|
### Enable Debug Logging
|
|
|
|
```python
|
|
import logging
|
|
|
|
logging.getLogger("middleware").setLevel(logging.DEBUG)
|
|
```
|
|
|
|
### Check Request State
|
|
|
|
In route handlers:
|
|
|
|
```python
|
|
@app.get("/debug")
|
|
async def debug_state(request: Request):
|
|
return {
|
|
"vendor": request.state.vendor.name if hasattr(request.state, 'vendor') else None,
|
|
"vendor_id": getattr(request.state, 'vendor_id', None),
|
|
"clean_path": getattr(request.state, 'clean_path', None),
|
|
"context_type": request.state.context_type.value if hasattr(request.state, 'context_type') else None,
|
|
"theme": bool(getattr(request.state, 'theme', None))
|
|
}
|
|
```
|
|
|
|
### Common Issues
|
|
|
|
| Issue | Cause | Solution |
|
|
|-------|-------|----------|
|
|
| Vendor not detected | Wrong host header | Check domain configuration |
|
|
| Context is FALLBACK | Path doesn't match patterns | Check route prefix |
|
|
| Theme not loading | Vendor ID missing | Check VendorContextMiddleware runs first |
|
|
| Sidebar broken | Variable name conflict | See frontend troubleshooting |
|
|
|
|
## Related Documentation
|
|
|
|
- [Multi-Tenant System](multi-tenant.md) - Detailed routing modes
|
|
- [Request Flow](request-flow.md) - Complete request journey
|
|
- [Authentication & RBAC](auth-rbac.md) - Security middleware
|
|
- [Backend API Reference](../backend/middleware-reference.md) - Technical API docs
|
|
- [Frontend Development](../frontend/overview.md) - Using middleware state in frontend
|
|
|
|
## Technical Reference
|
|
|
|
For detailed API documentation of middleware classes and methods, see:
|
|
- [Backend Middleware Reference](../backend/middleware-reference.md)
|
|
|
|
This includes:
|
|
- Complete class documentation
|
|
- Method signatures
|
|
- Parameter details
|
|
- Return types
|
|
- Auto-generated from source code
|