Database & Migrations: - Add application_logs table migration for hybrid cloud logging - Add companies table migration and restructure vendor relationships Logging System: - Implement hybrid logging system (database + file) - Add log_service for centralized log management - Create admin logs page with filtering and viewing capabilities - Add init_log_settings.py script for log configuration - Enhance core logging with database integration Marketplace Integration: - Add marketplace admin page with product management - Create marketplace vendor page with product listings - Implement marketplace.js for both admin and vendor interfaces - Add marketplace integration documentation Admin Enhancements: - Add imports management page and functionality - Create settings page for admin configuration - Add vendor themes management page - Enhance vendor detail and edit pages - Improve code quality dashboard and violation details - Add logs viewing and management - Update icons guide and shared icon system Architecture & Documentation: - Document frontend structure and component architecture - Document models structure and relationships - Add vendor-in-token architecture documentation - Add vendor RBAC (role-based access control) documentation - Document marketplace integration patterns - Update architecture patterns documentation Infrastructure: - Add platform static files structure (css, img, js) - Move architecture_scan.py to proper models location - Update model imports and registrations - Enhance exception handling - Update dependency injection patterns UI/UX: - Improve vendor edit interface - Update admin user interface - Enhance page templates documentation - Add vendor marketplace interface
17 KiB
Vendor-in-Token Architecture
Overview
This document describes the vendor-in-token authentication architecture used for vendor API endpoints. This architecture embeds vendor context directly into JWT tokens, eliminating the need for URL-based vendor detection and enabling clean, RESTful API endpoints.
The Problem: URL-Based Vendor Detection
Old Pattern (Deprecated)
# ❌ DEPRECATED: URL-based vendor detection
@router.get("/{product_id}")
def get_product(
product_id: int,
vendor: Vendor = Depends(require_vendor_context()), # ❌ Don't use
current_user: User = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
product = product_service.get_product(db, vendor.id, product_id)
return product
Issues with URL-Based Detection
-
Inconsistent API Routes
- Page routes:
/vendor/{vendor_code}/dashboard(has vendor in URL) - API routes:
/api/v1/vendor/products(no vendor in URL) require_vendor_context()only works when vendor is in the URL path
- Page routes:
-
404 Errors on API Endpoints
- API calls to
/api/v1/vendor/productswould return 404 - The dependency expected vendor 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 vendor context
- Harder to test and maintain
The Solution: Vendor-in-Token
Architecture Overview
┌─────────────────────────────────────────────────────────────────┐
│ Vendor Login Flow │
└─────────────────────────────────────────────────────────────────┘
│
↓
┌─────────────────────────────────────────────────────────────────┐
│ 1. Authenticate user credentials │
│ 2. Validate vendor membership │
│ 3. Create JWT with vendor context: │
│ { │
│ "sub": "user_id", │
│ "username": "john.doe", │
│ "vendor_id": 123, ← Vendor context in token │
│ "vendor_code": "WIZAMART", ← Vendor code in token │
│ "vendor_role": "Owner" ← Vendor role in token │
│ } │
└─────────────────────────────────────────────────────────────────┘
│
↓
┌─────────────────────────────────────────────────────────────────┐
│ 4. Set dual token storage: │
│ - HTTP-only cookie (path=/vendor) for page navigation │
│ - Response body for localStorage (API calls) │
└─────────────────────────────────────────────────────────────────┘
│
↓
┌─────────────────────────────────────────────────────────────────┐
│ 5. Subsequent API requests include vendor context │
│ Authorization: Bearer <token-with-vendor-context> │
└─────────────────────────────────────────────────────────────────┘
│
↓
┌─────────────────────────────────────────────────────────────────┐
│ 6. get_current_vendor_api() extracts vendor from token: │
│ - current_user.token_vendor_id │
│ - current_user.token_vendor_code │
│ - current_user.token_vendor_role │
│ 7. Validates user still has access to vendor │
└─────────────────────────────────────────────────────────────────┘
Implementation Components
1. Token Creation (middleware/auth.py)
def create_access_token(
self,
user: User,
vendor_id: int | None = None,
vendor_code: str | None = None,
vendor_role: str | None = None,
) -> dict[str, Any]:
"""Create JWT with optional vendor context."""
payload = {
"sub": str(user.id),
"username": user.username,
"email": user.email,
"role": user.role,
"exp": expire,
"iat": datetime.now(UTC),
}
# Include vendor information in token if provided
if vendor_id is not None:
payload["vendor_id"] = vendor_id
if vendor_code is not None:
payload["vendor_code"] = vendor_code
if vendor_role is not None:
payload["vendor_role"] = vendor_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. Vendor Login (app/api/v1/vendor/auth.py)
@router.post("/login", response_model=VendorLoginResponse)
def vendor_login(
user_credentials: UserLogin,
response: Response,
db: Session = Depends(get_db),
):
"""
Vendor team member login.
Creates vendor-scoped JWT token with vendor context embedded.
"""
# Authenticate user and determine vendor
login_result = auth_service.login_user(db=db, user_credentials=user_credentials)
user = login_result["user"]
# Determine vendor and role
vendor = determine_vendor(db, user) # Your vendor detection logic
vendor_role = determine_role(db, user, vendor) # Your role detection logic
# Create vendor-scoped access token
token_data = auth_service.auth_manager.create_access_token(
user=user,
vendor_id=vendor.id,
vendor_code=vendor.vendor_code,
vendor_role=vendor_role,
)
# Set cookie and return token
response.set_cookie(
key="vendor_token",
value=token_data["access_token"],
httponly=True,
path="/vendor", # Restricted to vendor routes
)
return VendorLoginResponse(**token_data, user=user, vendor=vendor)
3. Token Verification (app/api/deps.py)
def get_current_vendor_api(
authorization: str | None = Header(None, alias="Authorization"),
db: Session = Depends(get_db),
) -> User:
"""
Get current vendor API user from Authorization header.
Extracts vendor 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 vendor access if token is vendor-scoped
if hasattr(user, "token_vendor_id"):
vendor_id = user.token_vendor_id
# Verify user still has access to this vendor
if not user.is_member_of(vendor_id):
raise InsufficientPermissionsException(
"Access to vendor has been revoked. Please login again."
)
return user
4. Endpoint Usage (app/api/v1/vendor/products.py)
@router.get("", response_model=ProductListResponse)
def get_vendor_products(
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000),
current_user: User = Depends(get_current_vendor_api), # ✅ Only need this
db: Session = Depends(get_db),
):
"""
Get all products in vendor catalog.
Vendor is determined from JWT token (vendor_id claim).
"""
# Extract vendor ID from token
if not hasattr(current_user, "token_vendor_id"):
raise HTTPException(
status_code=400,
detail="Token missing vendor information. Please login again.",
)
vendor_id = current_user.token_vendor_id
# Use vendor_id from token for business logic
products, total = product_service.get_vendor_products(
db=db,
vendor_id=vendor_id,
skip=skip,
limit=limit,
)
return ProductListResponse(products=products, total=total)
Migration Guide
Step 1: Identify Endpoints Using require_vendor_context()
Search for all occurrences:
grep -r "require_vendor_context" app/api/v1/vendor/
Step 2: Update Endpoint Signature
Before:
@router.get("/{product_id}")
def get_product(
product_id: int,
vendor: Vendor = Depends(require_vendor_context()), # ❌ Remove this
current_user: User = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
After:
@router.get("/{product_id}")
def get_product(
product_id: int,
current_user: User = Depends(get_current_vendor_api), # ✅ Only need this
db: Session = Depends(get_db),
):
Step 3: Extract Vendor from Token
Before:
product = product_service.get_product(db, vendor.id, product_id)
After:
from fastapi import HTTPException
# Extract vendor ID from token
if not hasattr(current_user, "token_vendor_id"):
raise HTTPException(
status_code=400,
detail="Token missing vendor information. Please login again.",
)
vendor_id = current_user.token_vendor_id
# Use vendor_id from token
product = product_service.get_product(db, vendor_id, product_id)
Step 4: Update Logging References
Before:
logger.info(f"Product updated for vendor {vendor.vendor_code}")
After:
logger.info(f"Product updated for vendor {current_user.token_vendor_code}")
Complete Migration Example
Before (URL-based vendor detection):
@router.put("/{product_id}", response_model=ProductResponse)
def update_product(
product_id: int,
product_data: ProductUpdate,
vendor: Vendor = Depends(require_vendor_context()), # ❌
current_user: User = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""Update product in vendor catalog."""
product = product_service.update_product(
db=db,
vendor_id=vendor.id, # ❌ From URL
product_id=product_id,
product_update=product_data
)
logger.info(
f"Product {product_id} updated by {current_user.username} "
f"for vendor {vendor.vendor_code}" # ❌ From URL
)
return ProductResponse.model_validate(product)
After (Token-based vendor context):
@router.put("/{product_id}", response_model=ProductResponse)
def update_product(
product_id: int,
product_data: ProductUpdate,
current_user: User = Depends(get_current_vendor_api), # ✅ Only dependency
db: Session = Depends(get_db),
):
"""Update product in vendor catalog."""
from fastapi import HTTPException
# Extract vendor ID from token
if not hasattr(current_user, "token_vendor_id"):
raise HTTPException(
status_code=400,
detail="Token missing vendor information. Please login again.",
)
vendor_id = current_user.token_vendor_id # ✅ From token
product = product_service.update_product(
db=db,
vendor_id=vendor_id, # ✅ From token
product_id=product_id,
product_update=product_data
)
logger.info(
f"Product {product_id} updated by {current_user.username} "
f"for vendor {current_user.token_vendor_code}" # ✅ From token
)
return ProductResponse.model_validate(product)
Files to Migrate
Current files still using require_vendor_context():
app/api/v1/vendor/customers.pyapp/api/v1/vendor/notifications.pyapp/api/v1/vendor/media.pyapp/api/v1/vendor/marketplace.pyapp/api/v1/vendor/inventory.pyapp/api/v1/vendor/settings.pyapp/api/v1/vendor/analytics.pyapp/api/v1/vendor/payments.pyapp/api/v1/vendor/profile.py
Benefits of Vendor-in-Token
1. Clean RESTful APIs
✅ /api/v1/vendor/products
✅ /api/v1/vendor/orders
✅ /api/v1/vendor/customers
❌ /api/v1/vendor/{vendor_code}/products (unnecessary vendor in URL)
2. Security
- Vendor 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 vendor API endpoints
- No confusion between page routes and API routes
- Single source of truth (the token)
4. Performance
- No database lookup for vendor context on every request
- Vendor 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 vendor 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 vendor (database check)
Access Revocation
If a user's vendor access is revoked:
- Existing tokens remain valid until expiration
get_current_vendor_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 vendor membership
Testing
Unit Tests
def test_vendor_in_token():
"""Test vendor context in JWT token."""
# Create token with vendor context
token_data = auth_manager.create_access_token(
user=user,
vendor_id=123,
vendor_code="WIZAMART",
vendor_role="Owner",
)
# Verify token contains vendor data
payload = jwt.decode(token_data["access_token"], secret_key)
assert payload["vendor_id"] == 123
assert payload["vendor_code"] == "WIZAMART"
assert payload["vendor_role"] == "Owner"
def test_api_endpoint_uses_token_vendor():
"""Test API endpoint extracts vendor from token."""
response = client.get(
"/api/v1/vendor/products",
headers={"Authorization": f"Bearer {token}"}
)
assert response.status_code == 200
# Verify products are filtered by token vendor_id
Integration Tests
def test_vendor_login_and_api_access():
"""Test full vendor login and API access flow."""
# Login as vendor user
response = client.post("/api/v1/vendor/auth/login", json={
"username": "john.doe",
"password": "password123"
})
assert response.status_code == 200
token = response.json()["access_token"]
# Access vendor API with token
response = client.get(
"/api/v1/vendor/products",
headers={"Authorization": f"Bearer {token}"}
)
assert response.status_code == 200
# Verify vendor context from token
products = response.json()["products"]
# All products should belong to token vendor
Architecture Rules
See docs/architecture/rules/API-VND-001.md for the formal architecture rule enforcing this pattern.
Related Documentation
- Vendor RBAC System - Role-based access control for vendors
- Vendor Authentication - Complete authentication guide
- Architecture Rules - All architecture rules
- API Design Guidelines - RESTful API patterns
Summary
The vendor-in-token architecture:
- ✅ Embeds vendor context in JWT tokens
- ✅ Eliminates URL-based vendor detection
- ✅ Enables clean RESTful API endpoints
- ✅ Improves security and performance
- ✅ Simplifies endpoint implementation
- ✅ Follows architecture best practices
Migration Status: In progress - 9 endpoint files remaining to migrate