# 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 │ └─────────────────────────────────────────────────────────────────┘ │ ↓ ┌─────────────────────────────────────────────────────────────────┐ │ 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` ### Shop Endpoints Shop endpoints (public, no authentication) still use `require_store_context()`: - `app/api/v1/shop/products.py` - Uses URL/subdomain/domain detection - `app/api/v1/shop/cart.py` - Uses URL/subdomain/domain detection This is correct behavior - shop 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