revamping documentation
This commit is contained in:
522
docs/architecture/auth-rbac.md
Normal file
522
docs/architecture/auth-rbac.md
Normal 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)
|
||||
416
docs/architecture/diagrams/multitenant-diagrams.md
Normal file
416
docs/architecture/diagrams/multitenant-diagrams.md
Normal 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
|
||||
478
docs/architecture/diagrams/vendor-domain-diagrams.md
Normal file
478
docs/architecture/diagrams/vendor-domain-diagrams.md
Normal 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
|
||||
448
docs/architecture/middleware.md
Normal file
448
docs/architecture/middleware.md
Normal 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
|
||||
601
docs/architecture/multi-tenant.md
Normal file
601
docs/architecture/multi-tenant.md
Normal 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'])
|
||||
```
|
||||
356
docs/architecture/overview.md
Normal file
356
docs/architecture/overview.md
Normal 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)
|
||||
564
docs/architecture/request-flow.md
Normal file
564
docs/architecture/request-flow.md
Normal 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
|
||||
```
|
||||
698
docs/architecture/theme-system/overview.md
Normal file
698
docs/architecture/theme-system/overview.md
Normal 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! 🎨
|
||||
360
docs/architecture/theme-system/presets.md
Normal file
360
docs/architecture/theme-system/presets.md
Normal 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! 🎨
|
||||
419
docs/architecture/url-routing/overview.md
Normal file
419
docs/architecture/url-routing/overview.md
Normal 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
|
||||
Reference in New Issue
Block a user