revamping documentation
This commit is contained in:
448
docs/architecture/middleware.md
Normal file
448
docs/architecture/middleware.md
Normal file
@@ -0,0 +1,448 @@
|
||||
# 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. 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
|
||||
|
||||
### 3. Path Rewrite Middleware
|
||||
|
||||
**Purpose**: Rewrite request paths for proper FastAPI routing
|
||||
|
||||
**What it does**:
|
||||
- Uses the `clean_path` extracted by VendorContextMiddleware
|
||||
- Rewrites `request.scope['path']` to remove vendor prefix
|
||||
- Allows FastAPI routes to match correctly
|
||||
|
||||
**Example**:
|
||||
```
|
||||
Original path: /vendor/WIZAMART/shop/products
|
||||
Clean path: /shop/products (set by VendorContextMiddleware)
|
||||
↓
|
||||
Path Rewrite Middleware changes request path to: /shop/products
|
||||
↓
|
||||
FastAPI router can now match: @app.get("/shop/products")
|
||||
```
|
||||
|
||||
**Why it's needed**: FastAPI routes don't include vendor prefix, so we strip it
|
||||
|
||||
### 4. Context Detection Middleware
|
||||
|
||||
**Purpose**: Determine the type/context of the request
|
||||
|
||||
**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`
|
||||
|
||||
**Detection Rules**:
|
||||
```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
|
||||
```
|
||||
|
||||
**Why it's useful**: Error handlers and templates adapt based on context
|
||||
|
||||
### 5. 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
|
||||
|
||||
## Middleware Execution Order
|
||||
|
||||
### The Stack (First to Last)
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Client Request] --> B[1. LoggingMiddleware]
|
||||
B --> C[2. VendorContextMiddleware]
|
||||
C --> D[3. PathRewriteMiddleware]
|
||||
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. **VendorContextMiddleware second**
|
||||
- Must run before PathRewriteMiddleware (provides clean_path)
|
||||
- Must run before ContextDetectionMiddleware (provides vendor)
|
||||
- Must run before ThemeContextMiddleware (provides vendor_id)
|
||||
|
||||
3. **PathRewriteMiddleware third**
|
||||
- Depends on clean_path from VendorContextMiddleware
|
||||
- Must run before ContextDetectionMiddleware (rewrites path)
|
||||
|
||||
4. **ContextDetectionMiddleware fourth**
|
||||
- Uses clean_path from VendorContextMiddleware
|
||||
- Uses rewritten path from PathRewriteMiddleware
|
||||
- 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!**
|
||||
|
||||
## Request State Variables
|
||||
|
||||
Middleware components inject these variables into `request.state`:
|
||||
|
||||
| Variable | Set By | Type | Used By | Description |
|
||||
|----------|--------|------|---------|-------------|
|
||||
| `vendor` | VendorContextMiddleware | Vendor | Theme, Templates | Current vendor object |
|
||||
| `vendor_id` | VendorContextMiddleware | int | Queries, Theme | Current vendor ID |
|
||||
| `clean_path` | VendorContextMiddleware | str | PathRewrite, Context | Path without vendor prefix |
|
||||
| `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 context #}
|
||||
{% if request.state.context_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. PathRewriteMiddleware
|
||||
↓ Path already clean (no rewrite needed for subdomain mode)
|
||||
↓ request.scope['path'] = "/shop/products"
|
||||
|
||||
4. ContextDetectionMiddleware
|
||||
↓ Analyzes path: "/shop/products"
|
||||
↓ Has vendor: Yes
|
||||
↓ Not admin/api/vendor dashboard
|
||||
↓ Sets: request.state.context_type = RequestContext.SHOP
|
||||
|
||||
5. ThemeContextMiddleware
|
||||
↓ Loads theme for vendor_id = 1
|
||||
↓ Sets: request.state.theme = {...theme config...}
|
||||
|
||||
6. FastAPI Router
|
||||
↓ Matches route: @app.get("/shop/products")
|
||||
↓ Calls handler function
|
||||
|
||||
7. 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
|
||||
|
||||
### ContextDetectionMiddleware
|
||||
- If clean_path missing: Uses original path
|
||||
- If vendor missing: Defaults to FALLBACK context
|
||||
- Always sets a context_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(ContextDetectionMiddleware)
|
||||
app.add_middleware(VendorContextMiddleware)
|
||||
```
|
||||
|
||||
**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
|
||||
Reference in New Issue
Block a user