Replace all ~1,086 occurrences of Wizamart/wizamart/WIZAMART/WizaMart with Orion/orion/ORION across 184 files. This includes database identifiers, email addresses, domain references, R2 bucket names, DNS prefixes, encryption salt, Celery app name, config defaults, Docker configs, CI configs, documentation, seed data, and templates. Renames homepage-wizamart.html template to homepage-orion.html. Fixes duplicate file_pattern key in api.yaml architecture rule. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
579 lines
18 KiB
Markdown
579 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/*`)
|
|
- Store dashboard pages (`/store/*`)
|
|
- 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 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 shop the request is for (multi-tenant core)
|
|
|
|
**What it does**:
|
|
- Detects store from:
|
|
- Custom domain (e.g., `customdomain.com`)
|
|
- Subdomain (e.g., `store1.platform.com`)
|
|
- Path prefix (e.g., `/store/store1/` or `/stores/store1/`)
|
|
- Queries database to find store by domain or code
|
|
- Injects store object into `request.state.store`
|
|
- Extracts "clean path" (path without store prefix)
|
|
- Sets `request.state.clean_path` for routing
|
|
|
|
**Example**:
|
|
```
|
|
Request: https://orion.platform.com/shop/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 = "/shop/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 shop routes are registered twice with different prefixes (`/shop` and `/stores/{store_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
|
|
- `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.oms.lu) → ADMIN
|
|
2. Path-based detection:
|
|
- /admin/* or /api/v1/admin/* → ADMIN
|
|
- /store/* or /api/v1/store/* → STORE
|
|
- /storefront/*, /shop/*, /stores/* → STOREFRONT
|
|
- /api/v1/platform/* → PLATFORM
|
|
3. Store subdomain (orion.oms.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 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/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}/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) |
|
|
| `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("/shop/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: Shop Product Page Request
|
|
|
|
**URL**: `https://orion.myplatform.com/shop/products`
|
|
|
|
**Middleware Processing**:
|
|
|
|
```
|
|
1. LoggingMiddleware
|
|
↓ Starts timer
|
|
↓ Logs: "Request: GET /shop/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 = "/shop/products"
|
|
|
|
3. FrontendTypeMiddleware
|
|
↓ Uses FrontendDetector with path: "/shop/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("/shop/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 /shop/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_shop_request_flow(client):
|
|
response = client.get(
|
|
"/shop/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
|