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>
23 KiB
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)
# ❌ 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
-
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
- Page routes:
-
404 Errors on API Endpoints
- API calls to
/api/v1/store/productswould return 404 - The dependency expected store code in URL but API routes don't have it
- Breaking RESTful API design principles
- API calls to
-
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)
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)
@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)
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)
@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 thattoken_store_idis 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:
grep -r "require_store_context" app/api/v1/store/
Step 2: Update Endpoint Signature
Before:
@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:
@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:
product = product_service.get_product(db, store.id, product_id)
After:
# 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"). Theget_current_store_apidependency 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:
logger.info(f"Product updated for store {store.store_code}")
After:
logger.info(f"Product updated for store {current_user.token_store_code}")
Complete Migration Example
Before (URL-based store detection):
@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):
@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_apidependency handles all validation and raisesInvalidTokenExceptioniftoken_store_idis 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, setsrequest.state.storerequire_store_owner- Gets store from token, setsrequest.state.storerequire_any_store_permission()- Gets store from token, setsrequest.state.storerequire_all_store_permissions()- Gets store from token, setsrequest.state.storeget_user_permissions- Gets store from token, setsrequest.state.store
Shop Endpoints
Shop endpoints (public, no authentication) still use require_store_context():
app/api/v1/shop/products.py- Uses URL/subdomain/domain detectionapp/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:
- JWT signature verification (ensures token not tampered with)
- Token expiration check (typically 30 minutes)
- Optional: Verify user still member of store (database check)
Access Revocation
If a user's store access is revoked:
- Existing tokens remain valid until expiration
get_current_store_api()performs optional database check- User forced to re-login after token expires
- 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
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
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:
# .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:
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 - Role-based access control for stores
- Authentication & RBAC - Complete authentication guide
- Architecture Patterns - All architecture patterns
- Middleware Reference - 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