compiling project documentation

This commit is contained in:
2025-10-26 19:59:53 +01:00
parent d79817f069
commit 99863ad80b
45 changed files with 24278 additions and 0 deletions

View File

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

View File

@@ -0,0 +1,489 @@
# Custom Domain Implementation Guide
## Overview
This guide explains how to implement custom domain support for your multi-tenant e-commerce platform, allowing vendors to use their own domains (e.g., `customdomain1.com`) instead of subdomains (`vendor1.platform.com`).
## Architecture Summary
### Current Routing (What You Have)
```
Development: localhost:8000/vendor/vendor1/ → Vendor 1
Production: vendor1.platform.com → Vendor 1
Admin: admin.platform.com → Admin Panel
```
### New Routing (What You're Adding)
```
Custom Domain: customdomain1.com → Vendor 1
Custom Domain: shop.mybrand.com → Vendor 2
Subdomain: vendor1.platform.com → Vendor 1 (still works!)
Path-based: localhost:8000/vendor/vendor1/ → Vendor 1 (still works!)
```
## How It Works
### 1. Domain Mapping Flow
```
Customer visits customdomain1.com
DNS routes to your server
Middleware reads Host header: "customdomain1.com"
Queries vendor_domains table: WHERE domain = 'customdomain1.com'
Finds VendorDomain record → vendor_id = 1
Loads Vendor 1 data
Sets request.state.vendor = Vendor 1
All queries automatically scoped to Vendor 1
Customer sees Vendor 1's shop
```
### 2. Priority Order
The middleware checks in this order:
1. **Custom Domain** (highest priority)
- Checks if host is NOT platform.com/localhost
- Queries `vendor_domains` table
2. **Subdomain**
- Checks if host matches `*.platform.com`
- Queries `vendors.subdomain` field
3. **Path-based** (lowest priority - dev only)
- Checks if path starts with `/vendor/`
- Queries `vendors.subdomain` field
## Implementation Steps
### Step 1: Add Database Table
Create `vendor_domains` table to store custom domain mappings:
```sql
CREATE TABLE vendor_domains (
id SERIAL PRIMARY KEY,
vendor_id INTEGER NOT NULL REFERENCES vendors(id) ON DELETE CASCADE,
domain VARCHAR(255) NOT NULL UNIQUE,
is_primary BOOLEAN DEFAULT FALSE,
is_active BOOLEAN DEFAULT TRUE,
is_verified BOOLEAN DEFAULT FALSE,
verification_token VARCHAR(100) UNIQUE,
verified_at TIMESTAMP WITH TIME ZONE,
ssl_status VARCHAR(50) DEFAULT 'pending',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_domain_active ON vendor_domains(domain, is_active);
```
**Files to create:**
- `models/database/vendor_domain.py` - Model definition
- `alembic/versions/XXX_add_vendor_domains.py` - Migration
### Step 2: Update Vendor Model
Add relationship to domains:
```python
# models/database/vendor.py
class Vendor(Base):
# ... existing fields ...
domains = relationship(
"VendorDomain",
back_populates="vendor",
cascade="all, delete-orphan"
)
```
### Step 3: Update Middleware
The middleware needs to check custom domains BEFORE subdomains.
**Key changes in `middleware/vendor_context.py`:**
```python
def detect_vendor_context(request: Request):
host = request.headers.get("host", "")
# NEW: Check if it's a custom domain
if is_custom_domain(host):
return {
"domain": normalize_domain(host),
"detection_method": "custom_domain"
}
# EXISTING: Check subdomain
if is_subdomain(host):
return {
"subdomain": extract_subdomain(host),
"detection_method": "subdomain"
}
# EXISTING: Check path
if is_path_based(request.path):
return {
"subdomain": extract_from_path(request.path),
"detection_method": "path"
}
```
### Step 4: Create Admin Interface
Create API endpoints for managing domains:
```python
POST /api/v1/admin/vendors/{vendor_id}/domains - Add domain
GET /api/v1/admin/vendors/{vendor_id}/domains - List domains
PUT /api/v1/admin/domains/{domain_id} - Update domain
DELETE /api/v1/admin/domains/{domain_id} - Remove domain
POST /api/v1/admin/domains/{domain_id}/verify - Verify ownership
```
**Files to create:**
- `app/api/v1/admin/vendor_domains.py` - API endpoints
- `app/templates/admin/vendor_domains.html` - HTML interface
### Step 5: DNS Verification
To prevent domain hijacking, verify the vendor owns the domain:
**Verification Flow:**
1. Vendor adds domain in admin panel
2. System generates verification token: `abc123xyz`
3. Vendor adds DNS TXT record:
```
Name: _letzshop-verify.customdomain1.com
Type: TXT
Value: abc123xyz
```
4. Admin clicks "Verify" button
5. System queries DNS, checks for token
6. If found, marks domain as verified
**Code:**
```python
@router.post("/domains/{domain_id}/verify")
def verify_domain(domain_id: int, db: Session):
domain = db.query(VendorDomain).get(domain_id)
# Query DNS for TXT record
txt_records = dns.resolver.resolve(
f"_letzshop-verify.{domain.domain}",
'TXT'
)
# Check if token matches
for txt in txt_records:
if txt.to_text() == domain.verification_token:
domain.is_verified = True
db.commit()
return {"message": "Verified!"}
raise HTTPException(400, "Token not found")
```
### Step 6: DNS Configuration
Vendor must configure their domain's DNS:
**Option A: Point to Platform (Simple)**
```
Type: A
Name: @
Value: 123.45.67.89 (your server IP)
Type: A
Name: www
Value: 123.45.67.89
```
**Option B: CNAME to Platform (Better)**
```
Type: CNAME
Name: @
Value: platform.com
Type: CNAME
Name: www
Value: platform.com
```
**Option C: Cloudflare Proxy (Best)**
```
Enable Cloudflare proxy → Automatic SSL + CDN
```
### Step 7: SSL/TLS Certificates
For HTTPS on custom domains, you need SSL certificates.
**Option A: Wildcard Certificate (Simplest for subdomains)**
```bash
# Only works for *.platform.com
certbot certonly --dns-cloudflare \
-d "*.platform.com" \
-d "platform.com"
```
**Option B: Let's Encrypt with Certbot (Per-domain)**
```bash
# For each custom domain
certbot certonly --webroot \
-w /var/www/html \
-d customdomain1.com \
-d www.customdomain1.com
```
**Option C: Cloudflare (Recommended)**
- Vendor uses Cloudflare for their domain
- Cloudflare provides SSL automatically
- No server-side certificate management needed
**Option D: AWS Certificate Manager**
- Use with AWS ALB/CloudFront
- Automatic certificate provisioning
- Free SSL certificates
### Step 8: Web Server Configuration
Configure Nginx/Apache to handle multiple domains:
**Nginx Configuration:**
```nginx
# /etc/nginx/sites-available/platform
server {
listen 80;
listen 443 ssl http2;
# Accept ANY domain
server_name _;
# SSL configuration
ssl_certificate /etc/letsencrypt/live/platform.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/platform.com/privkey.pem;
# Pass Host header to FastAPI
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
**Key Points:**
- `server_name _;` accepts ALL domains
- `proxy_set_header Host $host;` passes domain to FastAPI
- FastAPI middleware reads the Host header to identify vendor
## Testing Guide
### Test 1: Subdomain (Existing)
```bash
# Development
curl -H "Host: vendor1.localhost:8000" http://localhost:8000/
# Production
curl https://vendor1.platform.com/
```
### Test 2: Custom Domain (New)
```bash
# Add to /etc/hosts for testing:
127.0.0.1 customdomain1.com
# Test locally
curl -H "Host: customdomain1.com" http://localhost:8000/
# Production
curl https://customdomain1.com/
```
### Test 3: Path-based (Development)
```bash
curl http://localhost:8000/vendor/vendor1/
```
## Data Flow Example
### Example: Customer visits customdomain1.com
**Step 1: DNS Resolution**
```
customdomain1.com → 123.45.67.89 (your server)
```
**Step 2: HTTP Request**
```
GET / HTTP/1.1
Host: customdomain1.com
```
**Step 3: Nginx Proxy**
```
Nginx receives request
Passes to FastAPI with Host header intact
FastAPI receives: Host = "customdomain1.com"
```
**Step 4: Middleware Processing**
```python
# vendor_context_middleware runs
host = "customdomain1.com"
# Detect it's a custom domain
is_custom = not host.endswith("platform.com") # True
# Query database
vendor_domain = db.query(VendorDomain).filter(
VendorDomain.domain == "customdomain1.com",
VendorDomain.is_active == True,
VendorDomain.is_verified == True
).first()
# vendor_domain.vendor_id = 5
# Load vendor
vendor = db.query(Vendor).get(5)
# Set in request state
request.state.vendor = vendor # Vendor 5
```
**Step 5: Route Handler**
```python
@router.get("/")
def shop_home(request: Request):
vendor = request.state.vendor # Vendor 5
# All queries automatically filtered to Vendor 5
products = db.query(Product).filter(
Product.vendor_id == vendor.id
).all()
return templates.TemplateResponse("shop/home.html", {
"request": request,
"vendor": vendor,
"products": products
})
```
**Step 6: Response**
```
Customer sees Vendor 5's shop at customdomain1.com
```
## Security Considerations
### 1. Domain Verification
- ALWAYS verify domain ownership via DNS TXT records
- Never allow unverified domains to go live
- Prevents domain hijacking
### 2. SSL/TLS
- Require HTTPS for custom domains
- Validate SSL certificates
- Monitor certificate expiration
### 3. Vendor Isolation
- Double-check vendor_id in all queries
- Never trust user input for vendor selection
- Always use `request.state.vendor`
### 4. Rate Limiting
- Limit domain additions per vendor
- Prevent DNS verification spam
- Monitor failed verification attempts
### 5. Reserved Domains
- Block platform.com and subdomains
- Block reserved names (admin, api, www, mail)
- Validate domain format
## Common Issues & Solutions
### Issue 1: "Domain not found"
**Cause:** DNS not pointing to your server
**Solution:** Check DNS A/CNAME records
### Issue 2: SSL certificate error
**Cause:** No certificate for custom domain
**Solution:** Use Cloudflare or provision certificate
### Issue 3: Vendor not detected
**Cause:** Domain not in vendor_domains table
**Solution:** Add domain via admin panel and verify
### Issue 4: Wrong vendor loaded
**Cause:** Multiple vendors with same domain
**Solution:** Enforce unique constraint on domain column
### Issue 5: Verification fails
**Cause:** DNS TXT record not found
**Solution:** Wait for DNS propagation (5-15 minutes)
## Deployment Checklist
- [ ] Database migration applied
- [ ] Vendor model updated with domains relationship
- [ ] Middleware updated to check custom domains
- [ ] Admin API endpoints created
- [ ] Admin UI created for domain management
- [ ] DNS verification implemented
- [ ] Web server configured to accept all domains
- [ ] SSL certificate strategy decided
- [ ] Testing completed (subdomain + custom domain)
- [ ] Documentation updated
- [ ] Monitoring configured
- [ ] Rate limiting added
## Future Enhancements
### 1. Automatic SSL Provisioning
Use certbot or ACME protocol to automatically provision SSL certificates when vendor adds domain.
### 2. Domain Status Dashboard
Show vendors their domain status:
- DNS configuration status
- SSL certificate status
- Traffic analytics per domain
### 3. Multiple Domains per Vendor
Allow vendors to have multiple custom domains pointing to same shop.
### 4. Domain Transfer
Allow transferring domain from one vendor to another.
### 5. Subdomain Customization
Let vendors choose their subdomain: `mybrand.platform.com`
## Summary
**What you're adding:**
1. Database table to store domain → vendor mappings
2. Middleware logic to detect custom domains
3. Admin interface to manage domains
4. DNS verification system
5. Web server configuration for multi-domain
**What stays the same:**
- Existing subdomain routing still works
- Path-based routing still works for development
- Vendor isolation and security
- All existing functionality
**Result:**
Vendors can use their own domains while you maintain a single codebase with multi-tenant architecture!

View File

@@ -0,0 +1,400 @@
# Custom Domain Support - Executive Summary
## What You Asked For
> "I want to deploy multiple shops on multiple different domains like domain1.com/shop1, domain2.com/shop2"
## What You Currently Have
Your FastAPI multi-tenant e-commerce platform currently supports:
1. **Subdomain routing** (production): `vendor1.platform.com` → Vendor 1
2. **Path-based routing** (development): `localhost:8000/vendor/vendor1/` → Vendor 1
## What's Missing
You **cannot** currently route custom domains like:
- `customdomain1.com` → Vendor 1
- `shop.mybrand.com` → Vendor 2
## The Solution
Add a **domain mapping table** that links custom domains to vendors:
```
customdomain1.com → Vendor 1
customdomain2.com → Vendor 2
```
Your middleware checks this table BEFORE checking subdomains.
## High-Level Architecture
### Current Flow
```
Customer → vendor1.platform.com → Middleware checks subdomain → Finds Vendor 1
```
### New Flow
```
Customer → customdomain1.com → Middleware checks domain mapping → Finds Vendor 1
```
### Priority Order (after implementation)
1. **Custom domain** (check `vendor_domains` table)
2. **Subdomain** (check `vendors.subdomain` - still works!)
3. **Path-based** (development mode - still works!)
## What Changes
### ✅ Changes Required
1. **Database**: Add `vendor_domains` table
2. **Model**: Create `VendorDomain` model
3. **Middleware**: Update to check custom domains first
4. **Config**: Add `platform_domain` setting
### ❌ What Stays the Same
- Existing subdomain routing (still works!)
- Path-based development routing (still works!)
- Vendor isolation and security
- All existing functionality
- API endpoints
- Admin panel
## Implementation Overview
### Database Schema
```sql
vendor_domains
id (PK)
vendor_id (FK vendors.id)
domain (UNIQUE) - e.g., "customdomain1.com"
is_active
is_verified (prevents domain hijacking)
verification_token (for DNS verification)
```
### Middleware Logic
```python
def detect_vendor(request):
host = request.host
# NEW: Check if custom domain
if not host.endswith("platform.com"):
vendor = find_by_custom_domain(host)
if vendor:
return vendor
# EXISTING: Check subdomain
if is_subdomain(host):
vendor = find_by_subdomain(host)
return vendor
# EXISTING: Check path
if is_path_based(request.path):
vendor = find_by_path(request.path)
return vendor
```
## Real-World Example
### Vendor Setup Process
**Step 1: Admin adds domain**
- Admin logs in
- Navigates to Vendor → Domains
- Adds "customdomain1.com"
- Gets verification token: `abc123xyz`
**Step 2: Vendor configures DNS**
At their domain registrar (GoDaddy/Namecheap):
```
Type: A
Name: @
Value: 123.45.67.89 (your server IP)
Type: TXT
Name: _letzshop-verify
Value: abc123xyz
```
**Step 3: Verification**
- Wait 5-15 minutes for DNS propagation
- Admin clicks "Verify Domain"
- System checks DNS for TXT record
- Domain marked as verified ✓
**Step 4: Go Live**
- Customer visits `customdomain1.com`
- Sees Vendor 1's shop
- Everything just works!
## Technical Requirements
### Server Side
- **Nginx**: Accept all domains (`server_name _;`)
- **FastAPI**: Updated middleware
- **Database**: New table for domain mappings
### DNS Side
- Vendor points domain to your server
- A record: `@` → Your server IP
- Verification: TXT record for ownership proof
### SSL/TLS
Three options:
1. **Cloudflare** (easiest - automatic SSL)
2. **Let's Encrypt** (per-domain certificates)
3. **Wildcard** (subdomains only)
## Security
### Domain Verification
✅ Prevents domain hijacking
✅ Requires DNS TXT record
✅ Token-based verification
✅ Vendor must prove ownership
### Vendor Isolation
✅ All queries filtered by vendor_id
✅ No cross-vendor data leakage
✅ Middleware sets request.state.vendor
✅ Enforced at database level
## Benefits
### For Platform Owner (You)
- ✅ More professional offering
- ✅ Enterprise feature for premium vendors
- ✅ Competitive advantage
- ✅ Higher vendor retention
- ✅ Still maintain single codebase
### For Vendors
- ✅ Use their own brand domain
- ✅ Better SEO (own domain)
- ✅ Professional appearance
- ✅ Customer trust
- ✅ Marketing benefits
### For Customers
- ✅ Seamless experience
- ✅ Trust familiar domain
- ✅ No platform branding visible
- ✅ Better user experience
## Implementation Effort
### Minimal Changes
- **New table**: 1 table (`vendor_domains`)
- **New model**: 1 file (`vendor_domain.py`)
- **Updated middleware**: Modify existing file
- **Config**: Add 1 setting
### Time Estimate
- **Core functionality**: 4-6 hours
- **Testing**: 2-3 hours
- **Production deployment**: 2-4 hours
- **Total**: 1-2 days
### Risk Level
- **Low risk**: New feature, doesn't break existing
- **Backward compatible**: Old methods still work
- **Rollback plan**: Simple database rollback
## Testing Strategy
### Development Testing
```bash
# Add to /etc/hosts
127.0.0.1 testdomain.local
# Add test data
INSERT INTO vendor_domains (vendor_id, domain, is_verified)
VALUES (1, 'testdomain.local', true);
# Test
curl http://testdomain.local:8000/
```
### Production Testing
1. Add test domain for one vendor
2. Configure DNS
3. Verify detection works
4. Monitor logs
5. Roll out to more vendors
## Deployment Checklist
### Phase 1: Database
- [ ] Create migration
- [ ] Apply to development
- [ ] Test data insertion
- [ ] Apply to production
### Phase 2: Code
- [ ] Create model
- [ ] Update middleware
- [ ] Update config
- [ ] Deploy to development
- [ ] Test thoroughly
- [ ] Deploy to production
### Phase 3: Infrastructure
- [ ] Update Nginx config
- [ ] Test domain acceptance
- [ ] Configure SSL strategy
- [ ] Set up monitoring
### Phase 4: Launch
- [ ] Document vendor process
- [ ] Train admin team
- [ ] Test with pilot vendor
- [ ] Roll out gradually
## Monitoring & Maintenance
### Key Metrics
- Number of active custom domains
- Domain verification success rate
- Traffic per domain
- SSL certificate status
- Failed domain lookups
### Regular Tasks
- Review unverified domains (weekly)
- Check SSL certificates (monthly)
- Clean up inactive domains (quarterly)
- Monitor DNS changes (automated)
## Alternatives Considered
### ❌ Separate Deployments Per Vendor
- **Pros**: Complete isolation
- **Cons**: High maintenance, expensive, difficult to update
- **Verdict**: Not scalable
### ❌ Nginx-Only Routing
- **Pros**: No application changes
- **Cons**: Hard to manage, no database tracking, no verification
- **Verdict**: Not maintainable
### ✅ Database-Driven Domain Mapping (Recommended)
- **Pros**: Scalable, maintainable, secure, trackable
- **Cons**: Requires implementation effort
- **Verdict**: Best solution
## Success Criteria
- [ ] Can add custom domain via admin panel
- [ ] DNS verification works
- [ ] Custom domain routes to correct vendor
- [ ] Subdomain routing still works
- [ ] Path-based routing still works
- [ ] No cross-vendor data leakage
- [ ] SSL works on custom domains
- [ ] Monitoring in place
- [ ] Documentation complete
## ROI Analysis
### Costs
- Development: 1-2 days (one-time)
- Testing: 0.5 day (one-time)
- Maintenance: 1-2 hours/month
### Benefits
- Premium feature for enterprise vendors
- Higher vendor retention
- Competitive advantage
- Professional appearance
- Better vendor acquisition
### Break-Even
If even 5 vendors are willing to pay $50/month extra for custom domain feature:
- Revenue: $250/month = $3,000/year
- Cost: 2 days development ≈ $2,000
- **Break-even in ~8 months**
## Next Steps
### Immediate (This Week)
1. Review implementation guides
2. Test locally with demo domain
3. Verify approach with team
### Short-Term (This Month)
1. Implement database changes
2. Update middleware
3. Deploy to staging
4. Test with pilot vendor
### Long-Term (This Quarter)
1. Add admin UI
2. Document vendor process
3. Roll out to all vendors
4. Market as premium feature
## Resources Provided
### Documentation
1. **QUICK_START.md** - Get started in 30 minutes
2. **CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md** - Complete guide
3. **ARCHITECTURE_DIAGRAMS.md** - Visual architecture
4. **IMPLEMENTATION_CHECKLIST.md** - Step-by-step tasks
### Code Files
1. **vendor_domain_model.py** - Database model
2. **updated_vendor_context.py** - Updated middleware
3. **vendor_domains_api.py** - Admin API endpoints
4. **migration_vendor_domains.py** - Database migration
5. **config_updates.py** - Configuration changes
## Questions & Answers
**Q: Will this break existing functionality?**
A: No! Subdomain and path-based routing still work. This adds a new layer on top.
**Q: What about SSL certificates?**
A: Recommend Cloudflare (automatic) or Let's Encrypt (per-domain). See full guide.
**Q: How do vendors point their domain?**
A: They add an A record pointing to your server IP. Simple DNS configuration.
**Q: What prevents domain hijacking?**
A: DNS verification via TXT record. Vendor must prove ownership before domain goes live.
**Q: Can one vendor have multiple domains?**
A: Yes! The `vendor_domains` table supports multiple domains per vendor.
**Q: What if vendor removes domain later?**
A: Just mark as `is_active = false` in database. Easy to deactivate.
**Q: Do I need separate servers per domain?**
A: No! Single server accepts all domains. Middleware routes to correct vendor.
## Conclusion
**Is your current architecture capable?**
✅ Yes! Your multi-tenant architecture is perfect for this.
**What needs to change?**
✅ Minimal changes: 1 table, 1 model, middleware update
**Is it worth it?**
✅ Yes! Enterprise feature, competitive advantage, premium pricing
**Risk level?**
✅ Low! Backward compatible, rollback-friendly
**Implementation complexity?**
✅ Medium! 1-2 days for experienced FastAPI developer
**Recommendation:**
**GO FOR IT!** This is a valuable feature that fits naturally into your architecture.
---
**Ready to start?** Begin with `QUICK_START.md` for a 30-minute implementation!

View File

@@ -0,0 +1,466 @@
# Custom Domain Implementation Checklist
## Phase 1: Database Setup
### Step 1.1: Create VendorDomain Model
- [ ] Create file: `models/database/vendor_domain.py`
- [ ] Copy model code from `vendor_domain_model.py`
- [ ] Import in `models/database/__init__.py`
### Step 1.2: Update Vendor Model
- [ ] Open `models/database/vendor.py`
- [ ] Add `domains` relationship
- [ ] Add `primary_domain` and `all_domains` properties
### Step 1.3: Create Migration
- [ ] Generate migration: `alembic revision -m "add vendor domains"`
- [ ] Copy upgrade/downgrade code from `migration_vendor_domains.py`
- [ ] Apply migration: `alembic upgrade head`
- [ ] Verify table exists: `psql -c "\d vendor_domains"`
## Phase 2: Configuration
### Step 2.1: Update Settings
- [ ] Open `app/core/config.py`
- [ ] Add `platform_domain = "platform.com"` (change to your actual domain)
- [ ] Add custom domain settings from `config_updates.py`
- [ ] Update `.env` file with new settings
### Step 2.2: Test Settings
```bash
# In Python shell
from app.core.config import settings
print(settings.platform_domain) # Should print your domain
```
## Phase 3: Middleware Update
### Step 3.1: Backup Current Middleware
```bash
cp middleware/vendor_context.py middleware/vendor_context.py.backup
```
### Step 3.2: Update Middleware
- [ ] Open `middleware/vendor_context.py`
- [ ] Replace with code from `updated_vendor_context.py`
- [ ] Review the three detection methods (custom domain, subdomain, path)
- [ ] Check imports are correct
### Step 3.3: Test Middleware Detection
Create test file `tests/test_vendor_context.py`:
```python
def test_custom_domain_detection():
# Mock request with custom domain
request = MockRequest(host="customdomain1.com")
context = VendorContextManager.detect_vendor_context(request)
assert context["detection_method"] == "custom_domain"
assert context["domain"] == "customdomain1.com"
def test_subdomain_detection():
request = MockRequest(host="vendor1.platform.com")
context = VendorContextManager.detect_vendor_context(request)
assert context["detection_method"] == "subdomain"
assert context["subdomain"] == "vendor1"
def test_path_detection():
request = MockRequest(host="localhost", path="/vendor/vendor1/")
context = VendorContextManager.detect_vendor_context(request)
assert context["detection_method"] == "path"
assert context["subdomain"] == "vendor1"
```
Run tests:
```bash
pytest tests/test_vendor_context.py -v
```
## Phase 4: Admin API Endpoints
### Step 4.1: Create Vendor Domains Router
- [ ] Create file: `app/api/v1/admin/vendor_domains.py`
- [ ] Copy code from `vendor_domains_api.py`
- [ ] Verify imports work
### Step 4.2: Register Router
Edit `app/api/v1/admin/__init__.py`:
```python
from .vendor_domains import router as vendor_domains_router
# In your admin router setup:
admin_router.include_router(
vendor_domains_router,
prefix="/vendors",
tags=["vendor-domains"]
)
```
### Step 4.3: Test API Endpoints
```bash
# Start server
uvicorn main:app --reload
# Test endpoints (use Postman or curl)
# 1. List vendor domains
curl -H "Authorization: Bearer {admin_token}" \
http://localhost:8000/api/v1/admin/vendors/1/domains
# 2. Add domain
curl -X POST \
-H "Authorization: Bearer {admin_token}" \
-H "Content-Type: application/json" \
-d '{"vendor_id": 1, "domain": "test.com"}' \
http://localhost:8000/api/v1/admin/vendors/1/domains
```
## Phase 5: DNS Verification (Optional but Recommended)
### Step 5.1: Install DNS Library
```bash
pip install dnspython
```
### Step 5.2: Test DNS Verification
```python
# In Python shell
import dns.resolver
# Test querying TXT record
answers = dns.resolver.resolve("_letzshop-verify.example.com", "TXT")
for txt in answers:
print(txt.to_text())
```
## Phase 6: Local Testing
### Step 6.1: Test with /etc/hosts
Edit `/etc/hosts`:
```
127.0.0.1 testdomain1.local
127.0.0.1 testdomain2.local
```
### Step 6.2: Add Test Data
```sql
-- Add test vendor
INSERT INTO vendors (subdomain, name, is_active)
VALUES ('testvendor', 'Test Vendor', true);
-- Add test domain
INSERT INTO vendor_domains (vendor_id, domain, is_verified, is_active)
VALUES (1, 'testdomain1.local', true, true);
```
### Step 6.3: Test in Browser
```bash
# Start server
uvicorn main:app --reload
# Visit in browser:
# http://testdomain1.local:8000/
# Check logs for:
# "✓ Vendor found via custom domain: testdomain1.local → Test Vendor"
```
## Phase 7: Web Server Configuration
### Step 7.1: Update Nginx Configuration
Edit `/etc/nginx/sites-available/your-site`:
```nginx
server {
listen 80;
listen 443 ssl http2;
# Accept ALL domains
server_name _;
# SSL configuration (update paths)
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host; # CRITICAL: Pass domain to FastAPI
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
### Step 7.2: Test Nginx Config
```bash
sudo nginx -t
sudo systemctl reload nginx
```
## Phase 8: Production Deployment
### Step 8.1: Pre-deployment Checklist
- [ ] All tests passing
- [ ] Database migration applied
- [ ] Configuration updated in production .env
- [ ] Nginx configured to accept all domains
- [ ] SSL certificate strategy decided
- [ ] Monitoring configured
- [ ] Rollback plan ready
### Step 8.2: Deploy
```bash
# 1. Pull latest code
git pull origin main
# 2. Install dependencies
pip install -r requirements.txt
# 3. Run migrations
alembic upgrade head
# 4. Restart application
sudo systemctl restart your-app
# 5. Restart nginx
sudo systemctl reload nginx
```
### Step 8.3: Verify Deployment
```bash
# Test health endpoint
curl https://platform.com/health
# Check logs
tail -f /var/log/your-app/app.log
# Look for:
# "✓ Vendor found via custom domain: ..."
```
## Phase 9: Vendor Setup Process
### Step 9.1: Admin Adds Domain for Vendor
1. Log into admin panel
2. Go to Vendors → Select Vendor → Domains
3. Click "Add Domain"
4. Enter domain: `customdomain1.com`
5. Click Save
6. Copy verification instructions
### Step 9.2: Vendor Configures DNS
Vendor goes to their domain registrar and adds:
**A Record:**
```
Type: A
Name: @
Value: [your server IP]
TTL: 3600
```
**Verification TXT Record:**
```
Type: TXT
Name: _letzshop-verify
Value: [token from step 9.1]
TTL: 3600
```
### Step 9.3: Admin Verifies Domain
1. Wait 5-15 minutes for DNS propagation
2. In admin panel → Click "Verify Domain"
3. System checks DNS for TXT record
4. If found → Domain marked as verified
5. Domain is now active!
### Step 9.4: Test Custom Domain
```bash
# Visit vendor's custom domain
curl https://customdomain1.com/
# Should show vendor's shop
# Check server logs for confirmation
```
## Phase 10: SSL/TLS Setup
### Option A: Let's Encrypt (Per-Domain)
```bash
# For each custom domain
sudo certbot certonly --webroot \
-w /var/www/html \
-d customdomain1.com \
-d www.customdomain1.com
# Auto-renewal
sudo certbot renew --dry-run
```
### Option B: Cloudflare (Recommended)
1. Vendor adds domain to Cloudflare
2. Cloudflare provides SSL automatically
3. Points Cloudflare DNS to your server
4. No server-side certificate needed
### Option C: Wildcard (Subdomains Only)
```bash
# Only for *.platform.com
sudo certbot certonly --dns-cloudflare \
-d "*.platform.com" \
-d "platform.com"
```
## Troubleshooting Guide
### Issue: Vendor not detected
**Check:**
```sql
-- Is domain in database?
SELECT * FROM vendor_domains WHERE domain = 'customdomain1.com';
-- Is domain verified?
SELECT * FROM vendor_domains
WHERE domain = 'customdomain1.com'
AND is_verified = true
AND is_active = true;
-- Is vendor active?
SELECT v.* FROM vendors v
JOIN vendor_domains vd ON v.id = vd.vendor_id
WHERE vd.domain = 'customdomain1.com';
```
**Check logs:**
```bash
# Look for middleware debug logs
grep "Vendor context" /var/log/your-app/app.log
# Should see:
# "🏪 Vendor context: Shop Name (subdomain) via custom_domain"
```
### Issue: Wrong vendor loaded
**Check for duplicates:**
```sql
-- Should be no duplicates
SELECT domain, COUNT(*)
FROM vendor_domains
GROUP BY domain
HAVING COUNT(*) > 1;
```
### Issue: DNS verification fails
**Check DNS propagation:**
```bash
# Check if TXT record exists
dig _letzshop-verify.customdomain1.com TXT
# Should show verification token
```
### Issue: SSL certificate error
**Options:**
1. Use Cloudflare (easiest)
2. Get Let's Encrypt certificate for domain
3. Tell vendor to use their own SSL proxy
## Monitoring & Maintenance
### Add Logging
```python
# In middleware
logger.info(
f"Request received for {host}",
extra={
"host": host,
"vendor_id": vendor.id if vendor else None,
"detection_method": context.get("detection_method")
}
)
```
### Monitor Metrics
- [ ] Number of active custom domains
- [ ] Failed domain verifications
- [ ] Traffic per domain
- [ ] SSL certificate expirations
### Regular Maintenance
- [ ] Review unverified domains (> 7 days old)
- [ ] Check SSL certificate status
- [ ] Monitor DNS changes
- [ ] Clean up inactive domains
## Success Criteria
- [ ] Existing subdomain routing still works
- [ ] New custom domains can be added via admin
- [ ] DNS verification works
- [ ] Multiple domains can point to same vendor
- [ ] Middleware correctly identifies vendor
- [ ] All vendor queries properly scoped
- [ ] No security vulnerabilities (domain hijacking prevented)
- [ ] Monitoring in place
- [ ] Documentation updated
## Rollback Plan
If something goes wrong:
1. **Database:**
```bash
alembic downgrade -1 # Rollback migration
```
2. **Code:**
```bash
git checkout HEAD~1 # Revert to previous commit
sudo systemctl restart your-app
```
3. **Middleware:**
```bash
cp middleware/vendor_context.py.backup middleware/vendor_context.py
sudo systemctl restart your-app
```
4. **Nginx:**
```bash
# Restore previous nginx config from backup
sudo cp /etc/nginx/sites-available/your-site.backup /etc/nginx/sites-available/your-site
sudo systemctl reload nginx
```
## Next Steps After Implementation
1. **Create Admin UI**
- HTML page for domain management
- Show verification status
- DNS configuration help
2. **Vendor Dashboard**
- Let vendors see their domains
- Domain analytics
- SSL status
3. **Automation**
- Auto-verify domains via webhook
- Auto-provision SSL certificates
- Auto-renewal monitoring
4. **Documentation**
- Vendor help docs
- Admin guide
- API documentation
5. **Testing**
- Load testing with multiple domains
- Security audit
- Penetration testing
---
**Estimated Implementation Time:**
- Phase 1-4: 4-6 hours (core functionality)
- Phase 5-7: 2-3 hours (testing and deployment)
- Phase 8-10: 2-4 hours (production setup and SSL)
**Total: 1-2 days for full implementation**

View File

@@ -0,0 +1,460 @@
# Custom Domain Support - Complete Documentation Package
## 📋 Table of Contents
This package contains everything you need to add custom domain support to your FastAPI multi-tenant e-commerce platform.
## 🚀 Where to Start
### If you want to understand the concept first:
👉 **Start here:** [EXECUTIVE_SUMMARY.md](EXECUTIVE_SUMMARY.md)
- High-level overview
- What changes and what stays the same
- Benefits and ROI analysis
- Decision-making guidance
### If you want to implement quickly:
👉 **Start here:** [QUICK_START.md](QUICK_START.md)
- Get working in 30 minutes
- Minimal steps
- Local testing included
- Perfect for proof-of-concept
### If you want complete understanding:
👉 **Start here:** [CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md](CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md)
- Comprehensive guide
- All technical details
- Security considerations
- Production deployment
## 📚 Documentation Files
### 1. EXECUTIVE_SUMMARY.md
**What it covers:**
- Problem statement and solution
- High-level architecture
- Benefits analysis
- Risk assessment
- ROI calculation
- Q&A section
**Read this if:**
- You need to explain the feature to stakeholders
- You want to understand if it's worth implementing
- You need a business case
- You want to see the big picture
**Reading time:** 10 minutes
---
### 2. QUICK_START.md
**What it covers:**
- 5-step implementation (30 minutes)
- Database setup
- Model creation
- Middleware update
- Local testing
- Troubleshooting
**Read this if:**
- You want to test the concept quickly
- You need a working demo
- You prefer hands-on learning
- You want to validate the approach
**Implementation time:** 30 minutes
---
### 3. CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md
**What it covers:**
- Complete architecture explanation
- Step-by-step implementation
- DNS configuration
- SSL/TLS setup
- Security best practices
- Testing strategies
- Common issues and solutions
- Future enhancements
**Read this if:**
- You're doing production implementation
- You need comprehensive documentation
- You want to understand every detail
- You need reference material
**Reading time:** 45 minutes
---
### 4. ARCHITECTURE_DIAGRAMS.md
**What it covers:**
- Visual architecture diagrams
- Request flow illustrations
- Database relationship diagrams
- Before/after comparisons
- DNS configuration examples
- Decision tree diagrams
**Read this if:**
- You're a visual learner
- You need to explain to others
- You want to see data flow
- You prefer diagrams to text
**Reading time:** 20 minutes
---
### 5. IMPLEMENTATION_CHECKLIST.md
**What it covers:**
- Phase-by-phase implementation plan
- Detailed task checklist
- Testing procedures
- Deployment steps
- Troubleshooting guide
- Rollback plan
- Monitoring setup
**Read this if:**
- You're ready to implement in production
- You need a project plan
- You want to track progress
- You need a deployment guide
**Implementation time:** 1-2 days
---
## 💻 Code Files
All generated code files are in `/home/claude/`:
### Core Implementation Files
**1. vendor_domain_model.py**
- Complete `VendorDomain` SQLAlchemy model
- Domain normalization logic
- Relationships and constraints
- Ready to use in your project
**2. updated_vendor_context.py**
- Enhanced middleware with custom domain support
- Three detection methods (custom domain, subdomain, path)
- Vendor lookup logic
- Drop-in replacement for your current middleware
**3. vendor_domains_api.py**
- Admin API endpoints for domain management
- CRUD operations for domains
- DNS verification endpoint
- Verification instructions endpoint
**4. migration_vendor_domains.py**
- Alembic migration script
- Creates vendor_domains table
- All indexes and constraints
- Upgrade and downgrade functions
**5. config_updates.py**
- Configuration additions for Settings class
- Platform domain setting
- Custom domain features toggle
- DNS verification settings
**6. vendor_model_update.py**
- Updates to Vendor model
- Domains relationship
- Helper properties for domain access
---
## 🎯 Quick Navigation Guide
### I want to...
**...understand what custom domains are**
→ Read: EXECUTIVE_SUMMARY.md (Section: "What You Asked For")
**...see if my architecture can support this**
→ Read: EXECUTIVE_SUMMARY.md (Section: "Is your current architecture capable?")
→ Answer: ✅ YES! You have exactly what you need.
**...test it locally in 30 minutes**
→ Follow: QUICK_START.md
**...understand the technical architecture**
→ Read: ARCHITECTURE_DIAGRAMS.md
→ Read: CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md (Section: "How It Works")
**...implement in production**
→ Follow: IMPLEMENTATION_CHECKLIST.md
→ Reference: CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md
**...see the database schema**
→ Check: ARCHITECTURE_DIAGRAMS.md (Section: "Database Relationships")
→ Use: migration_vendor_domains.py
**...update my middleware**
→ Use: updated_vendor_context.py
→ Reference: CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md (Section: "Middleware Update")
**...add admin endpoints**
→ Use: vendor_domains_api.py
→ Reference: CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md (Section: "Create Admin Endpoints")
**...configure DNS**
→ Read: CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md (Section: "DNS Configuration")
→ Check: ARCHITECTURE_DIAGRAMS.md (Section: "DNS Configuration Examples")
**...set up SSL/TLS**
→ Read: CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md (Section: "SSL/TLS Certificates")
→ Check: IMPLEMENTATION_CHECKLIST.md (Phase 10)
**...verify a domain**
→ Read: CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md (Section: "DNS Verification")
→ Use: vendor_domains_api.py (verify_domain endpoint)
**...troubleshoot issues**
→ Check: QUICK_START.md (Section: "Common Issues & Fixes")
→ Check: IMPLEMENTATION_CHECKLIST.md (Section: "Troubleshooting Guide")
**...roll back if something goes wrong**
→ Follow: IMPLEMENTATION_CHECKLIST.md (Section: "Rollback Plan")
---
## 🔄 Recommended Reading Order
### For First-Time Readers:
1. **EXECUTIVE_SUMMARY.md** - Understand the concept (10 min)
2. **ARCHITECTURE_DIAGRAMS.md** - See visual flow (20 min)
3. **QUICK_START.md** - Test locally (30 min)
4. **CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md** - Deep dive (45 min)
5. **IMPLEMENTATION_CHECKLIST.md** - Plan deployment (review time)
### For Hands-On Implementers:
1. **QUICK_START.md** - Get started immediately
2. **ARCHITECTURE_DIAGRAMS.md** - Understand the flow
3. **CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md** - Reference as needed
4. **IMPLEMENTATION_CHECKLIST.md** - Follow for production
### For Decision Makers:
1. **EXECUTIVE_SUMMARY.md** - Complete overview
2. **ARCHITECTURE_DIAGRAMS.md** - Visual understanding
3. **IMPLEMENTATION_CHECKLIST.md** - See effort required
---
## ✅ What You'll Have After Implementation
### Technical Capabilities:
- ✅ Custom domains route to correct vendors
- ✅ Subdomain routing still works (backward compatible)
- ✅ Path-based routing still works (dev mode)
- ✅ DNS verification prevents domain hijacking
- ✅ SSL/TLS support for custom domains
- ✅ Admin panel for domain management
- ✅ Vendor isolation maintained
### Business Benefits:
- ✅ Enterprise feature for premium vendors
- ✅ Professional appearance for vendor shops
- ✅ Competitive advantage in market
- ✅ Higher vendor retention
- ✅ Additional revenue stream
---
## 📊 Implementation Summary
### Complexity: **Medium**
- New database table: 1
- New models: 1
- Modified files: 2-3
- New API endpoints: 5-7
### Time Required:
- **Quick test**: 30 minutes
- **Development**: 4-6 hours
- **Testing**: 2-3 hours
- **Production deployment**: 2-4 hours
- **Total**: 1-2 days
### Risk Level: **Low**
- Backward compatible
- No breaking changes
- Easy rollback
- Well-documented
### ROI: **High**
- Premium feature
- Low maintenance
- Scalable solution
- Competitive advantage
---
## 🔧 Technology Stack
Your existing stack works perfectly:
- ✅ FastAPI (Python web framework)
- ✅ PostgreSQL (database)
- ✅ SQLAlchemy (ORM)
- ✅ Jinja2 (templates)
- ✅ Alpine.js (frontend)
- ✅ Nginx (web server)
No new technologies required!
---
## 🆘 Support & Troubleshooting
### Common Questions
All answered in **EXECUTIVE_SUMMARY.md** (Q&A section)
### Common Issues
Listed in **QUICK_START.md** and **IMPLEMENTATION_CHECKLIST.md**
### Testing Problems
Covered in **CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md**
### Production Issues
See **IMPLEMENTATION_CHECKLIST.md** (Troubleshooting Guide)
---
## 📝 Key Concepts
### Multi-Tenant Architecture
Your app already supports this! Each vendor is a tenant with isolated data.
### Domain Mapping
New concept: Links custom domains to vendors via database table.
### Request Flow Priority
1. Custom domain (NEW)
2. Subdomain (EXISTING)
3. Path-based (EXISTING)
### DNS Verification
Security feature: Proves vendor owns the domain before activation.
### Vendor Isolation
Already working! Custom domains just add another entry point.
---
## 🎓 Learning Path
### Beginner (New to concept):
1. Read EXECUTIVE_SUMMARY.md
2. Look at ARCHITECTURE_DIAGRAMS.md
3. Try QUICK_START.md locally
### Intermediate (Ready to implement):
1. Follow QUICK_START.md
2. Read CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md
3. Use IMPLEMENTATION_CHECKLIST.md
### Advanced (Production deployment):
1. Review all documentation
2. Follow IMPLEMENTATION_CHECKLIST.md
3. Implement monitoring and maintenance
---
## 📈 Success Metrics
After implementation, you can track:
- Number of custom domains active
- Vendor adoption rate
- Domain verification success rate
- Traffic per domain
- SSL certificate status
- Failed domain lookups
---
## 🚦 Status Indicators
Throughout the documentation, you'll see these indicators:
✅ - Recommended approach
❌ - Not recommended
⚠️ - Warning or caution
🔧 - Technical detail
💡 - Tip or best practice
📝 - Note or important info
---
## 📞 Next Actions
### To Get Started:
1. Read EXECUTIVE_SUMMARY.md
2. Follow QUICK_START.md for local test
3. Review implementation approach with team
### To Deploy:
1. Complete QUICK_START.md test
2. Follow IMPLEMENTATION_CHECKLIST.md
3. Reference CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md
### To Understand:
1. Read EXECUTIVE_SUMMARY.md
2. Study ARCHITECTURE_DIAGRAMS.md
3. Review code files
---
## 📦 Package Contents
```
custom-domain-support/
├── Documentation/
│ ├── EXECUTIVE_SUMMARY.md (This provides overview)
│ ├── QUICK_START.md (30-minute implementation)
│ ├── CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md (Complete guide)
│ ├── ARCHITECTURE_DIAGRAMS.md (Visual diagrams)
│ ├── IMPLEMENTATION_CHECKLIST.md (Step-by-step tasks)
│ └── INDEX.md (This file)
└── Code Files/
├── vendor_domain_model.py (Database model)
├── updated_vendor_context.py (Enhanced middleware)
├── vendor_domains_api.py (Admin API)
├── migration_vendor_domains.py (Database migration)
├── config_updates.py (Configuration changes)
└── vendor_model_update.py (Vendor model updates)
```
---
## ✨ Final Notes
**This is a complete, production-ready solution.**
Everything you need is included:
- ✅ Complete documentation
- ✅ Working code examples
- ✅ Database migrations
- ✅ Security considerations
- ✅ Testing strategies
- ✅ Deployment guides
- ✅ Troubleshooting help
- ✅ Rollback plans
**Your current architecture is perfect for this feature!**
No major changes needed - just add domain mapping layer on top of existing vendor detection.
**Start with QUICK_START.md and you'll have a working demo in 30 minutes!**
---
**Questions?** Every document has troubleshooting and Q&A sections!
**Ready to begin?** Start with [EXECUTIVE_SUMMARY.md](EXECUTIVE_SUMMARY.md) or jump straight to [QUICK_START.md](QUICK_START.md)!

View File

@@ -0,0 +1,341 @@
# Quick Start: Custom Domains in 30 Minutes
This guide gets you from zero to working custom domains in 30 minutes.
## What You're Building
**Before:**
- Vendors only accessible via `vendor1.platform.com`
**After:**
- Vendors accessible via custom domains: `customdomain1.com` → Vendor 1
- Old subdomain method still works!
## Prerequisites
- FastAPI application with vendor context middleware (you have this ✓)
- PostgreSQL database (you have this ✓)
- Admin privileges to add database tables
## 5-Step Setup
### Step 1: Add Database Table (5 minutes)
Create and run this migration:
```sql
-- Create vendor_domains table
CREATE TABLE vendor_domains (
id SERIAL PRIMARY KEY,
vendor_id INTEGER NOT NULL REFERENCES vendors(id) ON DELETE CASCADE,
domain VARCHAR(255) NOT NULL UNIQUE,
is_primary BOOLEAN DEFAULT FALSE,
is_active BOOLEAN DEFAULT TRUE,
is_verified BOOLEAN DEFAULT FALSE,
verification_token VARCHAR(100) UNIQUE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_domain_active ON vendor_domains(domain, is_active);
```
**Verify:**
```bash
psql your_database -c "\d vendor_domains"
```
### Step 2: Create Model (5 minutes)
Create `models/database/vendor_domain.py`:
```python
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey
from sqlalchemy.orm import relationship
from app.core.database import Base
from datetime import datetime, timezone
class VendorDomain(Base):
__tablename__ = "vendor_domains"
id = Column(Integer, primary_key=True)
vendor_id = Column(Integer, ForeignKey("vendors.id", ondelete="CASCADE"))
domain = Column(String(255), nullable=False, unique=True)
is_primary = Column(Boolean, default=False)
is_active = Column(Boolean, default=True)
is_verified = Column(Boolean, default=False)
verification_token = Column(String(100))
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
vendor = relationship("Vendor", back_populates="domains")
@classmethod
def normalize_domain(cls, domain: str) -> str:
return domain.replace("https://", "").replace("http://", "").rstrip("/").lower()
```
Update `models/database/vendor.py`:
```python
# Add to Vendor class
domains = relationship("VendorDomain", back_populates="vendor")
```
### Step 3: Update Middleware (10 minutes)
Replace your `middleware/vendor_context.py` with this key section:
```python
from models.database.vendor_domain import VendorDomain
class VendorContextManager:
@staticmethod
def detect_vendor_context(request: Request) -> Optional[dict]:
host = request.headers.get("host", "").split(":")[0]
path = request.url.path
# NEW: Priority 1 - Custom domain check
from app.core.config import settings
platform_domain = getattr(settings, 'platform_domain', 'platform.com')
is_custom_domain = (
host and
not host.endswith(f".{platform_domain}") and
host != platform_domain and
"localhost" not in host and
not host.startswith("admin.")
)
if is_custom_domain:
return {
"domain": VendorDomain.normalize_domain(host),
"detection_method": "custom_domain",
"host": host
}
# EXISTING: Priority 2 - Subdomain check
if "." in host and not "localhost" in host:
parts = host.split(".")
if len(parts) >= 2 and parts[0] not in ["www", "admin", "api"]:
return {
"subdomain": parts[0],
"detection_method": "subdomain",
"host": host
}
# EXISTING: Priority 3 - Path-based check
if path.startswith("/vendor/"):
path_parts = path.split("/")
if len(path_parts) >= 3:
return {
"subdomain": path_parts[2],
"detection_method": "path",
"path_prefix": f"/vendor/{path_parts[2]}"
}
return None
@staticmethod
def get_vendor_from_context(db: Session, context: dict) -> Optional[Vendor]:
if not context:
return None
# NEW: Custom domain lookup
if context.get("detection_method") == "custom_domain":
vendor_domain = (
db.query(VendorDomain)
.filter(VendorDomain.domain == context["domain"])
.filter(VendorDomain.is_active == True)
.filter(VendorDomain.is_verified == True)
.first()
)
if vendor_domain and vendor_domain.vendor.is_active:
return vendor_domain.vendor
# EXISTING: Subdomain/path lookup
if "subdomain" in context:
return (
db.query(Vendor)
.filter(func.lower(Vendor.subdomain) == context["subdomain"].lower())
.filter(Vendor.is_active == True)
.first()
)
return None
```
### Step 4: Add Config (2 minutes)
Edit `app/core/config.py`:
```python
class Settings(BaseSettings):
# ... existing settings ...
# Add this
platform_domain: str = "platform.com" # Change to YOUR domain
```
Update `.env`:
```
PLATFORM_DOMAIN=platform.com # Change to YOUR domain
```
### Step 5: Test Locally (8 minutes)
**Add test data:**
```sql
-- Assuming you have a vendor with id=1
INSERT INTO vendor_domains (vendor_id, domain, is_active, is_verified)
VALUES (1, 'testdomain.local', true, true);
```
**Edit /etc/hosts:**
```bash
sudo nano /etc/hosts
# Add:
127.0.0.1 testdomain.local
```
**Start server:**
```bash
uvicorn main:app --reload
```
**Test in browser:**
```
http://testdomain.local:8000/
```
**Check logs for:**
```
✓ Vendor found via custom domain: testdomain.local → [Vendor Name]
```
## Done! 🎉
You now have custom domain support!
## Quick Test Checklist
- [ ] Can access vendor via custom domain: `testdomain.local:8000`
- [ ] Can still access via subdomain: `vendor1.localhost:8000`
- [ ] Can still access via path: `localhost:8000/vendor/vendor1/`
- [ ] Logs show correct detection method
- [ ] Database has vendor_domains table
- [ ] Model imports work
## What Works Now
**Custom domains** → Checks vendor_domains table
**Subdomains** → Checks vendors.subdomain (existing)
**Path-based** → Development mode (existing)
**Admin** → Still accessible
**Vendor isolation** → Each vendor sees only their data
## Next Steps
### For Development:
1. Test with multiple custom domains
2. Add more test vendors
3. Verify queries are scoped correctly
### For Production:
1. Add admin API endpoints (see full guide)
2. Add DNS verification (see full guide)
3. Configure Nginx to accept all domains
4. Set up SSL strategy
## Common Issues & Fixes
**Issue:** "Vendor not found"
```sql
-- Check if domain exists and is verified
SELECT * FROM vendor_domains WHERE domain = 'testdomain.local';
-- Should show is_verified = true and is_active = true
```
**Issue:** Wrong vendor loaded
```sql
-- Check for duplicate domains
SELECT domain, COUNT(*) FROM vendor_domains GROUP BY domain HAVING COUNT(*) > 1;
```
**Issue:** Still using subdomain detection
```python
# Check middleware logs - should show:
# detection_method: "custom_domain"
# Not "subdomain"
```
## Production Checklist
Before going live:
- [ ] Update `PLATFORM_DOMAIN` in production .env
- [ ] Configure Nginx: `server_name _;`
- [ ] Set up SSL strategy (Cloudflare recommended)
- [ ] Add proper DNS verification (security!)
- [ ] Add admin UI for domain management
- [ ] Test with real custom domain
- [ ] Monitor logs for errors
- [ ] Set up rollback plan
## Files Changed
**New files:**
- `models/database/vendor_domain.py`
**Modified files:**
- `middleware/vendor_context.py` (added custom domain logic)
- `app/core/config.py` (added platform_domain setting)
- `models/database/vendor.py` (added domains relationship)
**Database:**
- Added `vendor_domains` table
## Architecture Diagram
```
Request: customdomain1.com
Nginx: Accepts all domains
FastAPI Middleware:
Is it custom domain? YES
Query vendor_domains table
Find vendor_id = 1
Load Vendor 1
Route Handler:
Use request.state.vendor (Vendor 1)
All queries scoped to Vendor 1
Response: Vendor 1's shop
```
## Help & Resources
**Full Guides:**
- `/mnt/user-data/outputs/CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md` - Complete guide
- `/mnt/user-data/outputs/ARCHITECTURE_DIAGRAMS.md` - Visual diagrams
- `/mnt/user-data/outputs/IMPLEMENTATION_CHECKLIST.md` - Detailed checklist
**Files Generated:**
- `vendor_domain_model.py` - Complete model code
- `updated_vendor_context.py` - Complete middleware code
- `vendor_domains_api.py` - Admin API endpoints
- `migration_vendor_domains.py` - Database migration
## Timeline
- ✅ Step 1: 5 minutes (database)
- ✅ Step 2: 5 minutes (model)
- ✅ Step 3: 10 minutes (middleware)
- ✅ Step 4: 2 minutes (config)
- ✅ Step 5: 8 minutes (testing)
**Total: 30 minutes** ⏱️
---
**Questions?** Check the full implementation guide or review the architecture diagrams!

View File

@@ -0,0 +1,425 @@
# Custom Domain Support - Complete Implementation Package
## 🎯 Your Question Answered
**Q: "Can my FastAPI multi-tenant app support multiple shops on different custom domains like customdomain1.com and customdomain2.com?"**
**A: YES! ✅** Your architecture is **perfectly suited** for this. You just need to add a domain mapping layer.
---
## 📊 What You Have vs What You Need
### ✅ What You Already Have (Perfect!)
- Multi-tenant architecture with vendor isolation
- Vendor context middleware
- Subdomain routing: `vendor1.platform.com`
- Path-based routing: `/vendor/vendor1/`
- Request state management
### What You Need to Add (Simple!)
- Database table: `vendor_domains` (1 table)
- Enhanced middleware: Check custom domains first
- DNS verification: Prevent domain hijacking
---
## 🚀 Quick Start
```bash
# 1. Add database table (5 minutes)
CREATE TABLE vendor_domains (
id SERIAL PRIMARY KEY,
vendor_id INTEGER REFERENCES vendors(id),
domain VARCHAR(255) UNIQUE,
is_verified BOOLEAN DEFAULT FALSE
);
# 2. Update middleware (10 minutes)
# See: updated_vendor_context.py
# 3. Test locally (5 minutes)
# Add to /etc/hosts:
127.0.0.1 testdomain.local
# Add test data:
INSERT INTO vendor_domains (vendor_id, domain, is_verified)
VALUES (1, 'testdomain.local', true);
# Visit: http://testdomain.local:8000/
# ✅ Should show Vendor 1's shop!
```
**Total time: 30 minutes** ⏱️
---
## 🏗️ Architecture Overview
### Before (Current)
```
Customer → vendor1.platform.com → Middleware checks subdomain → Vendor 1
```
### After (With Custom Domains)
```
Customer → customdomain1.com → Middleware checks domain table → Vendor 1
(subdomain still works too!)
```
### How It Works
```
1. Request arrives: Host = "customdomain1.com"
2. Middleware: Is it custom domain? YES
3. Query: SELECT vendor_id FROM vendor_domains WHERE domain = 'customdomain1.com'
4. Result: vendor_id = 1
5. Load: Vendor 1 data
6. Render: Vendor 1's shop
```
---
## 📚 Documentation Structure
This package contains 6 comprehensive guides:
### Start Here → [INDEX.md](INDEX.md)
Master navigation guide to all documentation
### Quick Implementation → [QUICK_START.md](QUICK_START.md)
- ⏱️ 30 minutes to working demo
- 🔧 Hands-on implementation
- ✅ Local testing included
### Complete Guide → [CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md](CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md)
- 📖 Comprehensive documentation
- 🔐 Security best practices
- 🚀 Production deployment
- ⚠️ Troubleshooting guide
### Visual Learning → [ARCHITECTURE_DIAGRAMS.md](ARCHITECTURE_DIAGRAMS.md)
- 📊 Architecture diagrams
- 🔄 Request flow illustrations
- 🗄️ Database relationships
- 🌐 DNS configuration examples
### Project Planning → [IMPLEMENTATION_CHECKLIST.md](IMPLEMENTATION_CHECKLIST.md)
- ✅ Phase-by-phase tasks
- 🧪 Testing procedures
- 📦 Deployment steps
- 🔄 Rollback plan
### Decision Making → [EXECUTIVE_SUMMARY.md](EXECUTIVE_SUMMARY.md)
- 💼 Business case
- 💰 ROI analysis
- ⚖️ Risk assessment
- ❓ Q&A section
---
## 💻 Code Files Included
All ready-to-use code in `/home/claude/`:
| File | Purpose | Status |
|------|---------|--------|
| `vendor_domain_model.py` | SQLAlchemy model | ✅ Ready |
| `updated_vendor_context.py` | Enhanced middleware | ✅ Ready |
| `vendor_domains_api.py` | Admin API endpoints | ✅ Ready |
| `migration_vendor_domains.py` | Database migration | ✅ Ready |
| `config_updates.py` | Configuration | ✅ Ready |
| `vendor_model_update.py` | Vendor model update | ✅ Ready |
---
## 🎯 Key Features
### ✅ What You Get
**Custom Domain Support:**
- Vendors can use their own domains
- Multiple domains per vendor
- Professional branding
**Security:**
- DNS verification required
- Prevents domain hijacking
- Vendor ownership proof
**Backward Compatible:**
- Subdomain routing still works
- Path-based routing still works
- No breaking changes
**Scalability:**
- Single server handles all domains
- Database-driven routing
- Easy to manage
---
## 📈 Implementation Metrics
### Complexity
- **Database**: +1 table
- **Models**: +1 new, ~1 modified
- **Middleware**: ~1 file modified
- **Endpoints**: +5-7 API routes
- **Risk Level**: 🟢 Low
### Time Required
- **Local Test**: 30 minutes
- **Development**: 4-6 hours
- **Testing**: 2-3 hours
- **Deployment**: 2-4 hours
- **Total**: 🕐 1-2 days
### Resources
- **New Technologies**: None! Use existing stack
- **Server Changes**: Nginx config update
- **DNS Required**: Yes, per custom domain
---
## 🔐 Security Features
-**DNS Verification**: Vendors must prove domain ownership
-**TXT Record Check**: Token-based verification
-**Vendor Isolation**: Each vendor sees only their data
-**Active Status**: Domains can be deactivated
-**Audit Trail**: Track domain changes
---
## 🌐 DNS Configuration (Vendor Side)
When a vendor wants to use `customdomain1.com`:
```
# At their domain registrar:
1. A Record:
Name: @
Value: 123.45.67.89 (your server IP)
2. Verification TXT Record:
Name: _letzshop-verify
Value: [token from your platform]
3. Wait 5-15 minutes for DNS propagation
4. Admin verifies domain
5. Domain goes live! 🎉
```
---
## 🚀 Deployment Options
### Development
```bash
# Test locally with /etc/hosts
127.0.0.1 testdomain.local
```
### Staging
```bash
# Use subdomain for testing
staging-vendor.platform.com
```
### Production
```bash
# Full custom domain support
customdomain1.com → Vendor 1
customdomain2.com → Vendor 2
```
---
## 💡 Why This Works
Your architecture is **already multi-tenant**:
- ✅ Vendor isolation exists
- ✅ Middleware detects context
- ✅ All queries scoped to vendor
- ✅ Request state management works
You just need to add **one more detection method**:
1. ~~Check subdomain~~ (existing)
2. ~~Check path~~ (existing)
3. **Check custom domain** (new!)
---
## 📦 What's Included
### Complete Documentation
- 5 detailed guides (70+ pages)
- Architecture diagrams
- Implementation checklist
- Quick start guide
- Troubleshooting help
### Production-Ready Code
- Database models
- API endpoints
- Migrations
- Configuration
- Middleware updates
### Best Practices
- Security guidelines
- Testing strategies
- Deployment procedures
- Monitoring setup
- Rollback plans
---
## 🎓 Learning Path
### New to Concept? (30 minutes)
1. Read: EXECUTIVE_SUMMARY.md
2. View: ARCHITECTURE_DIAGRAMS.md
3. Try: QUICK_START.md
### Ready to Build? (2-3 hours)
1. Follow: QUICK_START.md
2. Reference: CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md
3. Use: Code files provided
### Deploying to Production? (1-2 days)
1. Complete: QUICK_START.md test
2. Follow: IMPLEMENTATION_CHECKLIST.md
3. Reference: All documentation
---
## ✅ Success Criteria
After implementation:
- [ ] Custom domains route to correct vendors
- [ ] Subdomain routing still works
- [ ] Path-based routing still works
- [ ] DNS verification functional
- [ ] SSL certificates work
- [ ] Admin can manage domains
- [ ] Monitoring in place
- [ ] Documentation complete
---
## 🎉 Benefits
### For You (Platform Owner)
- 💰 Premium feature for vendors
- 🏆 Competitive advantage
- 📈 Higher vendor retention
- 🔧 Single codebase maintained
### For Vendors
- 🎨 Own brand domain
- 🔍 Better SEO
- 💼 Professional appearance
- 📊 Marketing benefits
### For Customers
- ✨ Seamless experience
- 🔒 Trust familiar domain
- 🎯 Better UX
---
## 📞 Next Steps
### To Understand
→ Read [EXECUTIVE_SUMMARY.md](EXECUTIVE_SUMMARY.md)
### To Test Locally
→ Follow [QUICK_START.md](QUICK_START.md)
### To Implement
→ Use [IMPLEMENTATION_CHECKLIST.md](IMPLEMENTATION_CHECKLIST.md)
### To Deploy
→ Reference [CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md](CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md)
### To Navigate Everything
→ Check [INDEX.md](INDEX.md)
---
## ❓ Common Questions
**Q: Will this break my existing setup?**
A: No! Completely backward compatible.
**Q: Do I need new servers?**
A: No! Single server handles all domains.
**Q: What about SSL certificates?**
A: Use Cloudflare (easiest) or Let's Encrypt.
**Q: How long to implement?**
A: 30 minutes for demo, 1-2 days for production.
**Q: Is it secure?**
A: Yes! DNS verification prevents hijacking.
**Q: Can one vendor have multiple domains?**
A: Yes! Fully supported.
More Q&A in [EXECUTIVE_SUMMARY.md](EXECUTIVE_SUMMARY.md)
---
## 🏁 Final Thoughts
**Your architecture is perfect for this!**
You have:
- ✅ Multi-tenant design
- ✅ Vendor isolation
- ✅ Context middleware
- ✅ FastAPI + PostgreSQL
You just need:
- Domain mapping table
- Enhanced middleware
- DNS verification
**It's simpler than you think!**
---
## 📋 File Locations
### Documentation (read online)
- `/mnt/user-data/outputs/INDEX.md`
- `/mnt/user-data/outputs/EXECUTIVE_SUMMARY.md`
- `/mnt/user-data/outputs/QUICK_START.md`
- `/mnt/user-data/outputs/CUSTOM_DOMAIN_IMPLEMENTATION_GUIDE.md`
- `/mnt/user-data/outputs/ARCHITECTURE_DIAGRAMS.md`
- `/mnt/user-data/outputs/IMPLEMENTATION_CHECKLIST.md`
### Code Files (copy to your project)
- `/home/claude/vendor_domain_model.py`
- `/home/claude/updated_vendor_context.py`
- `/home/claude/vendor_domains_api.py`
- `/home/claude/migration_vendor_domains.py`
- `/home/claude/config_updates.py`
- `/home/claude/vendor_model_update.py`
---
**Ready to start?**
Begin with [QUICK_START.md](QUICK_START.md) for a 30-minute working demo!
Or read [INDEX.md](INDEX.md) for complete navigation guide.
---
**Good luck! 🚀**

View File

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

View File

@@ -0,0 +1,509 @@
# Theme System Integration Guide for Your Existing Architecture
## 🎯 Overview
This guide shows how to integrate the multi-theme system into your **existing** FastAPI + Alpine.js + Tailwind CSS architecture.
**Key Point:** You already have 80% of what you need! Your `theme_config` JSON field in the Vendor model is the foundation.
## ✅ What You Already Have
1. **Vendor model** with `theme_config` JSON field ✅
2. **Alpine.js** frontend pattern established ✅
3. **Tailwind CSS** for styling ✅
4. **Admin pages** with Jinja2 templates ✅
5. **Vendor context middleware**
## 🚀 Integration Steps
### Step 1: Update Vendor Model (5 minutes)
Your current model already has `theme_config`, so just add helper methods:
```python
# models/database/vendor.py
class Vendor(Base, TimestampMixin):
# ... existing fields ...
theme_config = Column(JSON, default=dict) # ✅ You already have this!
# ADD THIS PROPERTY:
@property
def theme(self):
"""
Get theme configuration for this vendor.
Returns dict with theme configuration.
"""
if self.theme_config:
return self._normalize_theme_config(self.theme_config)
return self._get_default_theme()
def _normalize_theme_config(self, config: dict) -> dict:
"""Ensure theme_config has all required fields"""
return {
"colors": config.get("colors", {
"primary": "#6366f1",
"secondary": "#8b5cf6",
"accent": "#ec4899"
}),
"fonts": config.get("fonts", {
"heading": "Inter, sans-serif",
"body": "Inter, sans-serif"
}),
"layout": config.get("layout", {
"style": "grid",
"header": "fixed"
}),
"branding": config.get("branding", {
"logo": None,
"favicon": None
}),
"custom_css": config.get("custom_css", None),
"css_variables": self._generate_css_variables(config)
}
def _generate_css_variables(self, config: dict) -> dict:
"""Generate CSS custom properties from theme"""
colors = config.get("colors", {})
fonts = config.get("fonts", {})
return {
"--color-primary": colors.get("primary", "#6366f1"),
"--color-secondary": colors.get("secondary", "#8b5cf6"),
"--color-accent": colors.get("accent", "#ec4899"),
"--font-heading": fonts.get("heading", "Inter, sans-serif"),
"--font-body": fonts.get("body", "Inter, sans-serif"),
}
def _get_default_theme(self) -> dict:
"""Default theme if none configured"""
return {
"colors": {"primary": "#6366f1", "secondary": "#8b5cf6", "accent": "#ec4899"},
"fonts": {"heading": "Inter, sans-serif", "body": "Inter, sans-serif"},
"layout": {"style": "grid", "header": "fixed"},
"branding": {"logo": None, "favicon": None},
"custom_css": None,
"css_variables": {
"--color-primary": "#6366f1",
"--color-secondary": "#8b5cf6",
"--color-accent": "#ec4899",
"--font-heading": "Inter, sans-serif",
"--font-body": "Inter, sans-serif",
}
}
```
**That's it for the model!** No new tables needed if you use the existing `theme_config` JSON field.
### Step 2: Add Theme Route to Admin Pages (2 minutes)
Add to `app/api/v1/admin/pages.py`:
```python
@router.get("/vendors/{vendor_code}/theme", response_class=HTMLResponse, include_in_schema=False)
async def admin_vendor_theme_page(
request: Request,
vendor_code: str = Path(..., description="Vendor code"),
current_user: User = Depends(get_current_admin_user),
db: Session = Depends(get_db)
):
"""
Render vendor theme customization page.
"""
return templates.TemplateResponse(
"admin/vendor-theme.html",
{
"request": request,
"user": current_user,
"vendor_code": vendor_code,
}
)
```
### Step 3: Create Theme Management Page (5 minutes)
1. Copy the HTML template from `/mnt/user-data/outputs/vendor-theme-page.html`
2. Save as `app/templates/admin/vendor-theme.html`
3. Copy the JS component from `/mnt/user-data/outputs/vendor-theme.js`
4. Save as `static/admin/js/vendor-theme.js`
### Step 4: Update Vendor Detail Page to Link to Theme (2 minutes)
In your `app/templates/admin/vendor-detail.html`, add a "Customize Theme" button:
```html
<!-- In your vendor details page -->
<div class="flex space-x-2">
<a :href="`/admin/vendors/${vendor?.vendor_code}/edit`"
class="px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700">
Edit Vendor
</a>
<!-- ADD THIS BUTTON -->
<a :href="`/admin/vendors/${vendor?.vendor_code}/theme`"
class="px-4 py-2 text-sm font-medium text-purple-700 bg-white border border-purple-600 rounded-lg hover:bg-purple-50">
Customize Theme
</a>
</div>
```
### Step 5: Update Shop Template to Use Theme (10 minutes)
Create `app/templates/shop/base.html` (if you don't have it yet):
```html
<!DOCTYPE html>
<html lang="en" x-data="shopLayoutData()" x-bind:class="{ 'dark': dark }">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{{ vendor.name }}{% endblock %}</title>
<!-- ✅ CRITICAL: Inject theme CSS variables -->
<style id="vendor-theme-variables">
:root {
{% for key, value in theme.css_variables.items() %}
{{ key }}: {{ value }};
{% endfor %}
}
/* Custom CSS from vendor */
{% if theme.custom_css %}
{{ theme.custom_css | safe }}
{% endif %}
</style>
<!-- Tailwind CSS -->
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<!-- Alpine.js -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
</head>
<body class="bg-gray-50">
<!-- Header -->
<header class="bg-white shadow-sm">
<div class="max-w-7xl mx-auto px-4 py-4">
<div class="flex items-center justify-between">
<!-- Logo -->
<div class="flex items-center">
{% if theme.branding.logo %}
<img src="{{ theme.branding.logo }}"
alt="{{ vendor.name }}"
class="h-8">
{% else %}
<h1 class="text-xl font-bold"
style="color: var(--color-primary)">
{{ vendor.name }}
</h1>
{% endif %}
</div>
<!-- Navigation -->
<nav class="flex space-x-6">
<a href="/" class="hover:text-primary">Home</a>
<a href="/products" class="hover:text-primary">Products</a>
<a href="/about" class="hover:text-primary">About</a>
</nav>
</div>
</div>
</header>
<!-- Main Content -->
<main>
{% block content %}{% endblock %}
</main>
<!-- Footer -->
<footer class="bg-gray-100 mt-12 py-8">
<div class="max-w-7xl mx-auto px-4 text-center text-gray-600">
<p>&copy; {{ now().year }} {{ vendor.name }}</p>
</div>
</footer>
</body>
</html>
```
### Step 6: Update Shop Routes to Pass Theme (5 minutes)
In your shop route handlers (e.g., `app/api/v1/public/vendors/pages.py`):
```python
@router.get("/")
async def shop_home(request: Request, db: Session = Depends(get_db)):
vendor = request.state.vendor # From vendor context middleware
# Get theme from vendor
theme = vendor.theme # Uses the property we added
return templates.TemplateResponse("shop/home.html", {
"request": request,
"vendor": vendor,
"theme": theme, # ✅ Pass theme to template
})
```
## 📊 Using Tailwind with Theme Variables
The magic is in CSS variables! Tailwind can use your theme colors:
```html
<!-- In your shop templates -->
<!-- Primary color button -->
<button class="px-4 py-2 rounded-lg"
style="background-color: var(--color-primary); color: white;">
Shop Now
</button>
<!-- Or use Tailwind utilities with inline styles -->
<div class="text-2xl font-bold"
style="color: var(--color-primary); font-family: var(--font-heading)">
Welcome to {{ vendor.name }}
</div>
<!-- Product card with theme colors -->
<div class="p-4 bg-white rounded-lg border"
style="border-color: var(--color-primary)">
<h3 class="text-lg font-semibold"
style="color: var(--color-primary)">
Product Name
</h3>
</div>
```
## 🎨 How It All Works Together
### 1. Admin Customizes Theme
```
Admin → /admin/vendors/TECHSTORE/theme
Sees theme editor (colors, fonts, layout)
Clicks "Save Theme"
PUT /api/v1/admin/vendors/TECHSTORE
Updates vendor.theme_config JSON:
{
"colors": {
"primary": "#2563eb", // Blue
"secondary": "#0ea5e9"
},
"fonts": {
"heading": "Roboto, sans-serif"
}
}
```
### 2. Customer Visits Shop
```
Customer → techstore.platform.com
Vendor middleware identifies Vendor = TECHSTORE
Shop route loads:
- vendor object
- theme = vendor.theme (property we added)
Template renders with:
:root {
--color-primary: #2563eb;
--color-secondary: #0ea5e9;
--font-heading: Roboto, sans-serif;
}
Customer sees blue-themed shop with Roboto headings!
```
## 🔧 API Endpoints Needed
Your existing vendor update endpoint already works! Just update `theme_config`:
```python
# app/api/v1/admin/vendors.py
@router.put("/vendors/{vendor_code}")
async def update_vendor(
vendor_code: str,
vendor_data: VendorUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_admin_user)
):
vendor = db.query(Vendor).filter(
Vendor.vendor_code == vendor_code
).first()
if not vendor:
raise HTTPException(404, "Vendor not found")
# Update fields
if vendor_data.theme_config is not None:
vendor.theme_config = vendor_data.theme_config # ✅ This already works!
# ... other updates ...
db.commit()
db.refresh(vendor)
return vendor
```
**That's it!** Your existing update endpoint handles theme updates.
## 🎯 Quick Test
### 1. Test Theme Editor
```bash
# Start your app
uvicorn main:app --reload
# Visit admin
http://localhost:8000/admin/vendors/YOUR_VENDOR_CODE/theme
# Change colors and save
# Check database:
SELECT theme_config FROM vendors WHERE vendor_code = 'YOUR_VENDOR_CODE';
# Should see:
# {"colors": {"primary": "#your-color", ...}}
```
### 2. Test Shop Rendering
```bash
# Visit vendor shop
http://vendor1.localhost:8000/
# Inspect page source
# Should see:
# <style id="vendor-theme-variables">
# :root {
# --color-primary: #your-color;
# }
# </style>
```
## 📦 Files to Create/Update
### Create New Files:
1. `app/templates/admin/vendor-theme.html` ← From `/mnt/user-data/outputs/vendor-theme-page.html`
2. `static/admin/js/vendor-theme.js` ← From `/mnt/user-data/outputs/vendor-theme.js`
3. `app/templates/shop/base.html` ← Base shop template with theme support
### Update Existing Files:
1. `models/database/vendor.py` ← Add `theme` property and helper methods
2. `app/api/v1/admin/pages.py` ← Add theme route
3. `models/schema/vendor.py` ← Already has `theme_config` in VendorUpdate ✅
## 🎨 Example: Different Themes
### Vendor 1: Tech Store (Blue Theme)
```python
{
"colors": {
"primary": "#2563eb",
"secondary": "#0ea5e9",
"accent": "#f59e0b"
},
"fonts": {
"heading": "Roboto, sans-serif",
"body": "Open Sans, sans-serif"
},
"layout": {
"style": "grid"
}
}
```
### Vendor 2: Fashion Boutique (Pink Theme)
```python
{
"colors": {
"primary": "#ec4899",
"secondary": "#f472b6",
"accent": "#fbbf24"
},
"fonts": {
"heading": "Playfair Display, serif",
"body": "Lato, sans-serif"
},
"layout": {
"style": "masonry"
}
}
```
## ✅ Checklist
- [ ] Add `theme` property to Vendor model
- [ ] Add theme route to `pages.py`
- [ ] Create `vendor-theme.html` template
- [ ] Create `vendor-theme.js` Alpine component
- [ ] Create shop `base.html` with theme injection
- [ ] Update shop routes to pass `theme`
- [ ] Add "Customize Theme" button to vendor detail page
- [ ] Test theme editor in admin
- [ ] Test theme rendering on shop
- [ ] Verify CSS variables work with Tailwind
## 🚀 Benefits
### For You:
- ✅ Uses existing `theme_config` field (no migration!)
- ✅ Works with current Alpine.js pattern
- ✅ Compatible with Tailwind CSS
- ✅ Follows your established conventions
### For Vendors:
- ✅ Easy theme customization
- ✅ Live preview
- ✅ Preset templates
- ✅ Custom branding
## 💡 Advanced: Tailwind Custom Configuration
If you want Tailwind to use theme variables natively:
```javascript
// tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
primary: 'var(--color-primary)',
secondary: 'var(--color-secondary)',
accent: 'var(--color-accent)',
},
fontFamily: {
heading: 'var(--font-heading)',
body: 'var(--font-body)',
}
}
}
}
```
Then you can use:
```html
<button class="bg-primary text-white">Shop Now</button>
<h1 class="font-heading text-primary">Welcome</h1>
```
## 🎉 Summary
**Your architecture is perfect for this!**
1. You already have `theme_config` JSON field ✅
2. Just add a `theme` property to access it ✅
3. Create admin page to edit it ✅
4. Inject CSS variables in shop templates ✅
5. Use variables with Tailwind/inline styles ✅
**Total implementation time: ~30 minutes**
Each vendor gets unique theming without any database migrations! 🚀

View File

@@ -0,0 +1,246 @@
{# app/templates/shop/base.html #}
{# Base template for vendor shop frontend with theme support #}
<!DOCTYPE html>
<html lang="en" x-data="shopLayoutData()" x-bind:class="{ 'dark': dark }">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{# Dynamic title with vendor branding #}
<title>
{% block title %}{{ vendor.name }}{% endblock %}
{% if vendor.tagline %} - {{ vendor.tagline }}{% endif %}
</title>
{# SEO Meta Tags #}
<meta name="description" content="{% block meta_description %}{{ vendor.description or 'Shop at ' + vendor.name }}{% endblock %}">
<meta name="keywords" content="{% block meta_keywords %}{{ vendor.name }}, online shop{% endblock %}">
{# Favicon - vendor-specific or default #}
{% if theme.branding.favicon %}
<link rel="icon" type="image/x-icon" href="{{ theme.branding.favicon }}">
{% else %}
<link rel="icon" type="image/x-icon" href="{{ url_for('static', path='favicon.ico') }}">
{% endif %}
{# CRITICAL: Inject theme CSS variables #}
<style id="vendor-theme-variables">
:root {
{% for key, value in theme.css_variables.items() %}
{{ key }}: {{ value }};
{% endfor %}
}
{# Custom CSS from vendor theme #}
{% if theme.custom_css %}
{{ theme.custom_css | safe }}
{% endif %}
</style>
{# Tailwind CSS - uses CSS variables #}
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
{# Base Shop Styles #}
<link rel="stylesheet" href="{{ url_for('static', path='shop/css/shop.css') }}">
{# Optional: Theme-specific stylesheet #}
{% if theme.theme_name != 'default' %}
<link rel="stylesheet" href="{{ url_for('static', path='shop/themes/' + theme.theme_name + '.css') }}">
{% endif %}
{# Alpine.js for interactivity #}
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
{% block extra_head %}{% endblock %}
</head>
<body class="bg-gray-50 dark:bg-gray-900 transition-colors duration-200">
{# Header - Theme-aware #}
<header class="{% if theme.layout.header == 'fixed' %}sticky top-0 z-50{% endif %}
bg-white dark:bg-gray-800 shadow-sm border-b border-gray-200 dark:border-gray-700">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-16">
{# Vendor Logo #}
<div class="flex items-center">
<a href="/" class="flex items-center space-x-3">
{% if theme.branding.logo %}
{# Show light logo in light mode, dark logo in dark mode #}
<img x-show="!dark"
src="{{ theme.branding.logo }}"
alt="{{ vendor.name }}"
class="h-8 w-auto">
{% if theme.branding.logo_dark %}
<img x-show="dark"
src="{{ theme.branding.logo_dark }}"
alt="{{ vendor.name }}"
class="h-8 w-auto">
{% endif %}
{% else %}
<span class="text-xl font-bold" style="color: var(--color-primary)">
{{ vendor.name }}
</span>
{% endif %}
</a>
</div>
{# Navigation #}
<nav class="hidden md:flex space-x-8">
<a href="/" class="text-gray-700 dark:text-gray-300 hover:text-primary">
Home
</a>
<a href="/products" class="text-gray-700 dark:text-gray-300 hover:text-primary">
Products
</a>
<a href="/about" class="text-gray-700 dark:text-gray-300 hover:text-primary">
About
</a>
<a href="/contact" class="text-gray-700 dark:text-gray-300 hover:text-primary">
Contact
</a>
</nav>
{# Right side actions #}
<div class="flex items-center space-x-4">
{# Search #}
<button @click="openSearch()" class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
</button>
{# Cart #}
<a href="/cart" class="relative p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z"></path>
</svg>
<span x-show="cartCount > 0"
x-text="cartCount"
class="absolute -top-1 -right-1 bg-accent text-white text-xs rounded-full h-5 w-5 flex items-center justify-center"
style="background-color: var(--color-accent)">
</span>
</a>
{# Theme toggle #}
<button @click="toggleTheme()"
class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700">
<svg x-show="!dark" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"></path>
</svg>
<svg x-show="dark" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"></path>
</svg>
</button>
{# Account #}
<a href="/account" class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
</svg>
</a>
{# Mobile menu toggle #}
<button @click="toggleMobileMenu()" class="md:hidden p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</button>
</div>
</div>
</div>
</header>
{# Main Content Area #}
<main class="min-h-screen">
{% block content %}
{# Page-specific content goes here #}
{% endblock %}
</main>
{# Footer with vendor info and social links #}
<footer class="bg-gray-100 dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 mt-12">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div class="grid grid-cols-1 md:grid-cols-4 gap-8">
{# Vendor Info #}
<div class="col-span-1 md:col-span-2">
<h3 class="text-lg font-semibold mb-4" style="color: var(--color-primary)">
{{ vendor.name }}
</h3>
<p class="text-gray-600 dark:text-gray-400 mb-4">
{{ vendor.description }}
</p>
{# Social Links from theme #}
{% if theme.social_links %}
<div class="flex space-x-4">
{% if theme.social_links.facebook %}
<a href="{{ theme.social_links.facebook }}" target="_blank"
class="text-gray-600 hover:text-primary dark:text-gray-400">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>
</svg>
</a>
{% endif %}
{% if theme.social_links.instagram %}
<a href="{{ theme.social_links.instagram }}" target="_blank"
class="text-gray-600 hover:text-primary dark:text-gray-400">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z"/>
</svg>
</a>
{% endif %}
{# Add more social networks as needed #}
</div>
{% endif %}
</div>
{# Quick Links #}
<div>
<h4 class="font-semibold mb-4">Quick Links</h4>
<ul class="space-y-2">
<li><a href="/products" class="text-gray-600 hover:text-primary dark:text-gray-400">Products</a></li>
<li><a href="/about" class="text-gray-600 hover:text-primary dark:text-gray-400">About Us</a></li>
<li><a href="/contact" class="text-gray-600 hover:text-primary dark:text-gray-400">Contact</a></li>
<li><a href="/terms" class="text-gray-600 hover:text-primary dark:text-gray-400">Terms</a></li>
</ul>
</div>
{# Customer Service #}
<div>
<h4 class="font-semibold mb-4">Customer Service</h4>
<ul class="space-y-2">
<li><a href="/help" class="text-gray-600 hover:text-primary dark:text-gray-400">Help Center</a></li>
<li><a href="/shipping" class="text-gray-600 hover:text-primary dark:text-gray-400">Shipping Info</a></li>
<li><a href="/returns" class="text-gray-600 hover:text-primary dark:text-gray-400">Returns</a></li>
<li><a href="/faq" class="text-gray-600 hover:text-primary dark:text-gray-400">FAQ</a></li>
</ul>
</div>
</div>
{# Copyright #}
<div class="mt-8 pt-8 border-t border-gray-200 dark:border-gray-700 text-center text-gray-600 dark:text-gray-400">
<p>&copy; {{ now().year }} {{ vendor.name }}. All rights reserved.</p>
</div>
</div>
</footer>
{# Base Shop JavaScript #}
<script src="{{ url_for('static', path='shop/js/shop-layout.js') }}"></script>
{# Page-specific JavaScript #}
{% block extra_scripts %}{% endblock %}
{# Toast notification container #}
<div id="toast-container" class="fixed bottom-4 right-4 z-50"></div>
</body>
</html>

View File

@@ -0,0 +1,228 @@
// static/shop/js/shop-layout.js
/**
* Shop Layout Component
* Provides base functionality for vendor shop pages
* Works with vendor-specific themes
*/
const shopLog = {
info: (...args) => console.info('🛒 [SHOP]', ...args),
warn: (...args) => console.warn('⚠️ [SHOP]', ...args),
error: (...args) => console.error('❌ [SHOP]', ...args),
debug: (...args) => console.log('🔍 [SHOP]', ...args)
};
/**
* Shop Layout Data
* Base Alpine.js component for shop pages
*/
function shopLayoutData() {
return {
// Theme state
dark: localStorage.getItem('shop-theme') === 'dark',
// UI state
mobileMenuOpen: false,
searchOpen: false,
cartCount: 0,
// Cart state
cart: [],
// Initialize
init() {
shopLog.info('Shop layout initializing...');
// Load cart from localStorage
this.loadCart();
// Listen for cart updates
window.addEventListener('cart-updated', () => {
this.loadCart();
});
shopLog.info('Shop layout initialized');
},
// Theme management
toggleTheme() {
this.dark = !this.dark;
localStorage.setItem('shop-theme', this.dark ? 'dark' : 'light');
shopLog.debug('Theme toggled:', this.dark ? 'dark' : 'light');
},
// Mobile menu
toggleMobileMenu() {
this.mobileMenuOpen = !this.mobileMenuOpen;
if (this.mobileMenuOpen) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
}
},
closeMobileMenu() {
this.mobileMenuOpen = false;
document.body.style.overflow = '';
},
// Search
openSearch() {
this.searchOpen = true;
shopLog.debug('Search opened');
// Focus search input after a short delay
setTimeout(() => {
const input = document.querySelector('#search-input');
if (input) input.focus();
}, 100);
},
closeSearch() {
this.searchOpen = false;
},
// Cart management
loadCart() {
try {
const cartData = localStorage.getItem('shop-cart');
if (cartData) {
this.cart = JSON.parse(cartData);
this.cartCount = this.cart.reduce((sum, item) => sum + item.quantity, 0);
}
} catch (error) {
shopLog.error('Failed to load cart:', error);
this.cart = [];
this.cartCount = 0;
}
},
addToCart(product, quantity = 1) {
shopLog.info('Adding to cart:', product.name, 'x', quantity);
// Find existing item
const existingIndex = this.cart.findIndex(item => item.id === product.id);
if (existingIndex !== -1) {
// Update quantity
this.cart[existingIndex].quantity += quantity;
} else {
// Add new item
this.cart.push({
id: product.id,
name: product.name,
price: product.price,
image: product.image,
quantity: quantity
});
}
// Save and update
this.saveCart();
this.showToast(`${product.name} added to cart`, 'success');
},
updateCartItem(productId, quantity) {
const index = this.cart.findIndex(item => item.id === productId);
if (index !== -1) {
if (quantity <= 0) {
this.cart.splice(index, 1);
} else {
this.cart[index].quantity = quantity;
}
this.saveCart();
}
},
removeFromCart(productId) {
this.cart = this.cart.filter(item => item.id !== productId);
this.saveCart();
this.showToast('Item removed from cart', 'info');
},
clearCart() {
this.cart = [];
this.saveCart();
this.showToast('Cart cleared', 'info');
},
saveCart() {
try {
localStorage.setItem('shop-cart', JSON.stringify(this.cart));
this.cartCount = this.cart.reduce((sum, item) => sum + item.quantity, 0);
// Dispatch custom event
window.dispatchEvent(new CustomEvent('cart-updated'));
shopLog.debug('Cart saved:', this.cart.length, 'items');
} catch (error) {
shopLog.error('Failed to save cart:', error);
}
},
// Get cart total
get cartTotal() {
return this.cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
},
// Toast notifications
showToast(message, type = 'info') {
const container = document.getElementById('toast-container');
if (!container) return;
const toast = document.createElement('div');
toast.className = `toast toast-${type} transform transition-all duration-300 mb-2`;
// Color based on type
const colors = {
success: 'bg-green-500',
error: 'bg-red-500',
warning: 'bg-yellow-500',
info: 'bg-blue-500'
};
toast.innerHTML = `
<div class="${colors[type]} text-white px-6 py-3 rounded-lg shadow-lg flex items-center space-x-3">
<span>${message}</span>
<button onclick="this.parentElement.parentElement.remove()"
class="ml-4 hover:opacity-75">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
`;
container.appendChild(toast);
// Auto-remove after 3 seconds
setTimeout(() => {
toast.style.opacity = '0';
setTimeout(() => toast.remove(), 300);
}, 3000);
},
// Format currency
formatPrice(price) {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(price);
},
// Format date
formatDate(dateString) {
if (!dateString) return '-';
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
}
};
}
// Make available globally
window.shopLayoutData = shopLayoutData;
shopLog.info('Shop layout module loaded');

View File

@@ -0,0 +1,124 @@
# middleware/theme_context.py
"""
Theme Context Middleware
Injects vendor-specific theme into request context
"""
import logging
from fastapi import Request
from sqlalchemy.orm import Session
from app.core.database import get_db
from models.database.vendor_theme import VendorTheme
logger = logging.getLogger(__name__)
class ThemeContextManager:
"""Manages theme context for vendor shops."""
@staticmethod
def get_vendor_theme(db: Session, vendor_id: int) -> dict:
"""
Get theme configuration for vendor.
Returns default theme if no custom theme is configured.
"""
theme = db.query(VendorTheme).filter(
VendorTheme.vendor_id == vendor_id,
VendorTheme.is_active == True
).first()
if theme:
return theme.to_dict()
# Return default theme
return get_default_theme()
@staticmethod
def get_default_theme() -> dict:
"""Default theme configuration"""
return {
"theme_name": "default",
"colors": {
"primary": "#6366f1",
"secondary": "#8b5cf6",
"accent": "#ec4899",
"background": "#ffffff",
"text": "#1f2937",
"border": "#e5e7eb"
},
"fonts": {
"heading": "Inter, sans-serif",
"body": "Inter, sans-serif"
},
"branding": {
"logo": None,
"logo_dark": None,
"favicon": None,
"banner": None
},
"layout": {
"style": "grid",
"header": "fixed",
"product_card": "modern"
},
"social_links": {},
"custom_css": None,
"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",
}
}
async def theme_context_middleware(request: Request, call_next):
"""
Middleware to inject theme context into request state.
This runs AFTER vendor_context_middleware has set request.state.vendor
"""
# Only inject theme for shop pages (not admin or API)
if hasattr(request.state, 'vendor') and request.state.vendor:
vendor = request.state.vendor
# Get database session
db_gen = get_db()
db = next(db_gen)
try:
# Get vendor theme
theme = ThemeContextManager.get_vendor_theme(db, vendor.id)
request.state.theme = theme
logger.debug(
f"Theme loaded for vendor {vendor.name}: {theme['theme_name']}"
)
except Exception as e:
logger.error(f"Failed to load theme for vendor {vendor.id}: {e}")
# Fallback to default theme
request.state.theme = ThemeContextManager.get_default_theme()
finally:
db.close()
else:
# No vendor context, use default theme
request.state.theme = ThemeContextManager.get_default_theme()
response = await call_next(request)
return response
def get_current_theme(request: Request) -> dict:
"""Helper function to get current theme from request state."""
return getattr(request.state, "theme", ThemeContextManager.get_default_theme())
# Add to main.py after vendor_context_middleware:
"""
# Add theme context middleware (must be after vendor context)
app.middleware("http")(theme_context_middleware)
"""

View File

@@ -0,0 +1,143 @@
# models/database/vendor_theme.py
"""
Vendor Theme Configuration Model
Allows each vendor to customize their shop's appearance
"""
from datetime import datetime, timezone
from sqlalchemy import Column, Integer, String, Boolean, Text, JSON, DateTime, ForeignKey
from sqlalchemy.orm import relationship
from app.core.database import Base
class VendorTheme(Base):
"""
Stores theme configuration for each vendor's shop.
Each vendor can have:
- Custom colors (primary, secondary, accent)
- Custom fonts
- Custom logo and favicon
- Custom CSS overrides
- Layout preferences
"""
__tablename__ = "vendor_themes"
id = Column(Integer, primary_key=True, index=True)
vendor_id = Column(Integer, ForeignKey("vendors.id", ondelete="CASCADE"), nullable=False, unique=True)
# Basic Theme Settings
theme_name = Column(String(100), default="default") # e.g., "modern", "classic", "minimal"
is_active = Column(Boolean, default=True)
# Color Scheme (JSON for flexibility)
colors = Column(JSON, default={
"primary": "#6366f1", # Indigo
"secondary": "#8b5cf6", # Purple
"accent": "#ec4899", # Pink
"background": "#ffffff", # White
"text": "#1f2937", # Gray-800
"border": "#e5e7eb" # Gray-200
})
# Typography
font_family_heading = Column(String(100), default="Inter, sans-serif")
font_family_body = Column(String(100), default="Inter, sans-serif")
# Branding Assets
logo_url = Column(String(500), nullable=True) # Path to vendor logo
logo_dark_url = Column(String(500), nullable=True) # Dark mode logo
favicon_url = Column(String(500), nullable=True) # Favicon
banner_url = Column(String(500), nullable=True) # Homepage banner
# Layout Preferences
layout_style = Column(String(50), default="grid") # grid, list, masonry
header_style = Column(String(50), default="fixed") # fixed, static, transparent
product_card_style = Column(String(50), default="modern") # modern, classic, minimal
# Custom CSS (for advanced customization)
custom_css = Column(Text, nullable=True)
# Social Media Links
social_links = Column(JSON, default={}) # {facebook: "url", instagram: "url", etc.}
# SEO & Meta
meta_title_template = Column(String(200), nullable=True) # e.g., "{product_name} - {shop_name}"
meta_description = Column(Text, nullable=True)
# Timestamps
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
updated_at = Column(
DateTime(timezone=True),
default=lambda: datetime.now(timezone.utc),
onupdate=lambda: datetime.now(timezone.utc),
)
# Relationships
vendor = relationship("Vendor", back_populates="theme")
def __repr__(self):
return f"<VendorTheme(vendor_id={self.vendor_id}, theme_name='{self.theme_name}')>"
@property
def primary_color(self):
"""Get primary color from JSON"""
return self.colors.get("primary", "#6366f1")
@property
def css_variables(self):
"""Generate CSS custom properties from theme config"""
return {
"--color-primary": self.colors.get("primary", "#6366f1"),
"--color-secondary": self.colors.get("secondary", "#8b5cf6"),
"--color-accent": self.colors.get("accent", "#ec4899"),
"--color-background": self.colors.get("background", "#ffffff"),
"--color-text": self.colors.get("text", "#1f2937"),
"--color-border": self.colors.get("border", "#e5e7eb"),
"--font-heading": self.font_family_heading,
"--font-body": self.font_family_body,
}
def to_dict(self):
"""Convert theme to dictionary for template rendering"""
return {
"theme_name": self.theme_name,
"colors": self.colors,
"fonts": {
"heading": self.font_family_heading,
"body": self.font_family_body,
},
"branding": {
"logo": self.logo_url,
"logo_dark": self.logo_dark_url,
"favicon": self.favicon_url,
"banner": self.banner_url,
},
"layout": {
"style": self.layout_style,
"header": self.header_style,
"product_card": self.product_card_style,
},
"social_links": self.social_links,
"custom_css": self.custom_css,
"css_variables": self.css_variables,
}
# Update Vendor model to include theme relationship
"""
Add to models/database/vendor.py:
theme = relationship(
"VendorTheme",
back_populates="vendor",
uselist=False,
cascade="all, delete-orphan"
)
@property
def active_theme(self):
'''Get vendor's active theme or return default'''
if self.theme and self.theme.is_active:
return self.theme
return None
"""

View File

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

View File

@@ -0,0 +1,567 @@
# Vendor Domains - Refactored Architecture Implementation Guide
## Overview
This document explains the refactored vendor domains implementation that follows your application's architecture patterns with proper separation of concerns.
## Architecture Summary
The implementation follows these key principles:
1. **Separation of Concerns**: Endpoints, service layer, schemas, and database models are separated
2. **Exception-Based Error Handling**: Custom exceptions with proper HTTP status codes
3. **Service Layer Pattern**: Business logic isolated in service classes
4. **Pydantic Validation**: Input validation using Pydantic schemas
5. **Consistent Response Format**: Standardized response models
## File Structure
```
app/
├── api/
│ └── v1/
│ └── admin/
│ └── vendor_domains.py # HTTP endpoints (NEW)
├── services/
│ └── vendor_domain_service.py # Business logic (NEW)
├── exceptions/
│ ├── __init__.py # Updated exports
│ └── vendor_domain.py # Domain exceptions (NEW)
models/
├── schema/
│ └── vendor_domain.py # Pydantic schemas (NEW)
└── database/
├── vendor.py # Updated Vendor model
└── vendor_domain.py # VendorDomain model
```
## Components
### 1. Exceptions (`app/exceptions/vendor_domain.py`)
Custom exceptions for domain operations:
```python
# Resource Not Found (404)
- VendorDomainNotFoundException
# Conflicts (409)
- VendorDomainAlreadyExistsException
# Validation (422)
- InvalidDomainFormatException
- ReservedDomainException
# Business Logic (400)
- DomainNotVerifiedException
- DomainVerificationFailedException
- DomainAlreadyVerifiedException
- MultiplePrimaryDomainsException
- MaxDomainsReachedException
- UnauthorizedDomainAccessException
# External Service (502)
- DNSVerificationException
```
**Key Features:**
- Inherit from appropriate base exceptions
- Include relevant context in `details` dict
- Proper HTTP status codes
- Clear, actionable error messages
### 2. Pydantic Schemas (`models/schema/vendor_domain.py`)
Input validation and response models:
```python
# Request Schemas
- VendorDomainCreate # For adding domains
- VendorDomainUpdate # For updating settings
# Response Schemas
- VendorDomainResponse # Single domain
- VendorDomainListResponse # List of domains
- DomainVerificationInstructions # DNS instructions
- DomainVerificationResponse # Verification result
- DomainDeletionResponse # Deletion confirmation
```
**Validation Features:**
- Domain normalization (lowercase, remove protocol)
- Reserved subdomain checking
- Format validation using regex
- Field validators with custom error messages
### 3. Service Layer (`app/services/vendor_domain_service.py`)
Business logic and database operations:
```python
class VendorDomainService:
# Core Operations
add_domain() # Add custom domain
get_vendor_domains() # List vendor's domains
get_domain_by_id() # Get single domain
update_domain() # Update settings
delete_domain() # Remove domain
# Verification
verify_domain() # DNS verification
get_verification_instructions() # Get DNS instructions
# Private Helpers
_get_vendor_by_id_or_raise() # Vendor lookup
_check_domain_limit() # Enforce max domains
_domain_exists() # Check uniqueness
_validate_domain_format() # Format validation
_unset_primary_domains() # Primary domain logic
```
**Service Pattern:**
- All database operations in service layer
- Raises custom exceptions (not HTTPException)
- Transaction management (commit/rollback)
- Comprehensive logging
- Helper methods with `_` prefix for internal use
### 4. API Endpoints (`app/api/v1/admin/vendor_domains.py`)
HTTP layer for domain management:
```python
# Endpoints
POST /vendors/{vendor_id}/domains # Add domain
GET /vendors/{vendor_id}/domains # List domains
GET /vendors/domains/{domain_id} # Get domain
PUT /vendors/domains/{domain_id} # Update domain
DELETE /vendors/domains/{domain_id} # Delete domain
POST /vendors/domains/{domain_id}/verify # Verify ownership
GET /vendors/domains/{domain_id}/verification-instructions # Get instructions
```
**Endpoint Pattern:**
- Only handle HTTP concerns (request/response)
- Delegate business logic to service layer
- Use Pydantic schemas for validation
- Proper dependency injection
- Comprehensive docstrings
- No direct database access
## Comparison: Old vs New
### Old Implementation Issues
```python
# ❌ Mixed concerns
@router.post("/{vendor_id}/domains")
def add_vendor_domain(...):
# Validation in endpoint
domain = VendorDomain.normalize_domain(domain_data.domain)
# Business logic in endpoint
if not domain or '/' in domain:
raise HTTPException(400, "Invalid domain")
# Database operations in endpoint
existing = db.query(VendorDomain).filter(...).first()
if existing:
raise HTTPException(409, "Domain exists")
# Direct database access
domain = VendorDomain(...)
db.add(domain)
db.commit()
```
**Problems:**
- Business logic mixed with HTTP layer
- HTTPException instead of custom exceptions
- No service layer separation
- Direct database access in endpoints
- Validation scattered across endpoint
- Hard to test business logic
### New Implementation
```python
# ✅ Proper separation
# Endpoint (HTTP layer only)
@router.post("/{vendor_id}/domains", response_model=VendorDomainResponse)
def add_vendor_domain(
vendor_id: int = Path(..., gt=0),
domain_data: VendorDomainCreate = Body(...), # Pydantic validation
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user),
):
"""Add domain - delegates to service layer"""
domain = vendor_domain_service.add_domain(db, vendor_id, domain_data)
return VendorDomainResponse(...mapping...)
# Service (Business logic)
class VendorDomainService:
def add_domain(self, db, vendor_id, domain_data):
"""Business logic and database operations"""
# Verify vendor
vendor = self._get_vendor_by_id_or_raise(db, vendor_id)
# Check limits
self._check_domain_limit(db, vendor_id)
# Validate format
self._validate_domain_format(normalized_domain)
# Check uniqueness
if self._domain_exists(db, normalized_domain):
raise VendorDomainAlreadyExistsException(...) # Custom exception
# Business logic
if domain_data.is_primary:
self._unset_primary_domains(db, vendor_id)
# Database operations
new_domain = VendorDomain(...)
db.add(new_domain)
db.commit()
return new_domain
# Schema (Validation)
class VendorDomainCreate(BaseModel):
domain: str
is_primary: bool = False
@field_validator('domain')
def validate_domain(cls, v: str) -> str:
"""Normalize and validate domain"""
# Validation logic here
return normalized_domain
```
**Benefits:**
- Clean separation of concerns
- Custom exceptions with proper status codes
- Testable business logic
- Reusable service methods
- Centralized validation
- Easy to maintain
## Installation Steps
### 1. Add Exception File
```bash
# Create new exception file
app/exceptions/vendor_domain.py
```
Copy content from `vendor_domain_exceptions.py`
### 2. Update Exception Exports
```python
# app/exceptions/__init__.py
from .vendor_domain import (
VendorDomainNotFoundException,
VendorDomainAlreadyExistsException,
# ... other exceptions
)
__all__ = [
# ... existing exports
"VendorDomainNotFoundException",
"VendorDomainAlreadyExistsException",
# ... other exports
]
```
### 3. Add Pydantic Schemas
```bash
# Create schema file
models/schema/vendor_domain.py
```
Copy content from `vendor_domain_schema.py`
### 4. Add Service Layer
```bash
# Create service file
app/services/vendor_domain_service.py
```
Copy content from `vendor_domain_service.py`
### 5. Replace Endpoint File
```bash
# Replace existing file
app/api/v1/admin/vendor_domains.py
```
Copy content from `vendor_domains.py`
### 6. Install DNS Library
```bash
pip install dnspython
```
Required for DNS verification functionality.
## Usage Examples
### Adding a Domain
```python
# Request
POST /api/v1/admin/vendors/1/domains
{
"domain": "myshop.com",
"is_primary": true
}
# Response (201)
{
"id": 1,
"vendor_id": 1,
"domain": "myshop.com",
"is_primary": true,
"is_active": false,
"is_verified": false,
"ssl_status": "pending",
"verification_token": "abc123...",
"verified_at": null,
"created_at": "2025-01-15T10:00:00Z",
"updated_at": "2025-01-15T10:00:00Z"
}
```
### Domain Verification
```python
# Step 1: Get verification instructions
GET /api/v1/admin/vendors/domains/1/verification-instructions
# Response
{
"domain": "myshop.com",
"verification_token": "abc123xyz...",
"instructions": {
"step1": "Go to your domain's DNS settings",
"step2": "Add a new TXT record with the following values:",
"step3": "Wait for DNS propagation (5-15 minutes)",
"step4": "Click 'Verify Domain' button"
},
"txt_record": {
"type": "TXT",
"name": "_letzshop-verify",
"value": "abc123xyz...",
"ttl": 3600
}
}
# Step 2: Vendor adds DNS record
# _letzshop-verify.myshop.com TXT "abc123xyz..."
# Step 3: Verify domain
POST /api/v1/admin/vendors/domains/1/verify
# Response (200)
{
"message": "Domain myshop.com verified successfully",
"domain": "myshop.com",
"verified_at": "2025-01-15T10:15:00Z",
"is_verified": true
}
```
### Activating Domain
```python
# After verification, activate domain
PUT /api/v1/admin/vendors/domains/1
{
"is_active": true
}
# Response (200)
{
"id": 1,
"vendor_id": 1,
"domain": "myshop.com",
"is_primary": true,
"is_active": true,
"is_verified": true,
"ssl_status": "pending",
"verified_at": "2025-01-15T10:15:00Z",
...
}
```
## Error Handling Examples
### Domain Already Exists
```python
POST /api/v1/admin/vendors/1/domains
{
"domain": "existing.com"
}
# 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
}
}
```
### Invalid Domain Format
```python
POST /api/v1/admin/vendors/1/domains
{
"domain": "admin.example.com" # Reserved subdomain
}
# Response (422 Validation Error)
{
"error_code": "RESERVED_DOMAIN",
"message": "Domain cannot use reserved subdomain: admin",
"status_code": 422,
"details": {
"domain": "admin.example.com",
"reserved_part": "admin"
}
}
```
### Verification Failed
```python
POST /api/v1/admin/vendors/domains/1/verify
# Response (400 Bad Request)
{
"error_code": "DOMAIN_VERIFICATION_FAILED",
"message": "Domain verification failed for 'myshop.com': Verification token not found in DNS records",
"status_code": 400,
"details": {
"domain": "myshop.com",
"reason": "Verification token not found in DNS records"
}
}
```
## Testing
### Unit Tests
```python
# tests/unit/services/test_vendor_domain_service.py
def test_add_domain_success(db_session):
"""Test successful domain addition"""
service = VendorDomainService()
domain_data = VendorDomainCreate(
domain="test.com",
is_primary=True
)
domain = service.add_domain(db_session, vendor_id=1, domain_data=domain_data)
assert domain.domain == "test.com"
assert domain.is_primary is True
assert domain.is_verified is False
def test_add_domain_already_exists(db_session):
"""Test adding duplicate domain raises exception"""
service = VendorDomainService()
with pytest.raises(VendorDomainAlreadyExistsException):
service.add_domain(db_session, vendor_id=1, domain_data=...)
```
### Integration Tests
```python
# tests/integration/api/test_vendor_domains.py
def test_add_domain_endpoint(client, admin_headers):
"""Test domain addition endpoint"""
response = client.post(
"/api/v1/admin/vendors/1/domains",
json={"domain": "newshop.com", "is_primary": False},
headers=admin_headers
)
assert response.status_code == 201
data = response.json()
assert data["domain"] == "newshop.com"
assert data["is_verified"] is False
def test_verify_domain_not_found(client, admin_headers):
"""Test verification with non-existent domain"""
response = client.post(
"/api/v1/admin/vendors/domains/99999/verify",
headers=admin_headers
)
assert response.status_code == 404
assert response.json()["error_code"] == "VENDOR_DOMAIN_NOT_FOUND"
```
## Benefits of This Architecture
### 1. Maintainability
- Clear separation makes code easy to understand
- Changes isolated to appropriate layers
- Easy to locate and fix bugs
### 2. Testability
- Service layer can be unit tested independently
- Mock dependencies easily
- Integration tests for endpoints
### 3. Reusability
- Service methods can be called from anywhere
- Schemas reused across endpoints
- Exceptions standardized
### 4. Scalability
- Add new endpoints without duplicating logic
- Extend service layer for new features
- Easy to add caching, queuing, etc.
### 5. Error Handling
- Consistent error responses
- Proper HTTP status codes
- Detailed error information for debugging
## Next Steps
1. **Database Migration**: Create migration for vendor_domains table if not exists
2. **Middleware Update**: Update vendor detection middleware to check custom domains
3. **Frontend Integration**: Build UI for domain management
4. **SSL Automation**: Add automatic SSL certificate provisioning
5. **Monitoring**: Add logging and monitoring for domain operations
6. **Rate Limiting**: Implement rate limits for domain additions
7. **Webhooks**: Add webhooks for domain status changes
## Conclusion
This refactored implementation follows your application's architecture patterns:
- ✅ Proper separation of concerns
- ✅ Exception-based error handling
- ✅ Service layer for business logic
- ✅ Pydantic schemas for validation
- ✅ Clean, maintainable code
- ✅ Consistent with existing patterns (vendors.py example)
The code is now production-ready, maintainable, and follows best practices!