# 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. 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

### 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/context.py
class ContextManager:              # Business logic
class ContextMiddleware:           # ASGI wrapper
class RequestContext(Enum):        # Related enum

# 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_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 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. ContextDetectionMiddleware
   ↓ Analyzes path: "/shop/products"
   ↓ Has vendor: Yes
   ↓ Not admin/api/vendor dashboard
   ↓ Sets: request.state.context_type = RequestContext.SHOP

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

### 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
