# Store-in-Token Architecture

## Overview

This document describes the store-in-token authentication architecture used for store API endpoints. This architecture embeds store context directly into JWT tokens, eliminating the need for URL-based store detection and enabling clean, RESTful API endpoints.

## The Problem: URL-Based Store Detection

### Old Pattern (Deprecated)
```python
# ❌ DEPRECATED: URL-based store detection
@router.get("/{product_id}")
def get_product(
    product_id: int,
    store: Store = Depends(require_store_context()),  # ❌ Don't use
    current_user: User = Depends(get_current_store_api),
    db: Session = Depends(get_db),
):
    product = product_service.get_product(db, store.id, product_id)
    return product
```

### Issues with URL-Based Detection

1. **Inconsistent API Routes**
   - Page routes: `/store/{store_code}/dashboard` (has store in URL)
   - API routes: `/api/v1/store/products` (no store in URL)
   - `require_store_context()` only works when store is in the URL path

2. **404 Errors on API Endpoints**
   - API calls to `/api/v1/store/products` would return 404
   - The dependency expected store code in URL but API routes don't have it
   - Breaking RESTful API design principles

3. **Architecture Violation**
   - Mixed concerns: URL structure determining business logic
   - Tight coupling between routing and store context
   - Harder to test and maintain

## The Solution: Store-in-Token

### Architecture Overview

```
┌─────────────────────────────────────────────────────────────────┐
│                     Store Login Flow                           │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│  1. Authenticate user credentials                               │
│  2. Validate store membership                                  │
│  3. Create JWT with store context:                             │
│     {                                                            │
│       "sub": "user_id",                                         │
│       "username": "john.doe",                                   │
│       "store_id": 123,          ← Store context in token     │
│       "store_code": "ORION",  ← Store code in token       │
│       "store_role": "Owner"      ← Store role in token       │
│     }                                                            │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│  4. Set dual token storage:                                     │
│     - HTTP-only cookie (path=/store) for page navigation      │
│     - Response body for localStorage (API calls)               │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│  5. Subsequent API requests include store context             │
│     Authorization: Bearer <token-with-store-context>          │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│  6. get_current_store_api() extracts store from token:       │
│     - current_user.token_store_id                              │
│     - current_user.token_store_code                            │
│     - current_user.token_store_role                            │
│  7. Validates user still has access to store                  │
└─────────────────────────────────────────────────────────────────┘
```

### Implementation Components

#### 1. Token Creation (middleware/auth.py)
```python
def create_access_token(
    self,
    user: User,
    store_id: int | None = None,
    store_code: str | None = None,
    store_role: str | None = None,
) -> dict[str, Any]:
    """Create JWT with optional store context."""
    payload = {
        "sub": str(user.id),
        "username": user.username,
        "email": user.email,
        "role": user.role,
        "exp": expire,
        "iat": datetime.now(UTC),
    }

    # Include store information in token if provided
    if store_id is not None:
        payload["store_id"] = store_id
    if store_code is not None:
        payload["store_code"] = store_code
    if store_role is not None:
        payload["store_role"] = store_role

    return {
        "access_token": jwt.encode(payload, self.secret_key, algorithm=self.algorithm),
        "token_type": "bearer",
        "expires_in": self.access_token_expire_minutes * 60,
    }
```

#### 2. Store Login (app/api/v1/store/auth.py)
```python
@router.post("/login", response_model=StoreLoginResponse)
def store_login(
    user_credentials: UserLogin,
    response: Response,
    db: Session = Depends(get_db),
):
    """
    Store team member login.

    Creates store-scoped JWT token with store context embedded.
    """
    # Authenticate user and determine store
    login_result = auth_service.login_user(db=db, user_credentials=user_credentials)
    user = login_result["user"]

    # Determine store and role
    store = determine_store(db, user)  # Your store detection logic
    store_role = determine_role(db, user, store)  # Your role detection logic

    # Create store-scoped access token
    token_data = auth_service.auth_manager.create_access_token(
        user=user,
        store_id=store.id,
        store_code=store.store_code,
        store_role=store_role,
    )

    # Set cookie and return token
    response.set_cookie(
        key="store_token",
        value=token_data["access_token"],
        httponly=True,
        path="/store",  # Restricted to store routes
    )

    return StoreLoginResponse(**token_data, user=user, store=store)
```

#### 3. Token Verification (app/api/deps.py)
```python
def get_current_store_api(
    authorization: str | None = Header(None, alias="Authorization"),
    db: Session = Depends(get_db),
) -> User:
    """
    Get current store API user from Authorization header.

    Extracts store context from JWT token and validates access.
    """
    if not authorization or not authorization.startswith("Bearer "):
        raise AuthenticationException("Authorization header required for API calls")

    token = authorization.replace("Bearer ", "")
    user = auth_service.auth_manager.get_current_user(token, db)

    # Validate store access if token is store-scoped
    if hasattr(user, "token_store_id"):
        store_id = user.token_store_id

        # Verify user still has access to this store
        if not user.is_member_of(store_id):
            raise InsufficientPermissionsException(
                "Access to store has been revoked. Please login again."
            )

    return user
```

#### 4. Endpoint Usage (app/api/v1/store/products.py)
```python
@router.get("", response_model=ProductListResponse)
def get_store_products(
    skip: int = Query(0, ge=0),
    limit: int = Query(100, ge=1, le=1000),
    current_user: User = Depends(get_current_store_api),  # ✅ Guarantees token_store_id
    db: Session = Depends(get_db),
):
    """
    Get all products in store catalog.

    Store is determined from JWT token (store_id claim).
    The get_current_store_api dependency GUARANTEES token_store_id is present.
    """
    # Use store_id from token for business logic
    # NO validation needed - dependency guarantees token_store_id exists
    products, total = product_service.get_store_products(
        db=db,
        store_id=current_user.token_store_id,  # Safe to use directly
        skip=skip,
        limit=limit,
    )

    return ProductListResponse(products=products, total=total)
```

> **IMPORTANT**: The `get_current_store_api()` dependency now **guarantees** that `token_store_id` is present.
> Endpoints should NOT check for its existence - this would be redundant validation that belongs in the dependency layer.

## Migration Guide

### Step 1: Identify Endpoints Using require_store_context()

Search for all occurrences:
```bash
grep -r "require_store_context" app/api/v1/store/
```

### Step 2: Update Endpoint Signature

**Before:**
```python
@router.get("/{product_id}")
def get_product(
    product_id: int,
    store: Store = Depends(require_store_context()),  # ❌ Remove this
    current_user: User = Depends(get_current_store_api),
    db: Session = Depends(get_db),
):
```

**After:**
```python
@router.get("/{product_id}")
def get_product(
    product_id: int,
    current_user: User = Depends(get_current_store_api),  # ✅ Only need this
    db: Session = Depends(get_db),
):
```

### Step 3: Extract Store from Token

**Before:**
```python
product = product_service.get_product(db, store.id, product_id)
```

**After:**
```python
# Use store_id from token directly - dependency guarantees it exists
product = product_service.get_product(db, current_user.token_store_id, product_id)
```

> **NOTE**: Do NOT add validation like `if not hasattr(current_user, "token_store_id")`.
> The `get_current_store_api` dependency guarantees this attribute is present.
> Adding such checks violates the architecture rule API-003 (endpoints should not raise exceptions).

### Step 4: Update Logging References

**Before:**
```python
logger.info(f"Product updated for store {store.store_code}")
```

**After:**
```python
logger.info(f"Product updated for store {current_user.token_store_code}")
```

### Complete Migration Example

**Before (URL-based store detection):**
```python
@router.put("/{product_id}", response_model=ProductResponse)
def update_product(
    product_id: int,
    product_data: ProductUpdate,
    store: Store = Depends(require_store_context()),  # ❌
    current_user: User = Depends(get_current_store_api),
    db: Session = Depends(get_db),
):
    """Update product in store catalog."""
    product = product_service.update_product(
        db=db,
        store_id=store.id,  # ❌ From URL
        product_id=product_id,
        product_update=product_data
    )

    logger.info(
        f"Product {product_id} updated by {current_user.username} "
        f"for store {store.store_code}"  # ❌ From URL
    )

    return ProductResponse.model_validate(product)
```

**After (Token-based store context):**
```python
@router.put("/{product_id}", response_model=ProductResponse)
def update_product(
    product_id: int,
    product_data: ProductUpdate,
    current_user: User = Depends(get_current_store_api),  # ✅ Guarantees token_store_id
    db: Session = Depends(get_db),
):
    """Update product in store catalog."""
    # NO validation needed - dependency guarantees token_store_id exists
    product = product_service.update_product(
        db=db,
        store_id=current_user.token_store_id,  # ✅ From token - safe to use directly
        product_id=product_id,
        product_update=product_data
    )

    logger.info(
        f"Product {product_id} updated by {current_user.username} "
        f"for store {current_user.token_store_code}"  # ✅ From token
    )

    return ProductResponse.model_validate(product)
```

> **Architecture Rule API-003**: Endpoints should NOT raise exceptions. The `get_current_store_api` dependency
> handles all validation and raises `InvalidTokenException` if `token_store_id` is missing.

## Migration Status

**COMPLETED** - All store API endpoints have been migrated to use the token-based store context pattern.

### Migrated Files
All store API files now use `current_user.token_store_id`:
- `app/api/v1/store/customers.py` ✅
- `app/api/v1/store/notifications.py` ✅
- `app/api/v1/store/media.py` ✅
- `app/api/v1/store/marketplace.py` ✅
- `app/api/v1/store/inventory.py` ✅
- `app/api/v1/store/settings.py` ✅
- `app/api/v1/store/analytics.py` ✅
- `app/api/v1/store/payments.py` ✅
- `app/api/v1/store/profile.py` ✅
- `app/api/v1/store/dashboard.py` ✅
- `app/api/v1/store/products.py` ✅
- `app/api/v1/store/orders.py` ✅
- `app/api/v1/store/team.py` ✅ (uses permission dependencies)

### Permission Dependencies Updated
The following permission dependencies now use token-based store context:
- `require_store_permission()` - Gets store from token, sets `request.state.store`
- `require_store_owner` - Gets store from token, sets `request.state.store`
- `require_any_store_permission()` - Gets store from token, sets `request.state.store`
- `require_all_store_permissions()` - Gets store from token, sets `request.state.store`
- `get_user_permissions` - Gets store from token, sets `request.state.store`

### Storefront Endpoints
Storefront endpoints (public, no authentication) still use `require_store_context()`:
- `app/api/v1/storefront/products.py` - Uses URL/subdomain/domain detection
- `app/api/v1/storefront/cart.py` - Uses URL/subdomain/domain detection

This is correct behavior - storefront endpoints need to detect store from the request URL, not from JWT token.

## Benefits of Store-in-Token

### 1. Clean RESTful APIs
```
✅ /api/v1/store/products
✅ /api/v1/store/orders
✅ /api/v1/store/customers

❌ /api/v1/store/{store_code}/products  (unnecessary store in URL)
```

### 2. Security
- Store context cryptographically signed in JWT
- Cannot be tampered with by client
- Automatic validation on every request
- Token revocation possible via database checks

### 3. Consistency
- Same authentication mechanism for all store API endpoints
- No confusion between page routes and API routes
- Single source of truth (the token)

### 4. Performance
- No database lookup for store context on every request
- Store information already in token payload
- Optional validation for revoked access

### 5. Maintainability
- Simpler endpoint signatures
- Less boilerplate code
- Easier to test
- Follows architecture rule API-002 (no DB queries in endpoints)

## Security Considerations

### Token Validation
The token store context is validated on every request:
1. JWT signature verification (ensures token not tampered with)
2. Token expiration check (typically 30 minutes)
3. Optional: Verify user still member of store (database check)

### Access Revocation
If a user's store access is revoked:
1. Existing tokens remain valid until expiration
2. `get_current_store_api()` performs optional database check
3. User forced to re-login after token expires
4. New login will fail if access revoked

### Token Refresh
Tokens should be refreshed periodically:
- Default: 30 minutes expiration
- Refresh before expiration for seamless UX
- New login creates new token with current store membership

## Testing

### Unit Tests
```python
def test_store_in_token():
    """Test store context in JWT token."""
    # Create token with store context
    token_data = auth_manager.create_access_token(
        user=user,
        store_id=123,
        store_code="ORION",
        store_role="Owner",
    )

    # Verify token contains store data
    payload = jwt.decode(token_data["access_token"], secret_key)
    assert payload["store_id"] == 123
    assert payload["store_code"] == "ORION"
    assert payload["store_role"] == "Owner"

def test_api_endpoint_uses_token_store():
    """Test API endpoint extracts store from token."""
    response = client.get(
        "/api/v1/store/products",
        headers={"Authorization": f"Bearer {token}"}
    )
    assert response.status_code == 200
    # Verify products are filtered by token store_id
```

### Integration Tests
```python
def test_store_login_and_api_access():
    """Test full store login and API access flow."""
    # Login as store user
    response = client.post("/api/v1/store/auth/login", json={
        "username": "john.doe",
        "password": "password123"
    })
    assert response.status_code == 200
    token = response.json()["access_token"]

    # Access store API with token
    response = client.get(
        "/api/v1/store/products",
        headers={"Authorization": f"Bearer {token}"}
    )
    assert response.status_code == 200

    # Verify store context from token
    products = response.json()["products"]
    # All products should belong to token store
```

## Architecture Rules and Design Pattern Enforcement

### The Layered Exception Pattern

The architecture enforces a strict layered pattern for where exceptions should be raised:

```
┌────────────────────────────────────────────────────────────────────────────┐
│  ENDPOINTS (Thin Layer) - app/api/v1/**/*.py                               │
│                                                                            │
│  ❌ MUST NOT raise exceptions                                              │
│  ❌ MUST NOT check hasattr(current_user, 'token_store_id')                │
│  ✅ MUST trust dependencies to handle validation                           │
│  ✅ MUST directly use current_user.token_store_id                         │
└────────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌────────────────────────────────────────────────────────────────────────────┐
│  DEPENDENCIES (Validation Layer) - app/api/deps.py                         │
│                                                                            │
│  ✅ MUST raise InvalidTokenException if token_store_id missing            │
│  ✅ MUST validate user still has store access                             │
│  ✅ GUARANTEES token_store_id, token_store_code, token_store_role       │
└────────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌────────────────────────────────────────────────────────────────────────────┐
│  SERVICES (Business Logic) - app/services/**/*.py                          │
│                                                                            │
│  ✅ MUST raise domain exceptions for business rule violations              │
│  ✅ Examples: StoreNotFoundException, ProductNotFoundException            │
└────────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌────────────────────────────────────────────────────────────────────────────┐
│  GLOBAL EXCEPTION HANDLER - app/exceptions/handler.py                      │
│                                                                            │
│  ✅ Catches all OrionException subclasses                               │
│  ✅ Converts to appropriate HTTP responses                                 │
│  ✅ Provides consistent error formatting                                   │
└────────────────────────────────────────────────────────────────────────────┘
```

### Enforced by Architecture Validation

The validation script (`scripts/validate/validate_architecture.py`) enforces these rules:

**Rule API-003: Endpoints must NOT raise exceptions directly**
- Detects `raise HTTPException`, `raise InvalidTokenException`, etc. in endpoint files
- Detects redundant validation like `if not hasattr(current_user, 'token_store_id')`
- Blocks commits via pre-commit hook if violations found

### Pre-commit Hook

Architecture validation runs on every commit:

```yaml
# .pre-commit-config.yaml
- repo: local
  hooks:
    - id: validate-architecture
      name: Validate Architecture Patterns
      entry: python scripts/validate/validate_architecture.py
      language: python
      pass_filenames: false
      always_run: true
```

To run manually:
```bash
python scripts/validate/validate_architecture.py  # Full validation
python scripts/validate/validate_architecture.py -d app/api/v1/store/  # Specific directory
```

See `.architecture-rules.yaml` for the complete rule definitions.

## Related Documentation

- [Store RBAC System](./store-rbac.md) - Role-based access control for stores
- [Authentication & RBAC](../architecture/auth-rbac.md) - Complete authentication guide
- [Architecture Patterns](../architecture/architecture-patterns.md) - All architecture patterns
- [Middleware Reference](./middleware-reference.md) - Middleware patterns

## Summary

The store-in-token architecture:
- ✅ Embeds store context in JWT tokens
- ✅ Eliminates URL-based store detection
- ✅ Enables clean RESTful API endpoints
- ✅ Improves security and performance
- ✅ Simplifies endpoint implementation
- ✅ Follows architecture best practices

**Migration Status:** ✅ COMPLETED - All store API endpoints migrated and architecture rules enforced
