# 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**:
- Guards based on platform domain awareness:
  - When `host == platform.domain` (e.g., `rewardflow.lu`): Methods 1 & 2 are skipped (the platform's own domain is not a store)
  - When host is a subdomain of `platform.domain` (e.g., `acme.rewardflow.lu`): Method 1 (custom domain) is skipped
- Detects store from (in priority order):
  1. Custom domain (e.g., `customdomain.com`) — via `StoreDomain` lookup (skipped on platform domains)
  2. Subdomain with two-step lookup:
     - `StorePlatform.custom_subdomain` — per-platform subdomain overrides (e.g., `wizatech-rewards.rewardflow.lu`)
     - `Store.subdomain` — standard subdomain fallback (e.g., `wizatech.omsflow.lu`)
  3. Path prefix (e.g., `/store/store1/` or `/stores/store1/`) — always runs as fallback
- 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

## Login Platform Resolution

The store login endpoint (`POST /api/v1/store/auth/login`) resolves the platform through a 3-source priority chain. This is necessary because on localhost the API path carries no platform information (unlike production where the domain does).

### Source Priority

```
Source 1: Middleware (request.state.platform)
    ↓ if null or "main"
Source 2: Request body (platform_code field)
    ↓ if null
Source 3: Fallback (store's first active platform)
```

### Resolution by URL Pattern

| Environment | Login Page URL | API Request Host | Source 1 | Source 2 | Source 3 |
|-------------|---------------|-----------------|----------|----------|----------|
| **Dev path-based** | `/platforms/loyalty/store/ACME/login` | `localhost:8000` | null (localhost → "main" → skipped) | `"loyalty"` from JS | — |
| **Dev no prefix** | `/store/ACME/login` (after logout) | `localhost:8000` | null | `"loyalty"` from localStorage | — |
| **Dev fresh browser** | `/store/ACME/login` (first visit) | `localhost:8000` | null | null | First active platform for store |
| **Prod domain** | `omsflow.lu/store/ACME/login` | `omsflow.lu` | `"oms"` (domain lookup) | — | — |
| **Prod subdomain** | `acme.omsflow.lu/store/login` | `acme.omsflow.lu` | `"oms"` (root domain lookup) | — | — |
| **Prod custom domain** | `wizatech.shop/store/login` | `wizatech.shop` | `"oms"` (StoreDomain lookup) | — | — |

### Client-Side Platform Persistence

On successful login, `login.js` saves the platform to localStorage:
```
localStorage.setItem('store_platform', response.platform_code)
```

On the login page, the platform_code sent in the body uses this priority:
```
window.STORE_PLATFORM_CODE || localStorage.getItem('store_platform') || null
```

- `window.STORE_PLATFORM_CODE` is set by the server template when the URL contains `/platforms/{code}/`
- `localStorage.store_platform` persists across logout (intentionally not cleared)
- This ensures the logout → login cycle preserves platform context in dev mode

### Diagnostic Tools

- **Backend**: `/admin/platform-debug` — traces the full resolution pipeline for arbitrary host/path combos
- **Frontend**: `Ctrl+Shift+P` on any store page (localhost only) — shows JWT platform, localStorage, window globals, and consistency checks

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