Files
orion/docs/architecture/middleware.md
Samir Boulahtit d480b59df4
Some checks failed
CI / ruff (push) Successful in 9s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has been cancelled
docs: update routing docs and seed script for production routing changes
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>
2026-02-26 11:44:43 +01:00

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