revamping documentation

This commit is contained in:
2025-11-17 22:59:42 +01:00
parent bbd64a6f21
commit 807033be16
107 changed files with 11973 additions and 28413 deletions

View File

@@ -0,0 +1,522 @@
# Authentication & Role-Based Access Control (RBAC)
Complete guide to the authentication and authorization system powering the multi-tenant platform.
## Overview
The platform uses a JWT-based authentication system combined with role-based access control (RBAC) to secure all interfaces:
- **Admin** interface
- **Vendor** dashboard
- **Shop** storefront
- **REST API** endpoints
## Authentication System
### Technology Stack
- **JWT (JSON Web Tokens)**: Stateless authentication
- **bcrypt**: Secure password hashing
- **Jose**: JWT encoding/decoding library
- **FastAPI Security**: OAuth2 password bearer flow
### Authentication Flow
```mermaid
sequenceDiagram
participant Client
participant API
participant AuthManager
participant Database
Client->>API: POST /api/v1/auth/login<br/>{username, password}
API->>AuthManager: authenticate_user()
AuthManager->>Database: Query user by username/email
Database-->>AuthManager: User record
AuthManager->>AuthManager: verify_password()
AuthManager-->>API: User object
API->>AuthManager: create_access_token()
AuthManager-->>API: JWT token
API-->>Client: {access_token, token_type, expires_in}
Note over Client: Store token
Client->>API: GET /api/v1/resource<br/>Authorization: Bearer <token>
API->>AuthManager: verify_token()
AuthManager->>AuthManager: Decode JWT
AuthManager->>Database: Query user by ID
Database-->>AuthManager: User object
AuthManager-->>API: Current user
API->>API: Process request
API-->>Client: Resource data
```
## User Roles
The platform has three distinct user roles, each with specific permissions and access levels:
### Customer Role
**Access**: Public shop and own account space
**Capabilities**:
- Browse vendor shops
- Place orders
- Manage their own account and order history
- View order status
- Update profile information
- Can register directly from shop frontend
**Account Creation**: Self-registration via shop frontend (email verification required)
**Authentication**: Standard JWT authentication
### Vendor Role
**Access**: Vendor area based on permissions
**Types**:
- **Vendor Owner**: Full access to vendor dashboard and settings
- **Vendor Team Members**: Access based on assigned permissions
**Capabilities**:
- Manage products and inventory
- Process orders
- View analytics and reports
- Configure shop settings (owners only)
- Manage team members (owners only)
- Access vendor-specific APIs
**Account Creation**:
- Owners: Created automatically when admin creates a vendor
- Team members: Invited by vendor owner via email
**Permissions System**: Team members can have granular permissions for different areas
### Admin Role
**Access**: Full platform administration
**Capabilities**:
- Manage all vendors
- Create/manage vendor accounts
- Access system settings
- View all data across the platform
- Manage users of all types
- Access audit logs
- Platform-wide analytics
**Account Creation**: Created by super admins on the backend
**Super Privileges**: Admins can access all areas including vendor and customer sections
## Application Areas & Access Control
The platform has three distinct areas with different access requirements:
| Area | URL Pattern | Access | Purpose |
|------|-------------|--------|---------|
| **Admin** | `/admin/*` or `admin.platform.com` | Admin users only | Platform administration and vendor management |
| **Vendor** | `/vendor/*` | Vendor owners and team members | Vendor dashboard and shop management |
| **Shop** | `/shop/*`, custom domains, subdomains | Customers and public | Public-facing eCommerce storefront |
| **API** | `/api/*` | All authenticated users (role-based) | REST API for all operations |
## Account Registration Flow
### Admin Accounts
- ❌ Cannot register from frontend
- ✅ Created by super admins on the backend
- Used for: Platform administration
### Vendor Accounts
- ❌ Cannot register from frontend
-**Vendor Owners**: Automatically created when admin creates a new vendor
-**Team Members**: Invited by vendor owner via email invitation
- Activation: Upon clicking email verification link
### Customer Accounts
- ✅ Can register directly on vendor shop
- Activation: Upon clicking registration email link
- Used for: Shopping and order management
## Role Enforcement Methods
The `AuthManager` class provides several methods for role-based access control:
### require_admin()
Restricts access to admin users only.
**Usage**:
```python
from fastapi import Depends
from models.database.user import User
from middleware.auth import auth_manager
@app.get("/admin/dashboard")
async def admin_dashboard(
current_user: User = Depends(auth_manager.require_admin)
):
return {"message": "Admin access"}
```
**Raises**: `AdminRequiredException` if user is not admin
### require_vendor()
Allows access to vendor users and admins.
**Usage**:
```python
@app.get("/vendor/products")
async def vendor_products(
current_user: User = Depends(auth_manager.require_vendor)
):
return {"products": [...]}
```
**Raises**: `InsufficientPermissionsException` if user is not vendor or admin
### require_customer()
Allows access to customer users and admins.
**Usage**:
```python
@app.get("/shop/orders")
async def customer_orders(
current_user: User = Depends(auth_manager.require_customer)
):
return {"orders": [...]}
```
**Raises**: `InsufficientPermissionsException` if user is not customer or admin
### require_role()
Custom role enforcement for specific roles.
**Usage**:
```python
@app.get("/custom-endpoint")
@auth_manager.require_role("custom_role")
async def custom_endpoint(current_user: User):
return {"message": "Custom role access"}
```
**Returns**: Decorator function that validates role
## JWT Token Structure
### Token Payload
```json
{
"sub": "123", // User ID (JWT standard claim)
"username": "testuser", // Username for display
"email": "user@example.com", // User email
"role": "vendor", // User role
"exp": 1700000000, // Expiration timestamp (JWT standard)
"iat": 1699999000 // Issued at timestamp (JWT standard)
}
```
### Token Configuration
| Variable | Description | Default |
|----------|-------------|---------|
| `JWT_SECRET_KEY` | Secret key for JWT signing | Development key (change in production!) |
| `JWT_EXPIRE_MINUTES` | Token expiration time in minutes | 30 |
**Environment Configuration**:
```bash
# .env
JWT_SECRET_KEY=your-super-secret-key-change-in-production
JWT_EXPIRE_MINUTES=30
```
## Permission Hierarchy
```mermaid
graph TD
A[Admin] --> B[Full Platform Access]
A --> C[Can Access All Areas]
D[Vendor Owner] --> E[Vendor Dashboard]
D --> F[Team Management]
D --> G[Shop Settings]
D --> H[All Vendor Data]
I[Vendor Team Member] --> E
I --> J[Limited Based on Permissions]
K[Customer] --> L[Shop Access]
K --> M[Own Orders]
K --> N[Own Profile]
```
**Admin Override**: Admin users have implicit access to all areas, including vendor and customer sections. This allows admins to provide support and manage the platform effectively.
## Security Features
### Password Security
**Hashing**:
- Algorithm: bcrypt
- Automatic salt generation
- Configurable work factor
**Example**:
```python
from middleware.auth import auth_manager
# Hash password
hashed = auth_manager.hash_password("user_password")
# Verify password
is_valid = auth_manager.verify_password("user_password", hashed)
```
### Token Security
**Features**:
- Signed with secret key (prevents tampering)
- Includes expiration time
- Stateless (no server-side session storage)
- Short-lived (30 minutes default)
**Best Practices**:
- Use HTTPS in production
- Store tokens securely on client
- Implement token refresh mechanism
- Clear tokens on logout
### Protection Against Common Attacks
**SQL Injection**:
- ✅ SQLAlchemy ORM with parameterized queries
- ✅ Input validation with Pydantic
**XSS (Cross-Site Scripting)**:
- ✅ Jinja2 auto-escaping
- ✅ Content Security Policy headers
**CSRF (Cross-Site Request Forgery)**:
- ✅ JWT tokens in Authorization header (not cookies)
- ✅ SameSite cookie attribute for session cookies
**Brute Force**:
- ✅ Rate limiting on auth endpoints
- ✅ Account lockout after failed attempts (future)
## Authentication Endpoints
### Register User
```http
POST /api/v1/auth/register
Content-Type: application/json
{
"email": "user@example.com",
"username": "testuser",
"password": "securepassword123"
}
```
**Response**:
```json
{
"id": 123,
"username": "testuser",
"email": "user@example.com",
"role": "customer",
"is_active": true
}
```
### Login
```http
POST /api/v1/auth/login
Content-Type: application/json
{
"username": "testuser",
"password": "securepassword123"
}
```
**Response**:
```json
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
"token_type": "bearer",
"expires_in": 1800
}
```
### Using Authentication
Include the JWT token in the Authorization header:
```http
GET /api/v1/resource
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...
```
## Error Handling
### Authentication Errors
| Error | Status Code | Description |
|-------|-------------|-------------|
| `InvalidCredentialsException` | 401 | Username/password incorrect |
| `InvalidTokenException` | 401 | JWT token invalid or malformed |
| `TokenExpiredException` | 401 | JWT token has expired |
| `UserNotActiveException` | 403 | User account is inactive |
### Authorization Errors
| Error | Status Code | Description |
|-------|-------------|-------------|
| `AdminRequiredException` | 403 | Endpoint requires admin role |
| `InsufficientPermissionsException` | 403 | User lacks required permissions |
## Testing Authentication
### Unit Tests
```python
import pytest
from middleware.auth import AuthManager
def test_password_hashing():
auth_manager = AuthManager()
password = "test_password"
hashed = auth_manager.hash_password(password)
assert auth_manager.verify_password(password, hashed)
assert not auth_manager.verify_password("wrong_password", hashed)
def test_create_token():
auth_manager = AuthManager()
user = create_test_user(role="vendor")
token_data = auth_manager.create_access_token(user)
assert "access_token" in token_data
assert "token_type" in token_data
assert token_data["token_type"] == "bearer"
```
### Integration Tests
```python
def test_login_flow(client):
# Register user
response = client.post("/api/v1/auth/register", json={
"username": "testuser",
"email": "test@example.com",
"password": "password123"
})
assert response.status_code == 200
# Login
response = client.post("/api/v1/auth/login", json={
"username": "testuser",
"password": "password123"
})
assert response.status_code == 200
token = response.json()["access_token"]
# Access protected endpoint
response = client.get(
"/api/v1/profile",
headers={"Authorization": f"Bearer {token}"}
)
assert response.status_code == 200
```
## Best Practices
### For Developers
1. **Always hash passwords**: Never store plain text passwords
2. **Use dependency injection**: Leverage FastAPI's `Depends` for auth
3. **Validate tokens**: Always validate tokens on protected endpoints
4. **Check permissions**: Verify user has required role/permissions
5. **Log auth events**: Track login, logout, failed attempts
### For Operations
1. **Strong secret keys**: Use long, random JWT secret keys
2. **HTTPS only**: Never send tokens over HTTP
3. **Token expiration**: Keep token lifetimes short
4. **Rotate secrets**: Periodically rotate JWT secret keys
5. **Monitor auth logs**: Watch for suspicious activity
### For Security
1. **Rate limiting**: Limit auth endpoint requests
2. **Account lockout**: Implement after N failed attempts
3. **Email verification**: Require email confirmation
4. **Password policies**: Enforce strong password requirements
5. **2FA support**: Consider adding two-factor authentication
## Examples
### Protecting an Endpoint
```python
from fastapi import Depends, APIRouter
from sqlalchemy.orm import Session
from middleware.auth import auth_manager
from app.core.database import get_db
from models.database.user import User
router = APIRouter()
@router.get("/vendors")
async def get_vendors(
current_user: User = Depends(auth_manager.require_admin),
db: Session = Depends(get_db)
):
"""Only admins can list all vendors."""
vendors = db.query(Vendor).all()
return {"vendors": vendors}
```
### Multi-Role Access
```python
@router.get("/dashboard")
async def dashboard(
current_user: User = Depends(auth_manager.get_current_user),
db: Session = Depends(get_db)
):
"""Accessible by all authenticated users, but returns different data."""
if current_user.role == "admin":
# Admin sees everything
data = get_admin_dashboard(db)
elif current_user.role == "vendor":
# Vendor sees their data only
data = get_vendor_dashboard(db, current_user.id)
else:
# Customer sees their orders
data = get_customer_dashboard(db, current_user.id)
return data
```
## Related Documentation
- [Middleware Stack](middleware.md) - System-wide request processing
- [Error Handling](../api/error-handling.md) - Exception handling
- [Backend API Reference](../backend/middleware-reference.md) - Technical AuthManager docs
- [Testing Guide](../testing/testing-guide.md) - Testing authentication
## Technical Reference
For detailed API documentation of authentication classes and methods:
- [AuthManager API Reference](../backend/middleware-reference.md#authentication-authorization)

View File

@@ -0,0 +1,416 @@
# Multi-Domain Architecture Diagram
## Current vs New Architecture
### BEFORE (Current Setup)
```
┌─────────────────────────────────────────────────────────────────┐
│ Your FastAPI Application │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Vendor Context Middleware │ │
│ │ │ │
│ │ Check Host header: │ │
│ │ • vendor1.platform.com → Query Vendor.subdomain │ │
│ │ • /vendor/vendor1/ → Query Vendor.subdomain │ │
│ └────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Database: vendors table │ │
│ │ ┌──────────────────────────────────────────────────────┐ │ │
│ │ │ id │ subdomain │ name │ is_active │ │ │
│ │ ├────┼───────────┼─────────────┼─────────────────────┤ │ │
│ │ │ 1 │ vendor1 │ Shop Alpha │ true │ │ │
│ │ │ 2 │ vendor2 │ Shop Beta │ true │ │ │
│ │ └──────────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
Customers access via:
→ vendor1.platform.com (production)
→ /vendor/vendor1/ (development)
```
### AFTER (With Custom Domains)
```
┌─────────────────────────────────────────────────────────────────┐
│ Your FastAPI Application │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Enhanced Vendor Context Middleware │ │
│ │ │ │
│ │ Priority 1: Check if custom domain │ │
│ │ • customdomain1.com → Query VendorDomain.domain │ │
│ │ │ │
│ │ Priority 2: Check if subdomain │ │
│ │ • vendor1.platform.com → Query Vendor.subdomain │ │
│ │ │ │
│ │ Priority 3: Check if path-based │ │
│ │ • /vendor/vendor1/ → Query Vendor.subdomain │ │
│ └────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Database: vendors table │ │
│ │ ┌──────────────────────────────────────────────────────┐ │ │
│ │ │ id │ subdomain │ name │ is_active │ │ │
│ │ ├────┼───────────┼─────────────┼─────────────────────┤ │ │
│ │ │ 1 │ vendor1 │ Shop Alpha │ true │ │ │
│ │ │ 2 │ vendor2 │ Shop Beta │ true │ │ │
│ │ └──────────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ NEW TABLE: vendor_domains │ │
│ │ ┌──────────────────────────────────────────────────────┐ │ │
│ │ │ id │ vendor_id │ domain │ is_verified │ │ │
│ │ ├────┼───────────┼───────────────────┼───────────────┤ │ │
│ │ │ 1 │ 1 │ customdomain1.com │ true │ │ │
│ │ │ 2 │ 1 │ shop.alpha.com │ true │ │ │
│ │ │ 3 │ 2 │ customdomain2.com │ true │ │ │
│ │ └──────────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
Customers can now access via:
→ customdomain1.com (custom domain - Vendor 1)
→ shop.alpha.com (custom domain - Vendor 1)
→ customdomain2.com (custom domain - Vendor 2)
→ vendor1.platform.com (subdomain - still works!)
→ /vendor/vendor1/ (path-based - still works!)
```
## Request Flow Diagram
### Scenario 1: Customer visits customdomain1.com
```
┌──────────────────────┐
│ Customer Browser │
│ │
│ Visit: │
│ customdomain1.com │
└──────────┬───────────┘
│ HTTP Request
│ Host: customdomain1.com
┌──────────────────────┐
│ DNS Resolution │
│ │
│ customdomain1.com │
│ ↓ │
│ 123.45.67.89 │ (Your server IP)
└──────────┬───────────┘
│ Routes to server
┌──────────────────────┐
│ Nginx/Web Server │
│ │
│ Receives request │
│ server_name _; │ (Accept ALL domains)
│ │
│ Proxy to FastAPI │
│ with Host header │
└──────────┬───────────┘
│ proxy_set_header Host $host
┌─────────────────────────────────────────────────────────┐
│ FastAPI Application │
│ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ Vendor Context Middleware │ │
│ │ │ │
│ │ host = "customdomain1.com" │ │
│ │ │ │
│ │ Step 1: Is it a custom domain? │ │
│ │ not host.endswith("platform.com") → YES │ │
│ │ │ │
│ │ Step 2: Query vendor_domains table │ │
│ │ SELECT * FROM vendor_domains │ │
│ │ WHERE domain = 'customdomain1.com' │ │
│ │ AND is_active = true │ │
│ │ AND is_verified = true │ │
│ │ │ │
│ │ Result: vendor_id = 1 │ │
│ │ │ │
│ │ Step 3: Load Vendor 1 │ │
│ │ SELECT * FROM vendors WHERE id = 1 │ │
│ │ │ │
│ │ Step 4: Set request state │ │
│ │ request.state.vendor = Vendor(id=1, ...) │ │
│ └───────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ Route Handler │ │
│ │ │ │
│ │ @router.get("/") │ │
│ │ def shop_home(request): │ │
│ │ vendor = request.state.vendor # Vendor 1 │ │
│ │ │ │
│ │ # All queries auto-scoped to Vendor 1 │ │
│ │ products = get_products(vendor.id) │ │
│ │ │ │
│ │ return render("shop.html", { │ │
│ │ "vendor": vendor, │ │
│ │ "products": products │ │
│ │ }) │ │
│ └───────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
│ HTML Response
┌──────────────────────┐
│ Customer Browser │
│ │
│ Sees: │
│ Vendor 1's shop │
│ at customdomain1.com│
└──────────────────────┘
```
### Scenario 2: Customer visits vendor1.platform.com (subdomain)
```
Customer → DNS → Server → Nginx → FastAPI
FastAPI Middleware:
host = "vendor1.platform.com"
Step 1: Custom domain? NO (ends with .platform.com)
Step 2: Subdomain? YES
Extract "vendor1"
Query: SELECT * FROM vendors
WHERE subdomain = 'vendor1'
Result: Vendor 1
request.state.vendor = Vendor 1
Route → Render Vendor 1's shop
```
### Scenario 3: Development - localhost:8000/vendor/vendor1/
```
Customer → localhost:8000/vendor/vendor1/
FastAPI Middleware:
host = "localhost:8000"
path = "/vendor/vendor1/"
Step 1: Custom domain? NO (localhost)
Step 2: Subdomain? NO (localhost has no subdomain)
Step 3: Path-based? YES
Extract "vendor1" from path
Query: SELECT * FROM vendors
WHERE subdomain = 'vendor1'
Result: Vendor 1
request.state.vendor = Vendor 1
request.state.clean_path = "/" (strip /vendor/vendor1)
Route → Render Vendor 1's shop
```
## Database Relationships
```
┌─────────────────────────────────────────┐
│ vendors │
├─────────────────────────────────────────┤
│ id (PK) │
│ subdomain (UNIQUE) │
│ name │
│ is_active │
│ ... │
└─────────────────┬───────────────────────┘
│ One-to-Many
┌─────────┴──────────┐
│ │
↓ ↓
┌───────────────────┐ ┌─────────────────────┐
│ vendor_domains │ │ products │
├───────────────────┤ ├─────────────────────┤
│ id (PK) │ │ id (PK) │
│ vendor_id (FK) │ │ vendor_id (FK) │
│ domain (UNIQUE) │ │ name │
│ is_primary │ │ price │
│ is_active │ │ ... │
│ is_verified │ └─────────────────────┘
│ verification_token│
│ ... │
└───────────────────┘
Example Data:
vendors:
id=1, subdomain='vendor1', name='Shop Alpha'
id=2, subdomain='vendor2', name='Shop Beta'
vendor_domains:
id=1, vendor_id=1, domain='customdomain1.com', is_verified=true
id=2, vendor_id=1, domain='shop.alpha.com', is_verified=true
id=3, vendor_id=2, domain='customdomain2.com', is_verified=true
products:
id=1, vendor_id=1, name='Product A' ← Belongs to Vendor 1
id=2, vendor_id=1, name='Product B' ← Belongs to Vendor 1
id=3, vendor_id=2, name='Product C' ← Belongs to Vendor 2
```
## Middleware Decision Tree
```
[HTTP Request Received]
┌───────────────┐
│ Extract Host │
│ from headers │
└───────┬───────┘
┌─────────────────────────┐
│ Is admin request? │
│ (admin.* or /admin) │
└────┬────────────────┬───┘
│ YES │ NO
↓ │
[Skip vendor detection] │
Admin routing │
┌────────────────────────────┐
│ Does host end with │
│ .platform.com or localhost?│
└────┬───────────────────┬───┘
│ NO │ YES
│ │
↓ ↓
┌──────────────────────┐ ┌──────────────────────┐
│ CUSTOM DOMAIN │ │ Check for subdomain │
│ │ │ or path prefix │
│ Query: │ │ │
│ vendor_domains table │ │ Query: │
│ WHERE domain = host │ │ vendors table │
│ │ │ WHERE subdomain = X │
└──────────┬───────────┘ └──────────┬───────────┘
│ │
│ │
└─────────┬───────────────┘
┌─────────────────┐
│ Vendor found? │
└────┬────────┬───┘
│ YES │ NO
↓ ↓
[Set request.state.vendor] [404 or homepage]
[Continue to route handler]
```
## Full System Architecture
```
┌─────────────────────────────────────────────────────────────────────┐
│ Internet │
└────────────────────────────┬────────────────────────────────────────┘
┌───────────────────┼───────────────────┐
│ │ │
↓ ↓ ↓
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ customdomain1. │ │ vendor1. │ │ admin. │
│ com │ │ platform.com │ │ platform.com │
│ │ │ │ │ │
│ DNS → Server IP │ │ DNS → Server IP │ │ DNS → Server IP │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
└───────────────────┼───────────────────┘
┌──────────────────────────────┐
│ Cloudflare / Load Balancer │
│ (Optional) │
│ - SSL Termination │
│ - DDoS Protection │
│ - CDN │
└──────────────┬───────────────┘
┌──────────────────────────────┐
│ Nginx / Web Server │
│ │
│ server_name _; │ ← Accept ALL domains
│ proxy_pass FastAPI; │
│ proxy_set_header Host; │ ← Pass domain info
└──────────────┬───────────────┘
┌────────────────────────────────────────────────────────────────┐
│ FastAPI Application │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Middleware Stack │ │
│ │ 1. CORS │ │
│ │ 2. Vendor Context ← Detects vendor from domain │ │
│ │ 3. Auth │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Route Handlers │ │
│ │ - Shop pages (vendor-scoped) │ │
│ │ - Admin pages │ │
│ │ - API endpoints │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Database Queries │ │
│ │ All queries filtered by: │ │
│ │ WHERE vendor_id = request.state.vendor.id │ │
│ └──────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────┘
┌──────────────────────────────┐
│ PostgreSQL Database │
│ │
│ Tables: │
│ - vendors │
│ - vendor_domains ← NEW! │
│ - products │
│ - customers │
│ - orders │
└──────────────────────────────┘
```
## DNS Configuration Examples
### Vendor 1 wants to use customdomain1.com
**At Domain Registrar (GoDaddy/Namecheap/etc):**
```
Type: A
Name: @
Value: 123.45.67.89 (your server IP)
TTL: 3600
Type: A
Name: www
Value: 123.45.67.89
TTL: 3600
Type: TXT
Name: _wizamart-verify
Value: abc123xyz (verification token from your platform)
TTL: 3600
```
**After DNS propagates (5-15 mins):**
1. Customer visits customdomain1.com
2. DNS resolves to your server
3. Nginx accepts request
4. FastAPI middleware queries vendor_domains table
5. Finds vendor_id = 1
6. Shows Vendor 1's shop

View File

@@ -0,0 +1,478 @@
# Vendor Domains - Architecture Diagram
## System Architecture Overview
```
┌─────────────────────────────────────────────────────────────────┐
│ CLIENT REQUEST │
│ POST /vendors/1/domains │
│ {"domain": "myshop.com"} │
└────────────────────────────┬────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ ENDPOINT LAYER │
│ app/api/v1/admin/vendor_domains.py │
├─────────────────────────────────────────────────────────────────┤
│ │
│ @router.post("/{vendor_id}/domains") │
│ def add_vendor_domain( │
│ vendor_id: int, │
│ domain_data: VendorDomainCreate, ◄───┐ │
│ db: Session, │ │
│ current_admin: User │ │
│ ): │ │
│ domain = vendor_domain_service │ │
│ .add_domain(...) │ │
│ return VendorDomainResponse(...) │ │
│ │ │
└─────────────────────┬───────────────────────┼───────────────────┘
│ │
│ │
┌────────────▼──────────┐ ┌────────▼─────────┐
│ Pydantic Validation │ │ Authentication │
│ (Auto by FastAPI) │ │ Dependency │
└────────────┬──────────┘ └──────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ SERVICE LAYER │
│ app/services/vendor_domain_service.py │
├─────────────────────────────────────────────────────────────────┤
│ │
│ class VendorDomainService: │
│ │
│ def add_domain(db, vendor_id, domain_data): │
│ ┌─────────────────────────────────────┐ │
│ │ 1. Verify vendor exists │ │
│ │ 2. Check domain limit │ │
│ │ 3. Validate domain format │ │
│ │ 4. Check uniqueness │ │
│ │ 5. Handle primary domain logic │ │
│ │ 6. Create database record │ │
│ │ 7. Generate verification token │ │
│ └─────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ Raises Custom Exceptions │ │
│ │ - VendorNotFoundException │ │
│ │ - DomainAlreadyExistsException │ │
│ │ - MaxDomainsReachedException │ │
│ └─────────────────────────────────────┘ │
│ │
└─────────────────────┬───────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ DATABASE LAYER │
│ models/database/vendor_domain.py │
├─────────────────────────────────────────────────────────────────┤
│ │
│ class VendorDomain(Base): │
│ id: int │
│ vendor_id: int (FK) │
│ domain: str (unique) │
│ is_primary: bool │
│ is_active: bool │
│ is_verified: bool │
│ verification_token: str │
│ ssl_status: str │
│ ... │
│ │
└─────────────────────┬───────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ DATABASE │
│ PostgreSQL / MySQL │
└─────────────────────────────────────────────────────────────────┘
```
## Request Flow Diagram
```
┌──────────┐
│ Client │
└────┬─────┘
│ POST /vendors/1/domains
│ {"domain": "myshop.com", "is_primary": true}
┌────────────────────────────────────────────┐
│ FastAPI Router │
│ ┌──────────────────────────────────────┐ │
│ │ 1. URL Routing │ │
│ │ 2. Pydantic Validation │ │
│ │ 3. Dependency Injection │ │
│ │ - get_db() │ │
│ │ - get_current_admin_user() │ │
│ └──────────────────────────────────────┘ │
└────────────────┬───────────────────────────┘
┌────────────────────────────────────────────┐
│ Endpoint Function │
│ add_vendor_domain() │
│ │
│ ✓ Receives validated data │
│ ✓ Has DB session │
│ ✓ Has authenticated admin user │
│ ✓ Calls service layer │
│ ✓ Returns response model │
└────────────────┬───────────────────────────┘
┌────────────────────────────────────────────┐
│ Service Layer │
│ vendor_domain_service.add_domain() │
│ │
│ Business Logic: │
│ ┌──────────────────────────────────────┐ │
│ │ Vendor Validation │ │
│ │ ├─ Check vendor exists │ │
│ │ └─ Get vendor object │ │
│ │ │ │
│ │ Limit Checking │ │
│ │ ├─ Count existing domains │ │
│ │ └─ Enforce max limit │ │
│ │ │ │
│ │ Domain Validation │ │
│ │ ├─ Normalize format │ │
│ │ ├─ Check reserved subdomains │ │
│ │ └─ Validate regex pattern │ │
│ │ │ │
│ │ Uniqueness Check │ │
│ │ └─ Query existing domains │ │
│ │ │ │
│ │ Primary Domain Logic │ │
│ │ └─ Unset other primary domains │ │
│ │ │ │
│ │ Create Record │ │
│ │ ├─ Generate verification token │ │
│ │ ├─ Set initial status │ │
│ │ └─ Create VendorDomain object │ │
│ │ │ │
│ │ Database Transaction │ │
│ │ ├─ db.add() │ │
│ │ ├─ db.commit() │ │
│ │ └─ db.refresh() │ │
│ └──────────────────────────────────────┘ │
└────────────────┬───────────────────────────┘
┌────────────────────────────────────────────┐
│ Database │
│ INSERT INTO vendor_domains ... │
└────────────────┬───────────────────────────┘
┌────────────────────────────────────────────┐
│ Return to Endpoint │
│ ← VendorDomain object │
└────────────────┬───────────────────────────┘
┌────────────────────────────────────────────┐
│ Endpoint Response │
│ VendorDomainResponse( │
│ id=1, │
│ domain="myshop.com", │
│ is_verified=False, │
│ verification_token="abc123...", │
│ ... │
│ ) │
└────────────────┬───────────────────────────┘
┌────────────────────────────────────────────┐
│ FastAPI Serialization │
│ Convert to JSON │
└────────────────┬───────────────────────────┘
┌────────────────────────────────────────────┐
│ HTTP Response (201 Created) │
│ { │
│ "id": 1, │
│ "domain": "myshop.com", │
│ "is_verified": false, │
│ "verification_token": "abc123...", │
│ ... │
│ } │
└────────────────┬───────────────────────────┘
┌──────────┐
│ Client │
└──────────┘
```
## Error Handling Flow
```
┌──────────┐
│ Client │
└────┬─────┘
│ POST /vendors/1/domains
│ {"domain": "existing.com"}
┌────────────────────────────────────────────┐
│ Service Layer │
│ │
│ def add_domain(...): │
│ if self._domain_exists(db, domain): │
│ raise VendorDomainAlready │
│ ExistsException( │
│ domain="existing.com", │
│ existing_vendor_id=2 │
│ ) │
└────────────────┬───────────────────────────┘
│ Exception raised
┌────────────────────────────────────────────┐
│ Exception Handler │
│ app/exceptions/handler.py │
│ │
│ @app.exception_handler(WizamartException) │
│ async def custom_exception_handler(...): │
│ return JSONResponse( │
│ status_code=exc.status_code, │
│ content=exc.to_dict() │
│ ) │
└────────────────┬───────────────────────────┘
┌────────────────────────────────────────────┐
│ HTTP Response (409 Conflict) │
│ { │
│ "error_code": "VENDOR_DOMAIN_ │
│ ALREADY_EXISTS", │
│ "message": "Domain 'existing.com' │
│ is already registered", │
│ "status_code": 409, │
│ "details": { │
│ "domain": "existing.com", │
│ "existing_vendor_id": 2 │
│ } │
│ } │
└────────────────┬───────────────────────────┘
┌──────────┐
│ Client │
└──────────┘
```
## Component Interaction Diagram
```
┌─────────────────┐
│ Endpoints │ ◄─── HTTP Requests from client
│ (HTTP Layer) │ ───► HTTP Responses to client
└────────┬────────┘
│ Calls
┌─────────────────┐
│ Service │ ◄─── Business logic
│ Layer │ ───► Returns domain objects
└────────┬────────┘ or raises exceptions
│ Uses
├──────────────────┐
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ Database │ │ Exceptions │
│ Models │ │ (Custom) │
└─────────────────┘ └─────────────────┘
│ │
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ SQLAlchemy │ │ Exception │
│ ORM │ │ Handler │
└─────────────────┘ └─────────────────┘
│ │
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ Database │ │ JSON Error │
│ (PostgreSQL) │ │ Response │
└─────────────────┘ └─────────────────┘
```
## Data Flow for Domain Verification
```
Step 1: Add Domain
┌──────────┐
│ Admin │ POST /vendors/1/domains
└────┬─────┘ {"domain": "myshop.com"}
┌────────────────────────────────────┐
│ System creates domain record │
│ - domain: "myshop.com" │
│ - is_verified: false │
│ - verification_token: "abc123..." │
└────────────────────────────────────┘
Step 2: Get Instructions
┌──────────┐
│ Admin │ GET /domains/1/verification-instructions
└────┬─────┘
┌────────────────────────────────────┐
│ System returns instructions: │
│ "Add TXT record: │
│ _wizamart-verify.myshop.com │
│ Value: abc123..." │
└────────────────────────────────────┘
Step 3: Vendor Adds DNS Record
┌──────────┐
│ Vendor │ Adds TXT record at DNS provider
└────┬─────┘
┌────────────────────────────────────┐
│ DNS Provider (GoDaddy/etc) │
│ _wizamart-verify.myshop.com TXT │
│ "abc123..." │
└────────────────────────────────────┘
Step 4: Verify Domain
┌──────────┐
│ Admin │ POST /domains/1/verify
└────┬─────┘
┌────────────────────────────────────┐
│ System: │
│ 1. Queries DNS for TXT record │
│ 2. Checks token matches │
│ 3. Updates domain: │
│ - is_verified: true │
│ - verified_at: now() │
└────────────────────────────────────┘
Step 5: Activate Domain
┌──────────┐
│ Admin │ PUT /domains/1 {"is_active": true}
└────┬─────┘
┌────────────────────────────────────┐
│ System activates domain: │
│ - is_active: true │
│ - Domain now routes to vendor │
└────────────────────────────────────┘
Result: Domain Active!
┌──────────────┐
│ Customer │ Visits https://myshop.com
└──────┬───────┘
┌────────────────────────────────────┐
│ Middleware detects custom domain │
│ Routes to Vendor 1 │
└────────────────────────────────────┘
```
## File Structure Visual
```
project/
├── app/
│ ├── api/
│ │ └── v1/
│ │ └── admin/
│ │ ├── vendors.py ✓ Existing (reference)
│ │ └── vendor_domains.py ★ NEW (endpoints)
│ │
│ ├── services/
│ │ ├── vendor_service.py ✓ Existing (reference)
│ │ └── vendor_domain_service.py ★ NEW (business logic)
│ │
│ └── exceptions/
│ ├── __init__.py ✓ UPDATE (add exports)
│ ├── base.py ✓ Existing
│ ├── auth.py ✓ Existing
│ ├── admin.py ✓ Existing
│ └── vendor_domain.py ★ NEW (custom exceptions)
└── models/
├── schema/
│ ├── vendor.py ✓ Existing
│ └── vendor_domain.py ★ NEW (pydantic schemas)
└── database/
├── vendor.py ✓ UPDATE (add domains relationship)
└── vendor_domain.py ✓ Existing (database model)
Legend:
★ NEW - Files to create
✓ Existing - Files already exist
✓ UPDATE - Files to modify
```
## Separation of Concerns Visual
```
┌─────────────────────────────────────────────────────────────┐
│ ENDPOINT LAYER │
│ - HTTP request/response │
│ - FastAPI decorators │
│ - Dependency injection │
│ - Response models │
│ - Documentation │
│ │
│ ✓ No business logic │
│ ✓ No database operations │
│ ✓ No validation (handled by Pydantic) │
└──────────────────────┬──────────────────────────────────────┘
│ Calls
┌──────────────────────▼──────────────────────────────────────┐
│ SERVICE LAYER │
│ - Business logic │
│ - Database operations │
│ - Transaction management │
│ - Error handling │
│ - Validation logic │
│ - Logging │
│ │
│ ✓ Reusable methods │
│ ✓ Unit testable │
│ ✓ No HTTP concerns │
└──────────────────────┬──────────────────────────────────────┘
│ Uses
┌──────────────────────▼──────────────────────────────────────┐
│ DATABASE LAYER │
│ - SQLAlchemy models │
│ - Table definitions │
│ - Relationships │
│ - Database constraints │
│ │
│ ✓ Pure data models │
│ ✓ No business logic │
└─────────────────────────────────────────────────────────────┘
```
This architecture ensures:
- ✅ Clean separation of concerns
- ✅ Easy to test each layer
- ✅ Reusable business logic
- ✅ Maintainable codebase
- ✅ Follows SOLID principles

View File

@@ -0,0 +1,448 @@
# 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/*`)
- Vendor dashboard pages (`/vendor/*`)
- Shop pages (`/shop/*` or custom domains)
This middleware layer is **system-wide** and enables the multi-tenant architecture to function seamlessly.
## Middleware Components
### 1. 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. Vendor Context Middleware
**Purpose**: Detect which vendor's shop the request is for (multi-tenant core)
**What it does**:
- Detects vendor from:
- Custom domain (e.g., `customdomain.com`)
- Subdomain (e.g., `vendor1.platform.com`)
- Path prefix (e.g., `/vendor/vendor1/` or `/vendors/vendor1/`)
- Queries database to find vendor by domain or code
- Injects vendor object into `request.state.vendor`
- Extracts "clean path" (path without vendor prefix)
- Sets `request.state.clean_path` for routing
**Example**:
```
Request: https://wizamart.platform.com/shop/products
Middleware detects: vendor_code = "wizamart"
Queries database: SELECT * FROM vendors WHERE code = 'wizamart'
Injects: request.state.vendor = <Vendor object>
request.state.vendor_id = 1
request.state.clean_path = "/shop/products"
```
**Why it's critical**: Without this, the system wouldn't know which vendor's data to show
**See**: [Multi-Tenant System](multi-tenant.md) for routing modes
### 3. Path Rewrite Middleware
**Purpose**: Rewrite request paths for proper FastAPI routing
**What it does**:
- Uses the `clean_path` extracted by VendorContextMiddleware
- Rewrites `request.scope['path']` to remove vendor prefix
- Allows FastAPI routes to match correctly
**Example**:
```
Original path: /vendor/WIZAMART/shop/products
Clean path: /shop/products (set by VendorContextMiddleware)
Path Rewrite Middleware changes request path to: /shop/products
FastAPI router can now match: @app.get("/shop/products")
```
**Why it's needed**: FastAPI routes don't include vendor prefix, so we strip it
### 4. Context Detection Middleware
**Purpose**: Determine the type/context of the request
**What it does**:
- Analyzes the request path (using clean_path)
- Determines which interface is being accessed:
- `API` - `/api/*` paths
- `ADMIN` - `/admin/*` paths or `admin.*` subdomain
- `VENDOR_DASHBOARD` - `/vendor/*` paths (management area)
- `SHOP` - Storefront pages (has vendor + not admin/vendor/API)
- `FALLBACK` - Unknown context
- Injects `request.state.context_type`
**Detection Rules**:
```python
if path.startswith("/api/"):
context = API
elif path.startswith("/admin/") or host.startswith("admin."):
context = ADMIN
elif path.startswith("/vendor/"):
context = VENDOR_DASHBOARD
elif request.state.vendor exists:
context = SHOP
else:
context = FALLBACK
```
**Why it's useful**: Error handlers and templates adapt based on context
### 5. Theme Context Middleware
**Purpose**: Load vendor-specific theme settings
**What it does**:
- Checks if request has a vendor (from VendorContextMiddleware)
- Queries database for vendor's theme settings
- Injects theme configuration into `request.state.theme`
- Provides default theme if vendor has no custom theme
**Theme Data Structure**:
```python
{
"primary_color": "#3B82F6",
"secondary_color": "#10B981",
"logo_url": "/static/vendors/wizamart/logo.png",
"favicon_url": "/static/vendors/wizamart/favicon.ico",
"custom_css": "/* vendor-specific styles */"
}
```
**Why it's needed**: Each vendor shop can have custom branding
## Middleware Execution Order
### The Stack (First to Last)
```mermaid
graph TD
A[Client Request] --> B[1. LoggingMiddleware]
B --> C[2. VendorContextMiddleware]
C --> D[3. PathRewriteMiddleware]
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. **VendorContextMiddleware second**
- Must run before PathRewriteMiddleware (provides clean_path)
- Must run before ContextDetectionMiddleware (provides vendor)
- Must run before ThemeContextMiddleware (provides vendor_id)
3. **PathRewriteMiddleware third**
- Depends on clean_path from VendorContextMiddleware
- Must run before ContextDetectionMiddleware (rewrites path)
4. **ContextDetectionMiddleware fourth**
- Uses clean_path from VendorContextMiddleware
- Uses rewritten path from PathRewriteMiddleware
- Provides context_type for ThemeContextMiddleware
5. **ThemeContextMiddleware last**
- Depends on vendor from VendorContextMiddleware
- Depends on context_type from ContextDetectionMiddleware
**Breaking this order will break the application!**
## Request State Variables
Middleware components inject these variables into `request.state`:
| Variable | Set By | Type | Used By | Description |
|----------|--------|------|---------|-------------|
| `vendor` | VendorContextMiddleware | Vendor | Theme, Templates | Current vendor object |
| `vendor_id` | VendorContextMiddleware | int | Queries, Theme | Current vendor ID |
| `clean_path` | VendorContextMiddleware | str | PathRewrite, Context | Path without vendor prefix |
| `context_type` | ContextDetectionMiddleware | RequestContext | Theme, Error handlers | Request context enum |
| `theme` | ThemeContextMiddleware | dict | Templates | Vendor theme config |
### Using in Route Handlers
```python
from fastapi import Request
@app.get("/shop/products")
async def get_products(request: Request):
# Access vendor
vendor = request.state.vendor
vendor_id = request.state.vendor_id
# Access context
context = request.state.context_type
# Access theme
theme = request.state.theme
# Use in queries
products = db.query(Product).filter(
Product.vendor_id == vendor_id
).all()
return {"vendor": vendor.name, "products": products}
```
### Using in Templates
```jinja2
{# Access vendor #}
<h1>{{ request.state.vendor.name }}</h1>
{# Access theme #}
<style>
:root {
--primary-color: {{ request.state.theme.primary_color }};
--secondary-color: {{ request.state.theme.secondary_color }};
}
</style>
{# Access context #}
{% if request.state.context_type.value == "admin" %}
<div class="admin-badge">Admin Mode</div>
{% endif %}
```
## Request Flow Example
### Example: Shop Product Page Request
**URL**: `https://wizamart.myplatform.com/shop/products`
**Middleware Processing**:
```
1. LoggingMiddleware
↓ Starts timer
↓ Logs: "Request: GET /shop/products from 192.168.1.100"
2. VendorContextMiddleware
↓ Detects subdomain: "wizamart"
↓ Queries DB: vendor = get_vendor_by_code("wizamart")
↓ Sets: request.state.vendor = <Vendor: Wizamart>
↓ Sets: request.state.vendor_id = 1
↓ Sets: request.state.clean_path = "/shop/products"
3. PathRewriteMiddleware
↓ Path already clean (no rewrite needed for subdomain mode)
↓ request.scope['path'] = "/shop/products"
4. ContextDetectionMiddleware
↓ Analyzes path: "/shop/products"
↓ Has vendor: Yes
↓ Not admin/api/vendor dashboard
↓ Sets: request.state.context_type = RequestContext.SHOP
5. ThemeContextMiddleware
↓ Loads theme for vendor_id = 1
↓ Sets: request.state.theme = {...theme config...}
6. FastAPI Router
↓ Matches route: @app.get("/shop/products")
↓ Calls handler function
7. Route Handler
↓ Accesses: request.state.vendor_id
↓ Queries: products WHERE vendor_id = 1
↓ Renders template with vendor data
8. Response
↓ Returns HTML with vendor theme
9. LoggingMiddleware (response phase)
↓ Logs: "Response: 200 for GET /shop/products (0.143s)"
↓ Adds header: X-Process-Time: 0.143
```
## Error Handling in Middleware
Each middleware component handles errors gracefully:
### VendorContextMiddleware
- If vendor not found: Sets `request.state.vendor = None`
- If database error: Logs error, allows request to continue
- Fallback: Request proceeds without vendor context
### ContextDetectionMiddleware
- If clean_path missing: Uses original path
- If vendor missing: Defaults to FALLBACK context
- Always sets a context_type (never None)
### ThemeContextMiddleware
- If vendor 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 VendorContextMiddleware (vendor 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 vendor 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
- Vendor 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(ContextDetectionMiddleware)
app.add_middleware(VendorContextMiddleware)
```
**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.vendor_context import VendorContextManager
def test_vendor_detection_subdomain():
# Mock request
request = create_mock_request(host="wizamart.platform.com")
# Test detection
manager = VendorContextManager()
vendor = manager.detect_vendor_from_subdomain(request)
assert vendor.code == "wizamart"
```
### Integration Testing
Test the full middleware stack:
```python
def test_shop_request_flow(client):
response = client.get(
"/shop/products",
headers={"Host": "wizamart.platform.com"}
)
assert response.status_code == 200
assert "Wizamart" 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 {
"vendor": request.state.vendor.name if hasattr(request.state, 'vendor') else None,
"vendor_id": getattr(request.state, 'vendor_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 |
|-------|-------|----------|
| Vendor not detected | Wrong host header | Check domain configuration |
| Context is FALLBACK | Path doesn't match patterns | Check route prefix |
| Theme not loading | Vendor ID missing | Check VendorContextMiddleware 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

View File

@@ -0,0 +1,601 @@
# Multi-Tenant System
Complete guide to the multi-tenant architecture supporting custom domains, subdomains, and path-based routing.
## Overview
The Wizamart platform supports **three deployment modes** for multi-tenancy, allowing each vendor to have their own isolated shop while sharing the same application instance and database.
**Key Concept**: One application, multiple isolated vendor shops, each accessible via different URLs.
## The Three Routing Modes
### 1. Custom Domain Mode
**Concept**: Each vendor has their own domain pointing to the platform.
**Example**:
```
customdomain1.com → Vendor 1 Shop
anothershop.com → Vendor 2 Shop
beststore.net → Vendor 3 Shop
```
**How it works**:
1. Vendor registers a custom domain
2. Domain's DNS is configured to point to the platform
3. Platform detects vendor by matching domain in database
4. Vendor's shop is displayed with their theme/branding
**Use Case**: Professional vendors who want their own branded domain
**Configuration**:
```python
# Database: vendor_domains table
vendor_id | domain
----------|------------------
1 | customdomain1.com
2 | anothershop.com
3 | beststore.net
```
### 2. Subdomain Mode
**Concept**: Each vendor gets a subdomain of the platform domain.
**Example**:
```
vendor1.platform.com → Vendor 1 Shop
vendor2.platform.com → Vendor 2 Shop
vendor3.platform.com → Vendor 3 Shop
admin.platform.com → Admin Interface
```
**How it works**:
1. Vendor is assigned a unique code (e.g., "vendor1")
2. Subdomain is automatically available: `{code}.platform.com`
3. Platform detects vendor from subdomain prefix
4. No DNS configuration needed by vendor
**Use Case**: Easy setup, no custom domain required
**Configuration**:
```python
# Vendors table
id | code | name
---|---------|----------
1 | vendor1 | Vendor One Shop
2 | vendor2 | Vendor Two Shop
3 | vendor3 | Vendor Three Shop
```
### 3. Path-Based Mode
**Concept**: All vendors share the same domain, differentiated by URL path.
**Example**:
```
platform.com/vendor/vendor1/shop → Vendor 1 Shop
platform.com/vendor/vendor2/shop → Vendor 2 Shop
platform.com/vendors/vendor3/shop → Vendor 3 Shop (alternative)
```
**How it works**:
1. URL path includes vendor code
2. Platform extracts vendor code from path
3. Path is rewritten for routing
4. All vendors on same domain
**Use Case**: Simplest deployment, single domain certificate
**Path Patterns**:
- `/vendor/{code}/shop/*` - Storefront pages
- `/vendor/{code}/api/*` - API endpoints (if needed)
- `/vendors/{code}/shop/*` - Alternative pattern
## Routing Mode Comparison
| Feature | Custom Domain | Subdomain | Path-Based |
|---------|---------------|-----------|------------|
| **Professionalism** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ |
| **Setup Complexity** | High (DNS required) | Low (automatic) | Very Low |
| **SSL Complexity** | Medium (wildcard or per-domain) | Low (wildcard SSL) | Very Low (single cert) |
| **SEO Benefits** | Best (own domain) | Good | Limited |
| **Cost** | High (domain + SSL) | Low (wildcard SSL) | Lowest |
| **Isolation** | Best (separate domain) | Good | Good |
| **URL Appearance** | `shop.com` | `shop.platform.com` | `platform.com/vendor/shop` |
## Implementation Details
### Vendor Detection Logic
The `VendorContextMiddleware` detects vendors using this priority:
```python
def detect_vendor(request):
host = request.headers.get("host")
# 1. Try custom domain first
vendor = find_by_custom_domain(host)
if vendor:
return vendor, "custom_domain"
# 2. Try subdomain
if host != settings.platform_domain:
vendor_code = host.split('.')[0]
vendor = find_by_code(vendor_code)
if vendor:
return vendor, "subdomain"
# 3. Try path-based
path = request.url.path
if path.startswith("/vendor/") or path.startswith("/vendors/"):
vendor_code = extract_code_from_path(path)
vendor = find_by_code(vendor_code)
if vendor:
return vendor, "path_based"
return None, None
```
### Path Extraction
For path-based routing, clean paths are extracted:
**Example 1**: Single vendor prefix
```
Original: /vendor/WIZAMART/shop/products
Extracted: vendor_code = "WIZAMART"
Clean: /shop/products
```
**Example 2**: Plural vendors prefix
```
Original: /vendors/WIZAMART/shop/products
Extracted: vendor_code = "WIZAMART"
Clean: /shop/products
```
**Why Clean Path?**
- FastAPI routes don't include vendor prefix
- Routes defined as: `@app.get("/shop/products")`
- Path must be rewritten to match routes
## Database Schema
### Vendors Table
```sql
CREATE TABLE vendors (
id SERIAL PRIMARY KEY,
code VARCHAR(50) UNIQUE NOT NULL, -- For subdomain/path routing
name VARCHAR(255) NOT NULL,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT NOW()
);
```
### Vendor Domains Table
```sql
CREATE TABLE vendor_domains (
id SERIAL PRIMARY KEY,
vendor_id INTEGER REFERENCES vendors(id),
domain VARCHAR(255) UNIQUE NOT NULL, -- Custom domain
is_verified BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT NOW()
);
```
**Example Data**:
```sql
-- Vendors
INSERT INTO vendors (code, name) VALUES
('wizamart', 'Wizamart Shop'),
('techstore', 'Tech Store'),
('fashionhub', 'Fashion Hub');
-- Custom Domains
INSERT INTO vendor_domains (vendor_id, domain) VALUES
(1, 'wizamart.com'),
(2, 'mytechstore.net');
```
## Deployment Scenarios
### Scenario 1: Small Platform (Path-Based)
**Setup**:
- Single domain: `myplatform.com`
- All vendors use path-based routing
- Single SSL certificate
- Simplest infrastructure
**URLs**:
```
myplatform.com/admin
myplatform.com/vendor/shop1/shop
myplatform.com/vendor/shop2/shop
myplatform.com/vendor/shop3/shop
```
**Infrastructure**:
```
[Internet] → [Single Server] → [PostgreSQL]
myplatform.com
```
### Scenario 2: Medium Platform (Subdomain)
**Setup**:
- Main domain: `myplatform.com`
- Vendors get subdomains automatically
- Wildcard SSL certificate (`*.myplatform.com`)
- Better branding for vendors
**URLs**:
```
admin.myplatform.com
shop1.myplatform.com
shop2.myplatform.com
shop3.myplatform.com
```
**Infrastructure**:
```
[Internet] → [Load Balancer] → [App Servers] → [PostgreSQL]
*.myplatform.com
```
### Scenario 3: Large Platform (Mixed Mode)
**Setup**:
- Supports all three modes
- Premium vendors get custom domains
- Regular vendors use subdomains
- Free tier uses path-based
**URLs**:
```
# Custom domains (premium)
customdomain.com → Vendor 1
anotherdomain.com → Vendor 2
# Subdomains (standard)
shop3.myplatform.com → Vendor 3
shop4.myplatform.com → Vendor 4
# Path-based (free tier)
myplatform.com/vendor/shop5/shop → Vendor 5
myplatform.com/vendor/shop6/shop → Vendor 6
```
**Infrastructure**:
```
[CDN/Load Balancer]
|
+-----------------+------------------+
| | |
[App Server 1] [App Server 2] [App Server 3]
| | |
+-----------------+------------------+
|
[PostgreSQL Cluster]
```
## DNS Configuration
### For Custom Domains
**Vendor Side**:
```
# DNS A Record
customdomain.com. A 203.0.113.10 (platform IP)
# Or CNAME
customdomain.com. CNAME myplatform.com.
```
**Platform Side**:
- Add domain to `vendor_domains` table
- Generate SSL certificate (Let's Encrypt)
- Verify domain ownership
### For Subdomains
**Platform Side**:
```
# Wildcard DNS
*.myplatform.com. A 203.0.113.10
# Or individual subdomains
shop1.myplatform.com. A 203.0.113.10
shop2.myplatform.com. A 203.0.113.10
```
**SSL Certificate**:
```bash
# Wildcard certificate
*.myplatform.com
myplatform.com
```
## Tenant Isolation
### Data Isolation
Every database query is scoped to `vendor_id`:
```python
# Example: Get products for current vendor
products = db.query(Product).filter(
Product.vendor_id == request.state.vendor_id
).all()
# Example: Create order for vendor
order = Order(
vendor_id=request.state.vendor_id,
customer_id=customer_id,
# ... other fields
)
```
**Critical**: ALWAYS filter by `vendor_id` in queries!
### Theme Isolation
Each vendor has independent theme settings:
```python
# Vendor 1 theme
{
"primary_color": "#3B82F6",
"logo_url": "/static/vendors/vendor1/logo.png"
}
# Vendor 2 theme
{
"primary_color": "#10B981",
"logo_url": "/static/vendors/vendor2/logo.png"
}
```
### File Storage Isolation
Vendor files stored in separate directories:
```
static/
└── vendors/
├── vendor1/
│ ├── logo.png
│ ├── favicon.ico
│ └── products/
│ ├── product1.jpg
│ └── product2.jpg
└── vendor2/
├── logo.png
└── products/
└── product1.jpg
```
## Request Examples
### Example 1: Custom Domain Request
**Request**:
```http
GET /shop/products HTTP/1.1
Host: customdomain.com
```
**Processing**:
```
1. VendorContextMiddleware
- Checks: domain = "customdomain.com"
- Queries: vendor_domains WHERE domain = "customdomain.com"
- Finds: vendor_id = 1
- Sets: request.state.vendor = <Vendor 1>
2. ContextDetectionMiddleware
- Analyzes: path = "/shop/products"
- Sets: context_type = SHOP
3. ThemeContextMiddleware
- Queries: vendor_themes WHERE vendor_id = 1
- Sets: request.state.theme = {...}
4. Route Handler
- Queries: products WHERE vendor_id = 1
- Renders: template with Vendor 1 theme
```
### Example 2: Subdomain Request
**Request**:
```http
GET /shop/products HTTP/1.1
Host: wizamart.myplatform.com
```
**Processing**:
```
1. VendorContextMiddleware
- Checks: host != "myplatform.com"
- Extracts: subdomain = "wizamart"
- Queries: vendors WHERE code = "wizamart"
- Sets: request.state.vendor = <Vendor "wizamart">
2-4. Same as Example 1
```
### Example 3: Path-Based Request
**Request**:
```http
GET /vendor/WIZAMART/shop/products HTTP/1.1
Host: myplatform.com
```
**Processing**:
```
1. VendorContextMiddleware
- Checks: path starts with "/vendor/"
- Extracts: code = "WIZAMART"
- Queries: vendors WHERE code = "WIZAMART"
- Sets: request.state.vendor = <Vendor>
- Sets: request.state.clean_path = "/shop/products"
2. PathRewriteMiddleware
- Rewrites: request.scope['path'] = "/shop/products"
3-4. Same as previous examples
```
## Testing Multi-Tenancy
### Unit Tests
```python
def test_vendor_detection_custom_domain():
request = MockRequest(host="customdomain.com")
middleware = VendorContextMiddleware()
vendor, mode = middleware.detect_vendor(request, db)
assert vendor.code == "vendor1"
assert mode == "custom_domain"
def test_vendor_detection_subdomain():
request = MockRequest(host="shop1.platform.com")
middleware = VendorContextMiddleware()
vendor, mode = middleware.detect_vendor(request, db)
assert vendor.code == "shop1"
assert mode == "subdomain"
```
### Integration Tests
```python
def test_shop_page_multi_tenant(client):
# Test subdomain routing
response = client.get(
"/shop/products",
headers={"Host": "wizamart.platform.com"}
)
assert "Wizamart" in response.text
# Test different vendor
response = client.get(
"/shop/products",
headers={"Host": "techstore.platform.com"}
)
assert "Tech Store" in response.text
```
## Security Considerations
### 1. Tenant Isolation
**Always scope queries**:
```python
# ✅ Good - Scoped to vendor
products = db.query(Product).filter(
Product.vendor_id == request.state.vendor_id
).all()
# ❌ Bad - Not scoped, leaks data across tenants!
products = db.query(Product).all()
```
### 2. Domain Verification
Before activating custom domain:
1. Verify DNS points to platform
2. Check domain ownership (email/file verification)
3. Generate SSL certificate
4. Mark domain as verified
### 3. Input Validation
Validate vendor codes:
```python
# Sanitize vendor code
vendor_code = vendor_code.lower().strip()
# Validate format
if not re.match(r'^[a-z0-9-]{3,50}$', vendor_code):
raise ValidationError("Invalid vendor code")
```
## Performance Optimization
### 1. Cache Vendor Lookups
```python
# Cache vendor by domain/code
@lru_cache(maxsize=1000)
def get_vendor_by_code(code: str):
return db.query(Vendor).filter(Vendor.code == code).first()
```
### 2. Database Indexes
```sql
-- Index for fast lookups
CREATE INDEX idx_vendors_code ON vendors(code);
CREATE INDEX idx_vendor_domains_domain ON vendor_domains(domain);
CREATE INDEX idx_products_vendor_id ON products(vendor_id);
```
### 3. Connection Pooling
Ensure database connection pool is properly configured:
```python
# sqlalchemy engine
engine = create_engine(
DATABASE_URL,
pool_size=20,
max_overflow=40,
pool_pre_ping=True
)
```
## Related Documentation
- [Middleware Stack](middleware.md) - How vendor detection works
- [Request Flow](request-flow.md) - Complete request journey
- [Architecture Overview](overview.md) - System architecture
- [Authentication & RBAC](auth-rbac.md) - Multi-tenant security
## Migration Guide
### Adding Multi-Tenancy to Existing Tables
```python
# Alembic migration
def upgrade():
# Add vendor_id to existing table
op.add_column('products',
sa.Column('vendor_id', sa.Integer(), nullable=True)
)
# Set default vendor for existing data
op.execute("UPDATE products SET vendor_id = 1 WHERE vendor_id IS NULL")
# Make non-nullable
op.alter_column('products', 'vendor_id', nullable=False)
# Add foreign key
op.create_foreign_key(
'fk_products_vendor',
'products', 'vendors',
['vendor_id'], ['id']
)
# Add index
op.create_index('idx_products_vendor_id', 'products', ['vendor_id'])
```

View File

@@ -0,0 +1,356 @@
# System Architecture
High-level overview of the Wizamart multi-tenant e-commerce platform architecture.
## Overview
Wizamart is a **multi-tenant e-commerce platform** that supports three distinct interfaces:
- **Admin** - Platform administration and vendor management
- **Vendor** - Vendor dashboard for managing shops
- **Shop** - Customer-facing storefronts
## Technology Stack
### Backend
- **Framework**: FastAPI (Python)
- **Database**: PostgreSQL with SQLAlchemy ORM
- **Authentication**: JWT-based with bcrypt password hashing
- **API**: RESTful JSON APIs
### Frontend
- **Templates**: Jinja2 server-side rendering
- **JavaScript**: Alpine.js for reactive components
- **CSS**: Tailwind CSS
- **Icons**: Lucide Icons
### Infrastructure
- **Web Server**: Uvicorn (ASGI)
- **Middleware**: Custom middleware stack for multi-tenancy
- **Static Files**: FastAPI StaticFiles
## System Components
### 1. Multi-Tenant Routing
The platform supports three deployment modes:
#### Custom Domain Mode
```
customdomain.com → Vendor 1 Shop
anotherdomain.com → Vendor 2 Shop
```
#### Subdomain Mode
```
vendor1.platform.com → Vendor 1 Shop
vendor2.platform.com → Vendor 2 Shop
admin.platform.com → Admin Interface
```
#### Path-Based Mode
```
platform.com/vendor/vendor1/shop → Vendor 1 Shop
platform.com/vendor/vendor2/shop → Vendor 2 Shop
platform.com/admin → Admin Interface
```
**See:** [Multi-Tenant System](multi-tenant.md) for detailed implementation
### 2. Middleware Stack
Custom middleware handles:
- Vendor detection and context injection
- Request context detection (API/Admin/Vendor/Shop)
- Theme loading for vendor shops
- Request/response logging
- Path rewriting for multi-tenant routing
**See:** [Middleware Stack](middleware.md) for complete documentation
### 3. Authentication & Authorization
- JWT-based authentication
- Role-based access control (RBAC)
- Three user roles: Admin, Vendor, Customer
- Hierarchical permissions system
**See:** [Authentication & RBAC](auth-rbac.md) for details
### 4. Request Flow
```mermaid
graph TB
A[Client Request] --> B[Logging Middleware]
B --> C[Vendor Context Middleware]
C --> D[Path Rewrite Middleware]
D --> E[Context Detection Middleware]
E --> F[Theme Context Middleware]
F --> G{Request Type?}
G -->|API /api/*| H[API Router]
G -->|Admin /admin/*| I[Admin Page Router]
G -->|Vendor /vendor/*| J[Vendor Page Router]
G -->|Shop /shop/*| K[Shop Page Router]
H --> L[JSON Response]
I --> M[Admin HTML]
J --> N[Vendor HTML]
K --> O[Shop HTML]
```
**See:** [Request Flow](request-flow.md) for detailed journey
## Application Areas
### Admin Interface (`/admin/*`)
**Purpose**: Platform administration
**Features**:
- Vendor management
- User management
- System settings
- Audit logs
- Analytics dashboard
**Access**: Admin users only
### Vendor Dashboard (`/vendor/*`)
**Purpose**: Vendor shop management
**Features**:
- Product management
- Inventory tracking
- Order processing
- Shop settings
- Team member management
- Analytics
**Access**: Vendor users (owners and team members)
### Shop Interface (`/shop/*` or custom domains)
**Purpose**: Customer-facing storefront
**Features**:
- Product browsing
- Shopping cart
- Checkout
- Order tracking
- Customer account
**Access**: Public + registered customers
### API (`/api/*`)
**Purpose**: RESTful JSON API for all operations
**Features**:
- All CRUD operations
- Authentication endpoints
- Data export/import
- Webhook support
**Access**: Authenticated users based on role
## Data Architecture
### Database Schema
```
┌─────────────────┐
│ vendors │ ← Multi-tenant root
└────────┬────────┘
├─── vendor_domains
├─── vendor_themes
├─── vendor_settings
├─── products ────┬─── product_variants
│ ├─── product_images
│ └─── product_categories
├─── orders ──────┬─── order_items
│ └─── order_status_history
└─── customers ───┬─── customer_addresses
└─── customer_sessions
```
### Key Design Patterns
1. **Tenant Isolation**: All data scoped to vendor_id
2. **Soft Deletes**: Records marked as deleted, not removed
3. **Audit Trail**: All changes tracked with user and timestamp
4. **JSON Fields**: Flexible metadata storage
## Security Architecture
### Authentication Flow
```
1. User submits credentials
2. Server validates against database
3. JWT token generated with user info
4. Token returned to client
5. Client includes token in subsequent requests
6. Server validates token on each request
```
### Authorization Layers
1. **Route-level**: Middleware checks user authentication
2. **Role-level**: Decorators enforce role requirements
3. **Resource-level**: Services check ownership/permissions
4. **Tenant-level**: All queries scoped to vendor
**See:** [Authentication & RBAC](auth-rbac.md)
## Scalability Considerations
### Current Architecture
- **Single server**: Suitable for small to medium deployments
- **In-memory rate limiting**: Per-process limits
- **Session state**: Stateless JWT tokens
### Future Enhancements
- **Horizontal scaling**: Load balancer + multiple app servers
- **Redis integration**: Distributed rate limiting and caching
- **Database replication**: Read replicas for scaling
- **CDN integration**: Static asset distribution
- **Message queue**: Async task processing (Celery + Redis)
## Development Workflow
### Local Development
```bash
# Setup
make install-all
make db-setup
# Development
make dev # Start FastAPI server
make docs-serve # Start documentation server
# Testing
make test # Run all tests
make test-coverage # Run with coverage report
# Code Quality
make format # Format code (black + isort)
make lint # Run linters (ruff + mypy)
```
### Project Structure
```
project/
├── app/ # Application code
│ ├── api/ # API routes
│ ├── routes/ # Page routes (HTML)
│ ├── services/ # Business logic
│ ├── core/ # Core functionality
│ └── exceptions/ # Custom exceptions
├── middleware/ # Custom middleware
│ ├── auth.py # Authentication
│ ├── vendor_context.py # Tenant detection
│ ├── context_middleware.py # Context detection
│ └── theme_context.py # Theme loading
├── models/ # Data models
│ ├── database/ # SQLAlchemy models
│ └── schema/ # Pydantic schemas
├── static/ # Static files
│ ├── admin/ # Admin assets
│ ├── vendor/ # Vendor assets
│ └── shop/ # Shop assets
├── templates/ # Jinja2 templates
│ ├── admin/
│ ├── vendor/
│ └── shop/
├── tests/ # Test suite
│ ├── unit/
│ └── integration/
└── docs/ # Documentation
├── architecture/ # System architecture
├── frontend/ # Frontend guides
├── backend/ # Backend development
└── api/ # API documentation
```
## Monitoring & Observability
### Logging
- Structured logging with Python logging module
- Request/response logging via middleware
- Error tracking with stack traces
- Audit logging for admin actions
### Performance Monitoring
- Request timing headers (`X-Process-Time`)
- Database query monitoring (SQLAlchemy echo)
- Slow query identification
- Memory usage tracking
## Deployment Architecture
### Production Deployment
```
┌─────────────┐
Internet ───────────│ Load Balancer│
└──────┬───────┘
┌──────────────┼──────────────┐
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│ App │ │ App │ │ App │
│ Server 1│ │ Server 2│ │ Server 3│
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
└──────────────┼──────────────┘
┌──────▼───────┐
│ PostgreSQL │
│ (Primary + │
│ Replicas) │
└──────────────┘
```
**See:** [Deployment Documentation](../deployment/index.md)
## Related Documentation
- [Multi-Tenant System](multi-tenant.md) - Detailed multi-tenancy implementation
- [Middleware Stack](middleware.md) - Complete middleware documentation
- [Authentication & RBAC](auth-rbac.md) - Security and access control
- [Request Flow](request-flow.md) - Detailed request processing
- [Frontend Architecture](../frontend/overview.md) - Frontend development guides
- [Backend Development](../backend/overview.md) - Backend development guides
- [API Documentation](../api/index.md) - API reference
## Quick Links
### For Developers
- [Creating a New Admin Page](../frontend/admin/page-templates.md)
- [Backend Development Guide](../backend/overview.md)
- [Database Migrations](../development/database-migrations.md)
### For Operations
- [Deployment Guide](../deployment/production.md)
- [Environment Configuration](../deployment/environment.md)
- [Database Setup](../getting-started/database-setup.md)
### For Team Members
- [Contributing Guide](../development/contributing.md)
- [PyCharm Setup](../development/pycharm-configuration-make.md)
- [Troubleshooting](../development/troubleshooting.md)

View File

@@ -0,0 +1,564 @@
# Request Flow
Complete journey of a request through the Wizamart platform, from client to response.
## Overview
This document traces how requests flow through the multi-tenant system, showing the path through middleware, routing, and response generation.
## High-Level Flow
```mermaid
graph TB
A[Client Request] --> B{Request Type?}
B -->|API| C[REST API Flow]
B -->|HTML Page| D[Page Rendering Flow]
C --> E[Middleware Stack]
D --> E
E --> F[Vendor Detection]
F --> G[Context Detection]
G --> H[Theme Loading]
H --> I[Router]
I -->|API| J[API Handler]
I -->|Page| K[Route Handler]
J --> L[JSON Response]
K --> M[Jinja2 Template]
M --> N[HTML Response]
L --> O[Client]
N --> O
```
## Detailed Request Flow
### 1. Client Sends Request
**Example Requests**:
```http
# Shop page request (subdomain mode)
GET https://wizamart.platform.com/shop/products
Host: wizamart.platform.com
# API request
GET https://platform.com/api/v1/products?vendor_id=1
Authorization: Bearer eyJ0eXAi...
Host: platform.com
# Admin page request
GET https://platform.com/admin/vendors
Authorization: Bearer eyJ0eXAi...
Host: platform.com
```
### 2. LoggingMiddleware (Entry Point)
**What happens**:
- Request enters the application
- Timer starts
- Request logged with method, path, client IP
**Request State**:
```python
# Start time recorded
start_time = time.time()
# Log entry
logger.info(f"Request: GET /shop/products from 192.168.1.100")
```
**Output**: Nothing added to `request.state` yet
### 3. VendorContextMiddleware
**What happens**:
- Analyzes host header and path
- Determines routing mode (custom domain / subdomain / path-based)
- Queries database for vendor
- Extracts clean path
**Example Processing** (Subdomain Mode):
```python
# Input
host = "wizamart.platform.com"
path = "/shop/products"
# Detection logic
if host != settings.platform_domain:
# Subdomain detected
vendor_code = host.split('.')[0] # "wizamart"
# Query database
vendor = db.query(Vendor).filter(
Vendor.code == vendor_code
).first()
# Set request state
request.state.vendor = vendor
request.state.vendor_id = vendor.id
request.state.clean_path = "/shop/products" # Already clean
```
**Request State After**:
```python
request.state.vendor = <Vendor: Wizamart>
request.state.vendor_id = 1
request.state.clean_path = "/shop/products"
```
### 4. PathRewriteMiddleware
**What happens**:
- Checks if `clean_path` is different from original path
- If different, rewrites the request path for FastAPI routing
**Example** (Path-Based Mode):
```python
# Input (path-based mode)
original_path = "/vendor/WIZAMART/shop/products"
clean_path = "/shop/products" # From VendorContextMiddleware
# Path rewrite
if clean_path != original_path:
request.scope['path'] = clean_path
request._url = request.url.replace(path=clean_path)
```
**Request State After**: No changes to state, but internal path updated
### 5. ContextDetectionMiddleware
**What happens**:
- Analyzes the clean path
- Determines request context type
- Sets context in request state
**Detection Logic**:
```python
path = request.state.clean_path # "/shop/products"
if path.startswith("/api/"):
context = RequestContext.API
elif path.startswith("/admin/"):
context = RequestContext.ADMIN
elif path.startswith("/vendor/"):
context = RequestContext.VENDOR_DASHBOARD
elif hasattr(request.state, 'vendor') and request.state.vendor:
context = RequestContext.SHOP # ← Our example
else:
context = RequestContext.FALLBACK
request.state.context_type = context
```
**Request State After**:
```python
request.state.context_type = RequestContext.SHOP
```
### 6. ThemeContextMiddleware
**What happens**:
- Checks if request has a vendor
- Loads theme configuration from database
- Injects theme into request state
**Theme Loading**:
```python
if hasattr(request.state, 'vendor_id'):
theme = db.query(VendorTheme).filter(
VendorTheme.vendor_id == request.state.vendor_id
).first()
request.state.theme = {
"primary_color": theme.primary_color,
"secondary_color": theme.secondary_color,
"logo_url": theme.logo_url,
"custom_css": theme.custom_css
}
```
**Request State After**:
```python
request.state.theme = {
"primary_color": "#3B82F6",
"secondary_color": "#10B981",
"logo_url": "/static/vendors/wizamart/logo.png",
"custom_css": "..."
}
```
### 7. FastAPI Router
**What happens**:
- Request reaches FastAPI's router
- Router matches path to registered route
- Route dependencies are resolved
- Handler function is called
**Route Matching**:
```python
# Request path (after rewrite): "/shop/products"
# Matches this route
@app.get("/shop/products")
async def get_shop_products(request: Request):
# Handler code
pass
```
### 8. Route Handler Execution
**Example Handler**:
```python
from app.routes import shop_pages
@router.get("/shop/products")
async def shop_products_page(
request: Request,
db: Session = Depends(get_db)
):
# Access vendor from request state
vendor = request.state.vendor
vendor_id = request.state.vendor_id
# Query products for this vendor
products = db.query(Product).filter(
Product.vendor_id == vendor_id
).all()
# Render template with context
return templates.TemplateResponse(
"shop/products.html",
{
"request": request,
"vendor": vendor,
"products": products,
"theme": request.state.theme
}
)
```
### 9. Template Rendering (Jinja2)
**Template** (`templates/shop/products.html`):
```jinja2
<!DOCTYPE html>
<html>
<head>
<title>{{ vendor.name }} - Products</title>
<style>
:root {
--primary-color: {{ theme.primary_color }};
--secondary-color: {{ theme.secondary_color }};
}
</style>
</head>
<body>
<h1>{{ vendor.name }} Shop</h1>
<div class="products">
{% for product in products %}
<div class="product-card">
<h2>{{ product.name }}</h2>
<p>{{ product.price }}</p>
</div>
{% endfor %}
</div>
</body>
</html>
```
**Rendered HTML**:
```html
<!DOCTYPE html>
<html>
<head>
<title>Wizamart - Products</title>
<style>
:root {
--primary-color: #3B82F6;
--secondary-color: #10B981;
}
</style>
</head>
<body>
<h1>Wizamart Shop</h1>
<div class="products">
<div class="product-card">
<h2>Product 1</h2>
<p>$29.99</p>
</div>
<!-- More products... -->
</div>
</body>
</html>
```
### 10. Response Sent Back
**LoggingMiddleware (Response Phase)**:
- Calculates total request time
- Logs response status and duration
- Adds performance header
**Logging**:
```python
duration = time.time() - start_time # 0.143 seconds
logger.info(
f"Response: 200 for GET /shop/products (0.143s)"
)
# Add header
response.headers["X-Process-Time"] = "0.143"
```
**Final Response**:
```http
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
X-Process-Time: 0.143
Content-Length: 2847
```
## Flow Diagrams by Request Type
### API Request Flow
```mermaid
sequenceDiagram
participant Client
participant Logging
participant Vendor
participant Context
participant Router
participant Handler
participant DB
Client->>Logging: GET /api/v1/products?vendor_id=1
Logging->>Vendor: Pass request
Note over Vendor: No vendor detection<br/>(API uses query param)
Vendor->>Context: Pass request
Context->>Context: Detect API context
Note over Context: context_type = API
Context->>Router: Route request
Router->>Handler: Call API handler
Handler->>DB: Query products
DB-->>Handler: Product data
Handler-->>Router: JSON response
Router-->>Client: {products: [...]}
```
### Admin Page Flow
```mermaid
sequenceDiagram
participant Client
participant Logging
participant Vendor
participant Context
participant Theme
participant Router
participant Handler
participant Template
Client->>Logging: GET /admin/vendors
Logging->>Vendor: Pass request
Note over Vendor: No vendor<br/>(Admin area)
Vendor->>Context: Pass request
Context->>Context: Detect Admin context
Note over Context: context_type = ADMIN
Context->>Theme: Pass request
Note over Theme: Skip theme<br/>(No vendor)
Theme->>Router: Route request
Router->>Handler: Call handler
Handler->>Template: Render admin template
Template-->>Client: Admin HTML page
```
### Shop Page Flow (Full Multi-Tenant)
```mermaid
sequenceDiagram
participant Client
participant Logging
participant Vendor
participant Path
participant Context
participant Theme
participant Router
participant Handler
participant DB
participant Template
Client->>Logging: GET /shop/products<br/>Host: wizamart.platform.com
Logging->>Vendor: Pass request
Vendor->>DB: Query vendor by subdomain
DB-->>Vendor: Vendor object
Note over Vendor: Set vendor, vendor_id, clean_path
Vendor->>Path: Pass request
Note over Path: Path already clean
Path->>Context: Pass request
Context->>Context: Detect Shop context
Note over Context: context_type = SHOP
Context->>Theme: Pass request
Theme->>DB: Query theme
DB-->>Theme: Theme config
Note over Theme: Set theme in request.state
Theme->>Router: Route request
Router->>Handler: Call handler
Handler->>DB: Query products for vendor
DB-->>Handler: Product list
Handler->>Template: Render with theme
Template-->>Client: Themed shop HTML
```
## Request State Timeline
Showing how `request.state` is built up through the middleware stack:
```
Initial State: {}
After VendorContextMiddleware:
{
vendor: <Vendor: Wizamart>,
vendor_id: 1,
clean_path: "/shop/products"
}
After ContextDetectionMiddleware:
{
vendor: <Vendor: Wizamart>,
vendor_id: 1,
clean_path: "/shop/products",
context_type: RequestContext.SHOP
}
After ThemeContextMiddleware:
{
vendor: <Vendor: Wizamart>,
vendor_id: 1,
clean_path: "/shop/products",
context_type: RequestContext.SHOP,
theme: {
primary_color: "#3B82F6",
secondary_color: "#10B981",
logo_url: "/static/vendors/wizamart/logo.png",
custom_css: "..."
}
}
```
## Performance Metrics
Typical request timings:
| Component | Time | Percentage |
|-----------|------|------------|
| Middleware Stack | 5ms | 3% |
| - VendorContextMiddleware | 2ms | 1% |
| - ContextDetectionMiddleware | <1ms | <1% |
| - ThemeContextMiddleware | 2ms | 1% |
| Database Queries | 15ms | 10% |
| Business Logic | 50ms | 35% |
| Template Rendering | 75ms | 52% |
| **Total** | **145ms** | **100%** |
## Error Handling in Flow
### Middleware Errors
If middleware encounters an error:
```python
try:
# Middleware logic
vendor = detect_vendor(request)
except Exception as e:
logger.error(f"Vendor detection failed: {e}")
# Set default/None
request.state.vendor = None
# Continue to next middleware
```
### Handler Errors
If route handler raises an exception:
```python
try:
response = await handler(request)
except HTTPException as e:
# FastAPI handles HTTP exceptions
return error_response(e.status_code, e.detail)
except Exception as e:
# Custom exception handler
logger.error(f"Handler error: {e}")
return error_response(500, "Internal Server Error")
```
## Related Documentation
- [Middleware Stack](middleware.md) - Detailed middleware documentation
- [Multi-Tenant System](multi-tenant.md) - Tenant routing modes
- [Authentication & RBAC](auth-rbac.md) - Security flow
- [Architecture Overview](overview.md) - System architecture
## Debugging Request Flow
### Enable Request Logging
```python
import logging
logging.getLogger("middleware").setLevel(logging.DEBUG)
logging.getLogger("fastapi").setLevel(logging.DEBUG)
```
### Add Debug Endpoint
```python
@app.get("/debug/request-state")
async def debug_state(request: Request):
return {
"path": request.url.path,
"host": request.headers.get("host"),
"vendor": request.state.vendor.name if hasattr(request.state, 'vendor') else None,
"vendor_id": getattr(request.state, 'vendor_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,
"has_theme": bool(getattr(request.state, 'theme', None))
}
```
### Check Middleware Order
In `main.py`, middleware registration order is critical:
```python
# REVERSE order (Last In, First Out)
app.add_middleware(LoggingMiddleware) # Runs first
app.add_middleware(ThemeContextMiddleware) # Runs fifth
app.add_middleware(ContextDetectionMiddleware) # Runs fourth
app.add_middleware(VendorContextMiddleware) # Runs second
```
app.add_middleware(ThemeContextMiddleware) # Runs fifth
app.add_middleware(ContextDetectionMiddleware) # Runs fourth
app.add_middleware(VendorContextMiddleware) # Runs second
```

View File

@@ -0,0 +1,698 @@
# Multi-Theme Shop System - Complete Implementation Guide
## 🎨 Overview
This guide explains how to implement vendor-specific themes in your FastAPI multi-tenant e-commerce platform, allowing each vendor to have their own unique shop design, colors, branding, and layout.
## What You're Building
**Before:**
- All vendor shops look the same
- Same colors, fonts, layouts
- Only vendor name changes
**After:**
- Each vendor has unique theme
- Custom colors, fonts, logos
- Different layouts per vendor
- Vendor-specific branding
- CSS customization support
## Architecture Overview
```
Request → Vendor Middleware → Theme Middleware → Template Rendering
↓ ↓ ↓
Sets vendor Loads theme Applies styles
in request config for and branding
state vendor
```
### Data Flow
```
1. Customer visits: customdomain1.com
2. Vendor middleware: Identifies Vendor 1
3. Theme middleware: Loads Vendor 1's theme
4. Template receives:
- vendor: Vendor 1 object
- theme: Vendor 1 theme config
5. Template renders with:
- Vendor 1 colors
- Vendor 1 logo
- Vendor 1 layout preferences
- Vendor 1 custom CSS
```
## Implementation Steps
### Step 1: Add Theme Database Table
Create the `vendor_themes` table:
```sql
CREATE TABLE vendor_themes (
id SERIAL PRIMARY KEY,
vendor_id INTEGER UNIQUE NOT NULL REFERENCES vendors(id) ON DELETE CASCADE,
theme_name VARCHAR(100) DEFAULT 'default',
is_active BOOLEAN DEFAULT TRUE,
-- Colors (JSON)
colors JSONB DEFAULT '{
"primary": "#6366f1",
"secondary": "#8b5cf6",
"accent": "#ec4899",
"background": "#ffffff",
"text": "#1f2937",
"border": "#e5e7eb"
}'::jsonb,
-- Typography
font_family_heading VARCHAR(100) DEFAULT 'Inter, sans-serif',
font_family_body VARCHAR(100) DEFAULT 'Inter, sans-serif',
-- Branding
logo_url VARCHAR(500),
logo_dark_url VARCHAR(500),
favicon_url VARCHAR(500),
banner_url VARCHAR(500),
-- Layout
layout_style VARCHAR(50) DEFAULT 'grid',
header_style VARCHAR(50) DEFAULT 'fixed',
product_card_style VARCHAR(50) DEFAULT 'modern',
-- Customization
custom_css TEXT,
social_links JSONB DEFAULT '{}'::jsonb,
-- Meta
meta_title_template VARCHAR(200),
meta_description TEXT,
-- Timestamps
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_vendor_themes_vendor_id ON vendor_themes(vendor_id);
CREATE INDEX idx_vendor_themes_active ON vendor_themes(vendor_id, is_active);
```
### Step 2: Create VendorTheme Model
File: `models/database/vendor_theme.py`
See the complete model in `/home/claude/vendor_theme_model.py`
**Key features:**
- JSON fields for flexible color schemes
- Brand asset URLs (logo, favicon, banner)
- Layout preferences
- Custom CSS support
- CSS variables generator
- to_dict() for template rendering
### Step 3: Update Vendor Model
Add theme relationship to `models/database/vendor.py`:
```python
from sqlalchemy.orm import relationship
class Vendor(Base):
# ... existing fields ...
# Add theme relationship
theme = relationship(
"VendorTheme",
back_populates="vendor",
uselist=False, # One-to-one relationship
cascade="all, delete-orphan"
)
@property
def active_theme(self):
"""Get vendor's active theme or return None"""
if self.theme and self.theme.is_active:
return self.theme
return None
```
### Step 4: Create Theme Context Middleware
File: `middleware/theme_context.py`
See complete middleware in `/home/claude/theme_context_middleware.py`
**What it does:**
1. Runs AFTER vendor_context_middleware
2. Loads theme for detected vendor
3. Injects theme into request.state
4. Falls back to default theme if needed
**Add to main.py:**
```python
from middleware.theme_context import theme_context_middleware
# AFTER vendor_context_middleware
app.middleware("http")(theme_context_middleware)
```
### Step 5: Create Shop Base Template
File: `app/templates/shop/base.html`
See complete template in `/home/claude/shop_base_template.html`
**Key features:**
- Injects CSS variables from theme
- Vendor-specific logo (light/dark mode)
- Theme-aware header/footer
- Social links from theme config
- Custom CSS injection
- Dynamic favicon
- SEO meta tags
**Template receives:**
```python
{
"vendor": vendor_object, # From vendor middleware
"theme": theme_dict, # From theme middleware
}
```
### Step 6: Create Shop Layout JavaScript
File: `static/shop/js/shop-layout.js`
See complete code in `/home/claude/shop_layout.js`
**Provides:**
- Theme toggling (light/dark)
- Cart management
- Mobile menu
- Search overlay
- Toast notifications
- Price formatting
- Date formatting
### Step 7: Update Route Handlers
Ensure theme is passed to templates:
```python
from middleware.theme_context import get_current_theme
@router.get("/")
async def shop_home(request: Request, db: Session = Depends(get_db)):
vendor = request.state.vendor
theme = get_current_theme(request) # or request.state.theme
# Get products for vendor
products = db.query(Product).filter(
Product.vendor_id == vendor.id,
Product.is_active == True
).all()
return templates.TemplateResponse("shop/home.html", {
"request": request,
"vendor": vendor,
"theme": theme,
"products": products
})
```
**Note:** If middleware is set up correctly, theme is already in `request.state.theme`, so you may not need to explicitly pass it!
## How Themes Work
### CSS Variables System
Each theme generates CSS custom properties:
```css
:root {
--color-primary: #6366f1;
--color-secondary: #8b5cf6;
--color-accent: #ec4899;
--color-background: #ffffff;
--color-text: #1f2937;
--color-border: #e5e7eb;
--font-heading: Inter, sans-serif;
--font-body: Inter, sans-serif;
}
```
**Usage in HTML/CSS:**
```html
<!-- In templates -->
<button style="background-color: var(--color-primary)">
Click Me
</button>
<h1 style="font-family: var(--font-heading)">
Welcome
</h1>
```
```css
/* In stylesheets */
.btn-primary {
background-color: var(--color-primary);
color: var(--color-background);
}
.heading {
font-family: var(--font-heading);
color: var(--color-text);
}
```
### Theme Configuration Example
```python
# Example theme for "Modern Electronics Store"
theme = {
"theme_name": "tech-modern",
"colors": {
"primary": "#2563eb", # Blue
"secondary": "#0ea5e9", # Sky Blue
"accent": "#f59e0b", # Amber
"background": "#ffffff",
"text": "#111827",
"border": "#e5e7eb"
},
"fonts": {
"heading": "Roboto, sans-serif",
"body": "Open Sans, sans-serif"
},
"branding": {
"logo": "/media/vendors/tech-store/logo.png",
"logo_dark": "/media/vendors/tech-store/logo-dark.png",
"favicon": "/media/vendors/tech-store/favicon.ico",
"banner": "/media/vendors/tech-store/banner.jpg"
},
"layout": {
"style": "grid",
"header": "fixed",
"product_card": "modern"
},
"social_links": {
"facebook": "https://facebook.com/techstore",
"instagram": "https://instagram.com/techstore",
"twitter": "https://twitter.com/techstore"
},
"custom_css": """
.product-card {
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.product-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 12px rgba(0, 0, 0, 0.15);
}
"""
}
```
## Creating Theme Presets
You can create predefined theme templates:
```python
# app/core/theme_presets.py
THEME_PRESETS = {
"modern": {
"colors": {
"primary": "#6366f1",
"secondary": "#8b5cf6",
"accent": "#ec4899",
},
"fonts": {
"heading": "Inter, sans-serif",
"body": "Inter, sans-serif"
},
"layout": {
"style": "grid",
"header": "fixed"
}
},
"classic": {
"colors": {
"primary": "#1e40af",
"secondary": "#7c3aed",
"accent": "#dc2626",
},
"fonts": {
"heading": "Georgia, serif",
"body": "Arial, sans-serif"
},
"layout": {
"style": "list",
"header": "static"
}
},
"minimal": {
"colors": {
"primary": "#000000",
"secondary": "#404040",
"accent": "#666666",
},
"fonts": {
"heading": "Helvetica, sans-serif",
"body": "Helvetica, sans-serif"
},
"layout": {
"style": "grid",
"header": "transparent"
}
},
"vibrant": {
"colors": {
"primary": "#f59e0b",
"secondary": "#ef4444",
"accent": "#8b5cf6",
},
"fonts": {
"heading": "Poppins, sans-serif",
"body": "Open Sans, sans-serif"
},
"layout": {
"style": "masonry",
"header": "fixed"
}
}
}
def apply_preset(theme: VendorTheme, preset_name: str):
"""Apply a preset to a vendor theme"""
if preset_name not in THEME_PRESETS:
raise ValueError(f"Unknown preset: {preset_name}")
preset = THEME_PRESETS[preset_name]
theme.theme_name = preset_name
theme.colors = preset["colors"]
theme.font_family_heading = preset["fonts"]["heading"]
theme.font_family_body = preset["fonts"]["body"]
theme.layout_style = preset["layout"]["style"]
theme.header_style = preset["layout"]["header"]
return theme
```
## Admin Interface for Theme Management
Create admin endpoints for managing themes:
```python
# app/api/v1/admin/vendor_themes.py
@router.get("/vendors/{vendor_id}/theme")
def get_vendor_theme(vendor_id: int, db: Session = Depends(get_db)):
"""Get theme configuration for vendor"""
theme = db.query(VendorTheme).filter(
VendorTheme.vendor_id == vendor_id
).first()
if not theme:
# Return default theme
return get_default_theme()
return theme.to_dict()
@router.put("/vendors/{vendor_id}/theme")
def update_vendor_theme(
vendor_id: int,
theme_data: dict,
db: Session = Depends(get_db)
):
"""Update or create theme for vendor"""
theme = db.query(VendorTheme).filter(
VendorTheme.vendor_id == vendor_id
).first()
if not theme:
theme = VendorTheme(vendor_id=vendor_id)
db.add(theme)
# Update fields
if "colors" in theme_data:
theme.colors = theme_data["colors"]
if "fonts" in theme_data:
theme.font_family_heading = theme_data["fonts"].get("heading")
theme.font_family_body = theme_data["fonts"].get("body")
if "branding" in theme_data:
theme.logo_url = theme_data["branding"].get("logo")
theme.logo_dark_url = theme_data["branding"].get("logo_dark")
theme.favicon_url = theme_data["branding"].get("favicon")
if "layout" in theme_data:
theme.layout_style = theme_data["layout"].get("style")
theme.header_style = theme_data["layout"].get("header")
if "custom_css" in theme_data:
theme.custom_css = theme_data["custom_css"]
db.commit()
db.refresh(theme)
return theme.to_dict()
@router.post("/vendors/{vendor_id}/theme/preset/{preset_name}")
def apply_theme_preset(
vendor_id: int,
preset_name: str,
db: Session = Depends(get_db)
):
"""Apply a preset theme to vendor"""
from app.core.theme_presets import apply_preset, THEME_PRESETS
if preset_name not in THEME_PRESETS:
raise HTTPException(400, f"Unknown preset: {preset_name}")
theme = db.query(VendorTheme).filter(
VendorTheme.vendor_id == vendor_id
).first()
if not theme:
theme = VendorTheme(vendor_id=vendor_id)
db.add(theme)
apply_preset(theme, preset_name)
db.commit()
db.refresh(theme)
return {
"message": f"Applied {preset_name} preset",
"theme": theme.to_dict()
}
```
## Example: Different Themes for Different Vendors
### Vendor 1: Tech Electronics Store
```python
{
"colors": {
"primary": "#2563eb", # Blue
"secondary": "#0ea5e9",
"accent": "#f59e0b"
},
"fonts": {
"heading": "Roboto, sans-serif",
"body": "Open Sans, sans-serif"
},
"layout": {
"style": "grid",
"header": "fixed"
}
}
```
### Vendor 2: Fashion Boutique
```python
{
"colors": {
"primary": "#ec4899", # Pink
"secondary": "#f472b6",
"accent": "#fbbf24"
},
"fonts": {
"heading": "Playfair Display, serif",
"body": "Lato, sans-serif"
},
"layout": {
"style": "masonry",
"header": "transparent"
}
}
```
### Vendor 3: Organic Food Store
```python
{
"colors": {
"primary": "#10b981", # Green
"secondary": "#059669",
"accent": "#f59e0b"
},
"fonts": {
"heading": "Merriweather, serif",
"body": "Source Sans Pro, sans-serif"
},
"layout": {
"style": "list",
"header": "static"
}
}
```
## Testing Themes
### Test 1: View Different Vendor Themes
```bash
# Visit Vendor 1 (Tech store with blue theme)
curl http://vendor1.localhost:8000/
# Visit Vendor 2 (Fashion with pink theme)
curl http://vendor2.localhost:8000/
# Each should have different:
# - Colors in CSS variables
# - Logo
# - Fonts
# - Layout
```
### Test 2: Theme API
```bash
# Get vendor theme
curl http://localhost:8000/api/v1/admin/vendors/1/theme
# Update colors
curl -X PUT http://localhost:8000/api/v1/admin/vendors/1/theme \
-H "Content-Type: application/json" \
-d '{
"colors": {
"primary": "#ff0000",
"secondary": "#00ff00"
}
}'
# Apply preset
curl -X POST http://localhost:8000/api/v1/admin/vendors/1/theme/preset/modern
```
## Benefits
### For Platform Owner
- ✅ Premium feature for enterprise vendors
- ✅ Differentiate vendor packages (basic vs premium themes)
- ✅ Additional revenue stream
- ✅ Competitive advantage
### For Vendors
- ✅ Unique brand identity
- ✅ Professional appearance
- ✅ Better customer recognition
- ✅ Customizable to match brand
### For Customers
- ✅ Distinct shopping experiences
- ✅ Better brand recognition
- ✅ More engaging designs
- ✅ Professional appearance
## Advanced Features
### 1. Theme Preview
Allow vendors to preview themes before applying:
```python
@router.get("/vendors/{vendor_id}/theme/preview/{preset_name}")
def preview_theme(vendor_id: int, preset_name: str):
"""Generate preview URL for theme"""
# Return preview HTML with preset applied
pass
```
### 2. Theme Marketplace
Create a marketplace of premium themes:
```python
class PremiumTheme(Base):
__tablename__ = "premium_themes"
id = Column(Integer, primary_key=True)
name = Column(String(100))
description = Column(Text)
price = Column(Numeric(10, 2))
preview_image = Column(String(500))
config = Column(JSON)
```
### 3. Dark Mode Auto-Detection
Respect user's system preferences:
```javascript
// Detect system dark mode preference
if (window.matchMedia &&
window.matchMedia('(prefers-color-scheme: dark)').matches) {
this.dark = true;
}
```
### 4. Theme Analytics
Track which themes perform best:
```python
class ThemeAnalytics(Base):
__tablename__ = "theme_analytics"
theme_id = Column(Integer, ForeignKey("vendor_themes.id"))
conversion_rate = Column(Numeric(5, 2))
avg_session_duration = Column(Integer)
bounce_rate = Column(Numeric(5, 2))
```
## Summary
**What you've built:**
- ✅ Vendor-specific theme system
- ✅ CSS variables for dynamic styling
- ✅ Custom branding (logos, colors, fonts)
- ✅ Layout customization
- ✅ Custom CSS support
- ✅ Theme presets
- ✅ Admin theme management
**Each vendor now has:**
- Unique colors and fonts
- Custom logo and branding
- Layout preferences
- Social media links
- Custom CSS overrides
**All controlled by:**
- Database configuration
- No code changes needed per vendor
- Admin panel management
- Preview and testing
**Your architecture supports this perfectly!** The vendor context + theme middleware pattern works seamlessly with your existing Alpine.js frontend.
Start with the default theme, then let vendors customize their shops! 🎨

View File

@@ -0,0 +1,360 @@
# THEME PRESETS USAGE GUIDE
## What Changed in Your Presets
### ✅ What You Had Right
- Good preset structure with colors, fonts, layout
- Clean `apply_preset()` function
- Good preset names (modern, classic, minimal, vibrant)
### 🔧 What We Added
1. **Missing color fields:** `background`, `text`, `border`
2. **Missing layout field:** `product_card` style
3. **"default" preset:** Your platform's default theme
4. **Extra presets:** "elegant" and "nature" themes
5. **Helper functions:** `get_preset()`, `get_available_presets()`, `get_preset_preview()`
6. **Custom preset builder:** `create_custom_preset()`
---
## Usage Examples
### 1. Apply Preset to New Vendor
```python
from models.database.vendor_theme import VendorTheme
from app.core.theme_presets import apply_preset
from app.core.database import SessionLocal
# Create theme for vendor
db = SessionLocal()
vendor_id = 1
# Create and apply preset
theme = VendorTheme(vendor_id=vendor_id)
apply_preset(theme, "modern")
db.add(theme)
db.commit()
```
### 2. Change Vendor's Theme
```python
from models.database.vendor_theme import VendorTheme
from app.core.theme_presets import apply_preset
# Get existing theme
theme = db.query(VendorTheme).filter(
VendorTheme.vendor_id == vendor_id
).first()
if theme:
# Update to new preset
apply_preset(theme, "classic")
else:
# Create new theme
theme = VendorTheme(vendor_id=vendor_id)
apply_preset(theme, "classic")
db.add(theme)
db.commit()
```
### 3. Get Available Presets (For UI Dropdown)
```python
from app.core.theme_presets import get_available_presets, get_preset_preview
# Get list of preset names
presets = get_available_presets()
# Returns: ['default', 'modern', 'classic', 'minimal', 'vibrant', 'elegant', 'nature']
# Get preview info for UI
previews = []
for preset_name in presets:
preview = get_preset_preview(preset_name)
previews.append(preview)
# Returns list of dicts with:
# {
# "name": "modern",
# "description": "Contemporary tech-inspired design...",
# "primary_color": "#6366f1",
# "secondary_color": "#8b5cf6",
# ...
# }
```
### 4. API Endpoint to Apply Preset
```python
# In your API route
from fastapi import APIRouter, Depends
from app.core.theme_presets import apply_preset, get_available_presets
@router.put("/theme/preset")
def apply_theme_preset(
preset_name: str,
vendor: Vendor = Depends(require_vendor_context()),
db: Session = Depends(get_db),
):
"""Apply a theme preset to vendor."""
# Validate preset name
if preset_name not in get_available_presets():
raise HTTPException(
status_code=400,
detail=f"Invalid preset. Available: {get_available_presets()}"
)
# Get or create vendor theme
theme = db.query(VendorTheme).filter(
VendorTheme.vendor_id == vendor.id
).first()
if not theme:
theme = VendorTheme(vendor_id=vendor.id)
db.add(theme)
# Apply preset
apply_preset(theme, preset_name)
db.commit()
db.refresh(theme)
return {
"message": f"Theme preset '{preset_name}' applied successfully",
"theme": theme.to_dict()
}
```
### 5. Get All Presets for Theme Selector
```python
@router.get("/theme/presets")
def get_theme_presets():
"""Get all available theme presets with previews."""
from app.core.theme_presets import get_available_presets, get_preset_preview
presets = []
for preset_name in get_available_presets():
preview = get_preset_preview(preset_name)
presets.append(preview)
return {"presets": presets}
# Returns:
# {
# "presets": [
# {
# "name": "default",
# "description": "Clean and professional...",
# "primary_color": "#6366f1",
# "secondary_color": "#8b5cf6",
# "accent_color": "#ec4899",
# "heading_font": "Inter, sans-serif",
# "body_font": "Inter, sans-serif",
# "layout_style": "grid"
# },
# ...
# ]
# }
```
### 6. Create Custom Theme (Not from Preset)
```python
from app.core.theme_presets import create_custom_preset
# User provides custom colors
custom_preset = create_custom_preset(
colors={
"primary": "#ff0000",
"secondary": "#00ff00",
"accent": "#0000ff",
"background": "#ffffff",
"text": "#000000",
"border": "#cccccc"
},
fonts={
"heading": "Arial, sans-serif",
"body": "Verdana, sans-serif"
},
layout={
"style": "grid",
"header": "fixed",
"product_card": "modern"
},
name="my_custom"
)
# Apply to vendor theme
theme = VendorTheme(vendor_id=vendor_id)
theme.theme_name = "custom"
theme.colors = custom_preset["colors"]
theme.font_family_heading = custom_preset["fonts"]["heading"]
theme.font_family_body = custom_preset["fonts"]["body"]
theme.layout_style = custom_preset["layout"]["style"]
theme.header_style = custom_preset["layout"]["header"]
theme.product_card_style = custom_preset["layout"]["product_card"]
theme.is_active = True
db.add(theme)
db.commit()
```
---
## Available Presets
| Preset | Description | Primary Color | Use Case |
|--------|-------------|---------------|----------|
| `default` | Clean & professional | Indigo (#6366f1) | General purpose |
| `modern` | Tech-inspired | Indigo (#6366f1) | Tech products |
| `classic` | Traditional | Dark Blue (#1e40af) | Established brands |
| `minimal` | Ultra-clean B&W | Black (#000000) | Minimalist brands |
| `vibrant` | Bold & energetic | Orange (#f59e0b) | Creative brands |
| `elegant` | Sophisticated | Gray (#6b7280) | Luxury products |
| `nature` | Eco-friendly | Green (#059669) | Organic/eco brands |
---
## Complete Preset Structure
Each preset includes:
```python
{
"colors": {
"primary": "#6366f1", # Main brand color
"secondary": "#8b5cf6", # Supporting color
"accent": "#ec4899", # Call-to-action color
"background": "#ffffff", # Page background
"text": "#1f2937", # Text color
"border": "#e5e7eb" # Border/divider color
},
"fonts": {
"heading": "Inter, sans-serif", # Headings (h1-h6)
"body": "Inter, sans-serif" # Body text
},
"layout": {
"style": "grid", # grid | list | masonry
"header": "fixed", # fixed | static | transparent
"product_card": "modern" # modern | classic | minimal
}
}
```
---
## Integration with Admin Panel
### Theme Editor UI Flow
1. **Preset Selector**
```javascript
// Fetch available presets
fetch('/api/v1/vendor/theme/presets')
.then(r => r.json())
.then(data => {
// Display preset cards with previews
data.presets.forEach(preset => {
showPresetCard(preset.name, preset.primary_color, preset.description)
})
})
```
2. **Apply Preset Button**
```javascript
function applyPreset(presetName) {
fetch('/api/v1/vendor/theme/preset', {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({preset_name: presetName})
})
.then(() => {
alert('Theme updated!')
location.reload() // Refresh to show new theme
})
}
```
3. **Custom Color Picker** (After applying preset)
```javascript
// User can then customize colors
function updateColors(colors) {
fetch('/api/v1/vendor/theme/colors', {
method: 'PUT',
body: JSON.stringify({colors})
})
}
```
---
## Testing Presets
```python
# Test script
from app.core.theme_presets import apply_preset, get_available_presets
from models.database.vendor_theme import VendorTheme
def test_all_presets():
"""Test applying all presets"""
presets = get_available_presets()
for preset_name in presets:
theme = VendorTheme(vendor_id=999) # Test vendor
apply_preset(theme, preset_name)
assert theme.theme_name == preset_name
assert theme.colors is not None
assert theme.font_family_heading is not None
assert theme.is_active == True
print(f"✅ {preset_name} preset OK")
test_all_presets()
```
---
## CSS Variables Generation
Your middleware already handles this via `VendorTheme.to_dict()`, which includes:
```python
"css_variables": {
"--color-primary": "#6366f1",
"--color-secondary": "#8b5cf6",
"--color-accent": "#ec4899",
"--color-background": "#ffffff",
"--color-text": "#1f2937",
"--color-border": "#e5e7eb",
"--font-heading": "Inter, sans-serif",
"--font-body": "Inter, sans-serif",
}
```
Use in templates:
```html
<style>
:root {
{% for key, value in theme.css_variables.items() %}
{{ key }}: {{ value }};
{% endfor %}
}
</style>
```
---
## Next Steps
1. ✅ Copy `theme_presets.py` to `app/core/theme_presets.py`
2. ✅ Create API endpoints for applying presets
3. ✅ Build theme selector UI in admin panel
4. ✅ Test all presets work correctly
5. ✅ Add custom color picker for fine-tuning
Perfect! Your presets are now complete and production-ready! 🎨

View File

@@ -0,0 +1,419 @@
# Wizamart Multi-Tenant URL Routing Guide
## Quick Answer
**How do customers access a vendor's shop in Wizamart?**
There are three ways depending on the deployment mode:
### 1. **SUBDOMAIN MODE** (Production - Recommended)
```
https://VENDOR_SUBDOMAIN.platform.com/shop/products
Example:
https://acme.wizamart.com/shop/products
https://techpro.wizamart.com/shop/categories/electronics
```
### 2. **CUSTOM DOMAIN MODE** (Production - Premium)
```
https://VENDOR_CUSTOM_DOMAIN/shop/products
Example:
https://store.acmecorp.com/shop/products
https://shop.techpro.io/shop/cart
```
### 3. **PATH-BASED MODE** (Development Only)
```
http://localhost:PORT/vendor/VENDOR_CODE/shop/products
Example:
http://localhost:8000/vendor/acme/shop/products
http://localhost:8000/vendor/techpro/shop/checkout
```
---
## Three Deployment Modes Explained
### 1. SUBDOMAIN MODE (Production - Recommended)
**URL Pattern:** `https://VENDOR_SUBDOMAIN.platform.com/shop/...`
**Example:**
- Vendor subdomain: `acme`
- Platform domain: `wizamart.com`
- Customer Shop URL: `https://acme.wizamart.com/shop/products`
- Product Detail: `https://acme.wizamart.com/shop/products/123`
**How It Works:**
1. Customer visits `https://acme.wizamart.com/shop/products`
2. `vendor_context_middleware` detects subdomain `"acme"`
3. Queries: `SELECT * FROM vendors WHERE subdomain = 'acme'`
4. Finds Vendor with ID=1 (ACME Store)
5. Sets `request.state.vendor = Vendor(ACME Store)`
6. `context_middleware` detects it's a SHOP request
7. `theme_context_middleware` loads ACME's theme
8. Routes to `shop_pages.py``shop_products_page()`
9. Renders template with ACME's colors, logo, and products
**Advantages:**
- Single SSL certificate for all vendors (*.wizamart.com)
- Easy to manage DNS (just add subdomains)
- Customers don't need to bring their own domain
---
### 2. CUSTOM DOMAIN MODE (Production - Premium)
**URL Pattern:** `https://CUSTOM_DOMAIN/shop/...`
**Example:**
- Vendor name: "ACME Store"
- Custom domain: `store.acme-corp.com`
- Customer Shop URL: `https://store.acme-corp.com/shop/products`
**Database Setup:**
```sql
-- vendors table
id | name | subdomain
1 | ACME Store | acme
-- vendor_domains table (links custom domains to vendors)
id | vendor_id | domain | is_active | is_verified
1 | 1 | store.acme-corp.com | true | true
```
**How It Works:**
1. Customer visits `https://store.acme-corp.com/shop/products`
2. `vendor_context_middleware` detects custom domain (not *.wizamart.com, not localhost)
3. Normalizes domain to `"store.acme-corp.com"`
4. Queries: `SELECT * FROM vendor_domains WHERE domain = 'store.acme-corp.com'`
5. Finds `VendorDomain` with `vendor_id = 1`
6. Joins to get `Vendor(ACME Store)`
7. Rest is same as subdomain mode...
**Advantages:**
- Professional branding with vendor's own domain
- Better for premium vendors
- Vendor controls the domain
**Considerations:**
- Each vendor needs their own SSL certificate
- Vendor must own and configure the domain
---
### 3. PATH-BASED MODE (Development Only)
**URL Pattern:** `http://localhost:PORT/vendor/VENDOR_CODE/shop/...`
**Example:**
- Development: `http://localhost:8000/vendor/acme/shop/products`
- With port: `http://localhost:8000/vendor/acme/shop/products/123`
**How It Works:**
1. Developer visits `http://localhost:8000/vendor/acme/shop/products`
2. `vendor_context_middleware` detects path-based routing pattern `/vendor/acme/...`
3. Extracts vendor code `"acme"` from the path
4. Looks up Vendor: `SELECT * FROM vendors WHERE subdomain = 'acme'`
5. Sets `request.state.vendor = Vendor(acme)`
6. Routes to shop pages
**Advantages:**
- Perfect for local development
- No need to configure DNS/domains
- Test multiple vendors easily without domain setup
**Limitations:**
- Only for development (not production-ready)
- All vendors share same localhost address
---
## Complete Route Examples
### Subdomain/Custom Domain (PRODUCTION)
```
https://acme.wizamart.com/shop/ → Homepage
https://acme.wizamart.com/shop/products → Product Catalog
https://acme.wizamart.com/shop/products/123 → Product Detail
https://acme.wizamart.com/shop/categories/electronics → Category Page
https://acme.wizamart.com/shop/cart → Shopping Cart
https://acme.wizamart.com/shop/checkout → Checkout
https://acme.wizamart.com/shop/search?q=laptop → Search Results
https://acme.wizamart.com/shop/account/login → Customer Login
https://acme.wizamart.com/shop/account/dashboard → Account Dashboard (Auth Required)
https://acme.wizamart.com/shop/account/orders → Order History (Auth Required)
https://acme.wizamart.com/shop/account/profile → Profile (Auth Required)
```
### Path-Based (DEVELOPMENT)
```
http://localhost:8000/vendor/acme/shop/ → Homepage
http://localhost:8000/vendor/acme/shop/products → Products
http://localhost:8000/vendor/acme/shop/products/123 → Product Detail
http://localhost:8000/vendor/acme/shop/cart → Cart
http://localhost:8000/vendor/acme/shop/checkout → Checkout
http://localhost:8000/vendor/acme/shop/account/login → Login
```
### API Endpoints (Same for All Modes)
```
GET /api/v1/public/vendors/1/products → Get vendor products
GET /api/v1/public/vendors/1/products/123 → Get product details
POST /api/v1/public/vendors/1/products/{id}/reviews → Add product review
```
---
## How Vendor Isolation Works
### Multi-Layer Enforcement
**Layer 1: URL Routing**
- Vendor is detected from subdomain, custom domain, or path
- Each vendor gets their own request context
**Layer 2: Middleware**
- `request.state.vendor` is set to the detected Vendor object
- All downstream code can access the vendor
**Layer 3: Database Queries**
- All queries must include `WHERE vendor_id = ?`
- Product queries: `SELECT * FROM products WHERE vendor_id = 1`
- Order queries: `SELECT * FROM orders WHERE vendor_id = 1`
**Layer 4: API Authorization**
- Endpoints verify the vendor matches the request vendor
- Customers can only see their own vendor's products
### Example: No Cross-Vendor Leakage
```python
# Customer on acme.wizamart.com tries to access TechPro's products
# They make API call to /api/v1/public/vendors/2/products
# Backend checks:
vendor = get_vendor_from_request(request) # Returns Vendor(id=1, name="ACME")
if vendor.id != requested_vendor_id: # if 1 != 2
raise UnauthorizedShopAccessException()
```
---
## Request Lifecycle: Complete Flow
### Scenario: Customer visits `https://acme.wizamart.com/shop/products`
```
┌─────────────────────────────────────────────────────────────────┐
│ 1. REQUEST ARRIVES │
└─────────────────────────────────────────────────────────────────┘
method: GET
host: acme.wizamart.com
path: /shop/products
┌─────────────────────────────────────────────────────────────────┐
│ 2. MIDDLEWARE CHAIN │
└─────────────────────────────────────────────────────────────────┘
A) vendor_context_middleware
├─ Detects host: "acme.wizamart.com"
├─ Extracts subdomain: "acme"
├─ Queries: SELECT * FROM vendors WHERE subdomain = 'acme'
└─ Sets: request.state.vendor = Vendor(ACME Store)
B) context_middleware
├─ Checks path: "/shop/products"
├─ Has request.state.vendor? YES
└─ Sets: request.state.context_type = RequestContext.SHOP
C) theme_context_middleware
├─ Queries: SELECT * FROM vendor_themes WHERE vendor_id = 1
└─ Sets: request.state.theme = {...ACME's theme...}
┌─────────────────────────────────────────────────────────────────┐
│ 3. ROUTE MATCHING │
└─────────────────────────────────────────────────────────────────┘
Path: /shop/products
Matches: @router.get("/shop/products")
Handler: shop_products_page(request)
┌─────────────────────────────────────────────────────────────────┐
│ 4. HANDLER EXECUTES │
└─────────────────────────────────────────────────────────────────┘
@router.get("/shop/products", response_class=HTMLResponse)
async def shop_products_page(request: Request):
return templates.TemplateResponse(
"shop/products.html",
{"request": request}
)
┌─────────────────────────────────────────────────────────────────┐
│ 5. TEMPLATE RENDERS │
└─────────────────────────────────────────────────────────────────┘
Template accesses:
├─ request.state.vendor.name → "ACME Store"
├─ request.state.theme.colors.primary → "#FF6B6B"
├─ request.state.theme.branding.logo → "acme-logo.png"
└─ Products will load via JavaScript API call
┌─────────────────────────────────────────────────────────────────┐
│ 6. JAVASCRIPT LOADS PRODUCTS (Client-Side) │
└─────────────────────────────────────────────────────────────────┘
fetch(`/api/v1/public/vendors/1/products`)
.then(data => renderProducts(data.products, {theme}))
┌─────────────────────────────────────────────────────────────────┐
│ 7. RESPONSE SENT │
└─────────────────────────────────────────────────────────────────┘
HTML with ACME's colors, logo, and products
```
---
## Theme Integration
Each vendor's shop is fully branded with their custom theme:
```python
# Theme loaded for https://acme.wizamart.com
request.state.theme = {
"theme_name": "modern",
"colors": {
"primary": "#FF6B6B",
"secondary": "#FF8787",
"accent": "#FF5252",
"background": "#ffffff",
"text": "#1f2937"
},
"branding": {
"logo": "acme-logo.png",
"favicon": "acme-favicon.ico",
"banner": "acme-banner.jpg"
},
"fonts": {
"heading": "Poppins, sans-serif",
"body": "Inter, sans-serif"
}
}
```
In Jinja2 template:
```html
<style>
:root {
--color-primary: {{ request.state.theme.colors.primary }};
--color-secondary: {{ request.state.theme.colors.secondary }};
}
</style>
<img src="{{ request.state.theme.branding.logo }}" alt="{{ request.state.vendor.name }}" />
<h1 style="font-family: {{ request.state.theme.fonts.heading }}">
Welcome to {{ request.state.vendor.name }}
</h1>
```
---
## Key Points for Understanding
### 1. Customer Perspective
- Customers just visit a URL (like any normal e-commerce site)
- They have no awareness it's a multi-tenant platform
- Each store looks completely separate and branded
### 2. Vendor Perspective
- Vendors can use a subdomain (free/standard): `acme.wizamart.com`
- Or their own custom domain (premium): `store.acme-corp.com`
- Both routes go to the exact same backend code
### 3. Developer Perspective
- The middleware layer detects which vendor is being accessed
- All business logic remains vendor-unaware
- Database queries automatically filtered by vendor
- No risk of data leakage because of multi-layer isolation
### 4. Tech Stack
- **Frontend:** Jinja2 templates + Alpine.js + Tailwind CSS
- **Backend:** FastAPI + SQLAlchemy
- **Auth:** JWT with vendor-scoped cookies
- **Database:** All tables have `vendor_id` foreign key
---
## Potential Issue: Path-Based Development Mode
⚠️ **Current Implementation Gap:**
The `vendor_context_middleware` sets `clean_path` for path-based URLs, but this isn't used for FastAPI routing.
**Problem:**
- Incoming: `GET http://localhost:8000/vendor/acme/shop/products`
- Routes registered: `@router.get("/shop/products")`
- FastAPI tries to match `/vendor/acme/shop/products` against `/shop/products`
- Result: ❌ 404 Not Found
**Solution (Recommended):**
Add a path rewriting middleware in `main.py`:
```python
async def path_rewrite_middleware(request: Request, call_next):
"""Rewrite path for path-based vendor routing in development mode."""
if hasattr(request.state, 'clean_path'):
# Replace request path for FastAPI routing
request._url = request._url.replace(path=request.state.clean_path)
return await call_next(request)
# In main.py, add after vendor_context_middleware:
app.middleware("http")(path_rewrite_middleware)
```
Or alternatively, mount the router twice:
```python
app.include_router(shop_pages.router, prefix="/shop")
app.include_router(shop_pages.router, prefix="/vendor") # Allows /vendor/* paths
```
---
## Authentication in Multi-Tenant Shop
Customer authentication uses vendor-scoped cookies:
```python
# Login sets cookie scoped to vendor's shop
Set-Cookie: customer_token=eyJ...; Path=/shop; HttpOnly; SameSite=Lax
# This prevents:
# - Tokens leaking across vendors
# - Cross-site request forgery
# - Cookie scope confusion in multi-tenant setup
```
---
## Summary Table
| Mode | URL | Use Case | SSL | DNS |
|------|-----|----------|-----|-----|
| Subdomain | `vendor.platform.com/shop` | Production (standard) | *.platform.com | Add subdomains |
| Custom Domain | `vendor-domain.com/shop` | Production (premium) | Per vendor | Vendor configures |
| Path-Based | `localhost:8000/vendor/v/shop` | Development only | None | None |
---
## Next Steps
1. **For Production:** Use subdomain or custom domain mode
2. **For Development:** Use path-based mode locally
3. **For Deployment:** Configure DNS for subdomains or custom domains
4. **For Testing:** Create test vendors with different themes
5. **For Scaling:** Consider CDN for vendor-specific assets
---
Generated: November 7, 2025
Wizamart Version: Current Development