Some checks failed
Reflect the production routing refactor (ce5b54f): document store dashboard
double-mounting, per-platform subdomain overrides via StorePlatform.custom_subdomain,
get_resolved_store_code dependency, and /merchants/ reserved path. Update seed
script to populate custom_subdomain and StoreDomain.platform_id for demo data.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
583 lines
19 KiB
Markdown
583 lines
19 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/*`)
|
|
- Store dashboard pages (`/store/*`)
|
|
- Storefront pages (`/storefront/*` 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., `omsflow.lu`, `rewardflow.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? (omsflow.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 StoreContextMiddleware (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. Store Context Middleware
|
|
|
|
**Purpose**: Detect which store's storefront the request is for (multi-tenant core)
|
|
|
|
**What it does**:
|
|
- Detects store from:
|
|
- Custom domain (e.g., `customdomain.com`) — via `StoreDomain` lookup (optionally scoped to a platform via `StoreDomain.platform_id`)
|
|
- Subdomain with two-step lookup:
|
|
1. `StorePlatform.custom_subdomain` — per-platform subdomain overrides (e.g., `wizatech-rewards.rewardflow.lu`)
|
|
2. `Store.subdomain` — standard subdomain fallback (e.g., `wizatech.omsflow.lu`)
|
|
- Path prefix (e.g., `/store/store1/` or `/stores/store1/`)
|
|
- Queries database to find store by domain or code
|
|
- Injects store object and platform into `request.state.store`
|
|
- Extracts "clean path" (path without store prefix)
|
|
- Sets `request.state.clean_path` for routing
|
|
|
|
**Reserved paths**: When a storefront domain is detected, certain paths are excluded from the `/storefront/` rewrite: `/store/`, `/admin/`, `/api/`, `/static/`, `/storefront/`, `/health`, `/docs`, `/redoc`, `/media/`, `/assets/`, and `/merchants/`. This ensures that staff dashboards, APIs, and merchant-level routes are not incorrectly routed to storefront pages.
|
|
|
|
**Example**:
|
|
```
|
|
Request: https://orion.platform.com/storefront/products
|
|
↓
|
|
Middleware detects: store_code = "orion"
|
|
↓
|
|
Queries database: SELECT * FROM stores WHERE code = 'orion'
|
|
↓
|
|
Injects: request.state.store = <Store object>
|
|
request.state.store_id = 1
|
|
request.state.clean_path = "/storefront/products"
|
|
```
|
|
|
|
**Why it's critical**: Without this, the system wouldn't know which store'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 storefront routes are registered twice with different prefixes (`/storefront` and `/stores/{store_code}/storefront`). 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
|
|
- `STORE` - `/store/*`, `/api/v1/store/*` paths (management area)
|
|
- `STOREFRONT` - Customer shop pages (`/storefront/*`, `/stores/*`, store subdomains)
|
|
- `PLATFORM` - Marketing pages (`/`, `/pricing`, `/about`)
|
|
- Injects `request.state.frontend_type` (FrontendType enum)
|
|
|
|
**Detection Priority** (handled by `FrontendDetector`):
|
|
```python
|
|
1. Admin subdomain (admin.omsflow.lu) → ADMIN
|
|
2. Path-based detection:
|
|
- /admin/* or /api/v1/admin/* → ADMIN
|
|
- /store/* or /api/v1/store/* → STORE
|
|
- /storefront/*, /stores/* → STOREFRONT
|
|
- /api/v1/platform/* → PLATFORM
|
|
3. Store subdomain (orion.omsflow.lu) → STOREFRONT
|
|
4. Store 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 store-specific theme settings
|
|
|
|
**What it does**:
|
|
- Checks if request has a store (from StoreContextMiddleware)
|
|
- Queries database for store's theme settings
|
|
- Injects theme configuration into `request.state.theme`
|
|
- Provides default theme if store has no custom theme
|
|
|
|
**Theme Data Structure**:
|
|
```python
|
|
{
|
|
"primary_color": "#3B82F6",
|
|
"secondary_color": "#10B981",
|
|
"logo_url": "/static/stores/orion/logo.png",
|
|
"favicon_url": "/static/stores/orion/favicon.ico",
|
|
"custom_css": "/* store-specific styles */"
|
|
}
|
|
```
|
|
|
|
**Why it's needed**: Each store storefront 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/store_context.py
|
|
middleware/auth.py
|
|
|
|
❌ Avoid:
|
|
middleware/logging_middleware.py
|
|
middleware/store_context_middleware.py
|
|
middleware/auth_middleware.py
|
|
```
|
|
|
|
**Rationale**:
|
|
- Keeps names concise and consistent
|
|
- Follows Django, Flask, and FastAPI conventions
|
|
- Makes imports cleaner: `from middleware.store_context import StoreContextMiddleware`
|
|
- 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/store_context.py → tests/unit/middleware/test_store_context.py
|
|
middleware/auth.py → tests/unit/middleware/test_auth.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
|
|
```
|
|
|
|
#### 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_store_context.py # Tests only StoreContextManager/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.store_context import StoreContextManager
|
|
from middleware.auth import AuthManager
|
|
|
|
# ❌ Avoid - Less clear
|
|
from middleware import logging_middleware
|
|
from middleware import store_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. StoreContextMiddleware]
|
|
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 StoreContextMiddleware (sets platform context)
|
|
- Rewrites path for `/platforms/{code}/` prefixed requests
|
|
- Sets `request.state.platform` for downstream middleware
|
|
|
|
3. **StoreContextMiddleware third**
|
|
- Uses rewritten path from PlatformContextMiddleware
|
|
- Must run before ContextDetectionMiddleware (provides store and clean_path)
|
|
- Must run before ThemeContextMiddleware (provides store_id)
|
|
|
|
4. **ContextDetectionMiddleware fourth**
|
|
- Uses clean_path from StoreContextMiddleware
|
|
- Provides context_type for ThemeContextMiddleware
|
|
|
|
5. **ThemeContextMiddleware last**
|
|
- Depends on store from StoreContextMiddleware
|
|
- Depends on context_type from ContextDetectionMiddleware
|
|
|
|
**Breaking this order will break the application!**
|
|
|
|
**Note:** Path-based routing (e.g., `/stores/{code}/storefront/*`) 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) |
|
|
| `store` | StoreContextMiddleware | Store | Theme, Templates | Current store object |
|
|
| `store_id` | StoreContextMiddleware | int | Queries, Theme | Current store ID |
|
|
| `clean_path` | StoreContextMiddleware | str | Context | Path without store prefix (for context detection) |
|
|
| `context_type` | ContextDetectionMiddleware | RequestContext | Theme, Error handlers | Request context enum |
|
|
| `theme` | ThemeContextMiddleware | dict | Templates | Store theme config |
|
|
|
|
### Using in Route Handlers
|
|
|
|
```python
|
|
from fastapi import Request
|
|
|
|
@app.get("/storefront/products")
|
|
async def get_products(request: Request):
|
|
# Access store
|
|
store = request.state.store
|
|
store_id = request.state.store_id
|
|
|
|
# Access context
|
|
context = request.state.context_type
|
|
|
|
# Access theme
|
|
theme = request.state.theme
|
|
|
|
# Use in queries
|
|
products = db.query(Product).filter(
|
|
Product.store_id == store_id
|
|
).all()
|
|
|
|
return {"store": store.name, "products": products}
|
|
```
|
|
|
|
### Using in Templates
|
|
|
|
```jinja2
|
|
{# Access store #}
|
|
<h1>{{ request.state.store.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: Storefront Product Page Request
|
|
|
|
**URL**: `https://orion.myplatform.com/storefront/products`
|
|
|
|
**Middleware Processing**:
|
|
|
|
```
|
|
1. LoggingMiddleware
|
|
↓ Starts timer
|
|
↓ Logs: "Request: GET /storefront/products from 192.168.1.100"
|
|
|
|
2. StoreContextMiddleware
|
|
↓ Detects subdomain: "orion"
|
|
↓ Queries DB: store = get_store_by_code("orion")
|
|
↓ Sets: request.state.store = <Store: Orion>
|
|
↓ Sets: request.state.store_id = 1
|
|
↓ Sets: request.state.clean_path = "/storefront/products"
|
|
|
|
3. FrontendTypeMiddleware
|
|
↓ Uses FrontendDetector with path: "/storefront/products"
|
|
↓ Has store context: Yes
|
|
↓ Detects storefront frontend
|
|
↓ Sets: request.state.frontend_type = FrontendType.STOREFRONT
|
|
|
|
4. ThemeContextMiddleware
|
|
↓ Loads theme for store_id = 1
|
|
↓ Sets: request.state.theme = {...theme config...}
|
|
|
|
5. FastAPI Router
|
|
↓ Matches route: @app.get("/storefront/products")
|
|
↓ Calls handler function
|
|
|
|
6. Route Handler
|
|
↓ Accesses: request.state.store_id
|
|
↓ Queries: products WHERE store_id = 1
|
|
↓ Renders template with store data
|
|
|
|
8. Response
|
|
↓ Returns HTML with store theme
|
|
|
|
9. LoggingMiddleware (response phase)
|
|
↓ Logs: "Response: 200 for GET /storefront/products (0.143s)"
|
|
↓ Adds header: X-Process-Time: 0.143
|
|
```
|
|
|
|
## Error Handling in Middleware
|
|
|
|
Each middleware component handles errors gracefully:
|
|
|
|
### StoreContextMiddleware
|
|
- If store not found: Sets `request.state.store = None`
|
|
- If database error: Logs error, allows request to continue
|
|
- Fallback: Request proceeds without store context
|
|
|
|
### FrontendTypeMiddleware
|
|
- If clean_path missing: Uses original path
|
|
- If store missing: Defaults to PLATFORM frontend type
|
|
- Always sets a frontend_type (never None)
|
|
|
|
### ThemeContextMiddleware
|
|
- If store 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 StoreContextMiddleware (store 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 store 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
|
|
- Store 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(StoreContextMiddleware)
|
|
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.store_context import StoreContextManager
|
|
|
|
def test_store_detection_subdomain():
|
|
# Mock request
|
|
request = create_mock_request(host="orion.platform.com")
|
|
|
|
# Test detection
|
|
manager = StoreContextManager()
|
|
store = manager.detect_store_from_subdomain(request)
|
|
|
|
assert store.code == "orion"
|
|
```
|
|
|
|
### Integration Testing
|
|
|
|
Test the full middleware stack:
|
|
|
|
```python
|
|
def test_storefront_request_flow(client):
|
|
response = client.get(
|
|
"/storefront/products",
|
|
headers={"Host": "orion.platform.com"}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert "Orion" 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 {
|
|
"store": request.state.store.name if hasattr(request.state, 'store') else None,
|
|
"store_id": getattr(request.state, 'store_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 |
|
|
|-------|-------|----------|
|
|
| Store not detected | Wrong host header | Check domain configuration |
|
|
| Context is FALLBACK | Path doesn't match patterns | Check route prefix |
|
|
| Theme not loading | Store ID missing | Check StoreContextMiddleware 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
|