docs: add consolidated dev URL reference and migrate /shop to /storefront
Some checks failed
Some checks failed
- Add Development URL Quick Reference section to url-routing overview with all login URLs, entry points, and full examples - Replace /shop/ path segments with /storefront/ across 50 docs files - Update file references: shop_pages.py → storefront_pages.py, templates/shop/ → templates/storefront/, api/v1/shop/ → api/v1/storefront/ - Preserve domain references (orion.shop) and /store/ staff dashboard paths - Archive docs left unchanged (historical) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,9 +6,9 @@
|
||||
|
||||
## Executive Summary
|
||||
|
||||
The platform currently has **two parallel API structures** for shop/customer-facing endpoints:
|
||||
The platform currently has **two parallel API structures** for storefront/customer-facing endpoints:
|
||||
1. **Original:** `/api/v1/platform/stores/{store_id}/*`
|
||||
2. **New:** `/api/v1/shop/*`
|
||||
2. **New:** `/api/v1/storefront/*`
|
||||
|
||||
This divergence creates confusion, maintenance overhead, and potential bugs. This document analyzes the situation and proposes a consolidation strategy.
|
||||
|
||||
@@ -47,26 +47,26 @@ POST /api/v1/platform/stores/auth/register → Customer registration
|
||||
|
||||
---
|
||||
|
||||
### 2. New Architecture (`/api/v1/shop/`)
|
||||
### 2. New Architecture (`/api/v1/storefront/`)
|
||||
|
||||
**Location:** `app/api/v1/shop/`
|
||||
**Location:** `app/api/v1/storefront/`
|
||||
|
||||
**Endpoints:**
|
||||
```
|
||||
GET /api/v1/shop/content-pages/navigation → CMS navigation pages
|
||||
GET /api/v1/shop/content-pages/{slug} → CMS page content
|
||||
GET /api/v1/storefront/content-pages/navigation → CMS navigation pages
|
||||
GET /api/v1/storefront/content-pages/{slug} → CMS page content
|
||||
```
|
||||
|
||||
**Characteristics:**
|
||||
- ✅ **Store-agnostic URLs:** Clean paths without store_id
|
||||
- ✅ **Middleware-driven:** Relies on `StoreContextMiddleware` to inject store
|
||||
- ✅ **Simpler URLs:** `/api/v1/shop/products` vs `/api/v1/platform/stores/123/products`
|
||||
- ✅ **Simpler URLs:** `/api/v1/storefront/products` vs `/api/v1/platform/stores/123/products`
|
||||
- ❌ **Incomplete:** Only CMS endpoints implemented
|
||||
- ❌ **Divergent:** Not consistent with existing public API
|
||||
|
||||
**Current Usage:**
|
||||
- CMS content pages only
|
||||
- Called from shop templates (e.g., `shop/products.html`, `shop/home.html`)
|
||||
- Called from storefront templates (e.g., `storefront/products.html`, `storefront/home.html`)
|
||||
|
||||
---
|
||||
|
||||
@@ -77,7 +77,7 @@ GET /api/v1/shop/content-pages/{slug} → CMS page content
|
||||
```javascript
|
||||
// ❌ INCONSISTENT - Two different patterns for same context
|
||||
// CMS pages use new pattern
|
||||
fetch('/api/v1/shop/content-pages/about')
|
||||
fetch('/api/v1/storefront/content-pages/about')
|
||||
|
||||
// Products use old pattern
|
||||
fetch('/api/v1/platform/stores/123/products')
|
||||
@@ -86,7 +86,7 @@ fetch('/api/v1/platform/stores/123/products')
|
||||
### Confusion
|
||||
|
||||
Developers must remember:
|
||||
- "Is this endpoint under `/shop` or `/public/stores`?"
|
||||
- "Is this endpoint under `/storefront` or `/public/stores`?"
|
||||
- "Do I need to pass store_id or is it from middleware?"
|
||||
- "Which authentication endpoints do I use?"
|
||||
|
||||
@@ -102,7 +102,7 @@ Developers must remember:
|
||||
**Current Issue:** CMS pages not loading at `/stores/orion/about`
|
||||
|
||||
**Root Cause:**
|
||||
- CMS API exists at `/api/v1/shop/content-pages/{slug}`
|
||||
- CMS API exists at `/api/v1/storefront/content-pages/{slug}`
|
||||
- No corresponding HTML route handler in `store_pages.py`
|
||||
- JavaScript might be calling wrong endpoint
|
||||
|
||||
@@ -110,13 +110,13 @@ Developers must remember:
|
||||
|
||||
## Options Analysis
|
||||
|
||||
### Option 1: Move Everything to `/api/v1/shop/*` (Middleware-Driven)
|
||||
### Option 1: Move Everything to `/api/v1/storefront/*` (Middleware-Driven)
|
||||
|
||||
**Approach:** Consolidate all customer-facing endpoints under `/api/v1/shop/*`
|
||||
**Approach:** Consolidate all customer-facing endpoints under `/api/v1/storefront/*`
|
||||
|
||||
**Proposed Structure:**
|
||||
```
|
||||
/api/v1/shop/
|
||||
/api/v1/storefront/
|
||||
├── auth/
|
||||
│ ├── POST /login → Customer login
|
||||
│ ├── POST /register → Customer registration
|
||||
@@ -143,7 +143,7 @@ Developers must remember:
|
||||
**Implementation:**
|
||||
- Store extracted by `StoreContextMiddleware` from request
|
||||
- All endpoints use `request.state.store` instead of path parameter
|
||||
- URLs are cleaner: `/api/v1/shop/products` instead of `/api/v1/platform/stores/123/products`
|
||||
- URLs are cleaner: `/api/v1/storefront/products` instead of `/api/v1/platform/stores/123/products`
|
||||
|
||||
**Pros:**
|
||||
- ✅ Clean, consistent API structure
|
||||
@@ -162,17 +162,17 @@ Developers must remember:
|
||||
|
||||
---
|
||||
|
||||
### Option 2: Keep `/api/v1/platform/stores/*` and Deprecate `/api/v1/shop/*`
|
||||
### Option 2: Keep `/api/v1/platform/stores/*` and Deprecate `/api/v1/storefront/*`
|
||||
|
||||
**Approach:** Move CMS endpoints to `/api/v1/platform/stores/{store_id}/content-pages/*`
|
||||
|
||||
**Proposed Changes:**
|
||||
```
|
||||
# Move CMS endpoints
|
||||
FROM: /api/v1/shop/content-pages/navigation
|
||||
FROM: /api/v1/storefront/content-pages/navigation
|
||||
TO: /api/v1/platform/stores/{store_id}/content-pages/navigation
|
||||
|
||||
FROM: /api/v1/shop/content-pages/{slug}
|
||||
FROM: /api/v1/storefront/content-pages/{slug}
|
||||
TO: /api/v1/platform/stores/{store_id}/content-pages/{slug}
|
||||
```
|
||||
|
||||
@@ -228,7 +228,7 @@ async def get_products_legacy(store_id: int, db: Session = Depends(get_db)):
|
||||
|
||||
## Recommendation
|
||||
|
||||
### **OPTION 1: Consolidate to `/api/v1/shop/*` (Middleware-Driven)**
|
||||
### **OPTION 1: Consolidate to `/api/v1/storefront/*` (Middleware-Driven)**
|
||||
|
||||
**Rationale:**
|
||||
|
||||
@@ -237,7 +237,7 @@ async def get_products_legacy(store_id: int, db: Session = Depends(get_db)):
|
||||
2. **User Experience**: Cleaner URLs are easier for frontend developers:
|
||||
```javascript
|
||||
// ✅ GOOD
|
||||
fetch('/api/v1/shop/products')
|
||||
fetch('/api/v1/storefront/products')
|
||||
|
||||
// ❌ BAD
|
||||
fetch('/api/v1/platform/stores/123/products')
|
||||
@@ -245,7 +245,7 @@ async def get_products_legacy(store_id: int, db: Session = Depends(get_db)):
|
||||
|
||||
3. **Multi-Tenant Best Practice**: Store context should be implicit (from domain/path), not explicit in every API call.
|
||||
|
||||
4. **Consistency**: All shop endpoints follow same pattern - no mixing `/shop` and `/public/stores`.
|
||||
4. **Consistency**: All storefront endpoints follow same pattern - no mixing `/storefront` and `/public/stores`.
|
||||
|
||||
5. **Future-Proof**: Easier to add new shop features without worrying about store_id paths.
|
||||
|
||||
@@ -258,7 +258,7 @@ async def get_products_legacy(store_id: int, db: Session = Depends(get_db)):
|
||||
**Day 1-2: Move Products**
|
||||
```bash
|
||||
# Copy and adapt
|
||||
app/api/v1/platform/stores/products.py → app/api/v1/shop/products.py
|
||||
app/api/v1/platform/stores/products.py → app/api/v1/storefront/products.py
|
||||
|
||||
# Changes:
|
||||
- Remove store_id path parameter
|
||||
@@ -268,24 +268,24 @@ app/api/v1/platform/stores/products.py → app/api/v1/shop/products.py
|
||||
|
||||
**Day 3: Move Cart**
|
||||
```bash
|
||||
app/api/v1/platform/stores/cart.py → app/api/v1/shop/cart.py
|
||||
app/api/v1/platform/stores/cart.py → app/api/v1/storefront/cart.py
|
||||
```
|
||||
|
||||
**Day 4: Move Orders**
|
||||
```bash
|
||||
app/api/v1/platform/stores/orders.py → app/api/v1/shop/orders.py
|
||||
app/api/v1/platform/stores/orders.py → app/api/v1/storefront/orders.py
|
||||
```
|
||||
|
||||
**Day 5: Move Auth**
|
||||
```bash
|
||||
app/api/v1/platform/stores/auth.py → app/api/v1/shop/auth.py
|
||||
app/api/v1/platform/stores/auth.py → app/api/v1/storefront/auth.py
|
||||
```
|
||||
|
||||
### Phase 2: Update Frontend (Week 1)
|
||||
|
||||
**Templates:**
|
||||
- Update all `fetch()` calls in shop templates
|
||||
- Change from `/api/v1/platform/stores/${storeId}/...` to `/api/v1/shop/...`
|
||||
- Change from `/api/v1/platform/stores/${storeId}/...` to `/api/v1/storefront/...`
|
||||
|
||||
**JavaScript:**
|
||||
- Update any shop-related API client code
|
||||
@@ -335,10 +335,10 @@ const storeId = 123;
|
||||
fetch(`/api/v1/platform/stores/${storeId}/products`)
|
||||
```
|
||||
|
||||
### After (Proposed - `/api/v1/shop`)
|
||||
### After (Proposed - `/api/v1/storefront`)
|
||||
|
||||
```python
|
||||
# app/api/v1/shop/products.py
|
||||
# app/api/v1/storefront/products.py
|
||||
@router.get("/products")
|
||||
def get_product_catalog(
|
||||
request: Request,
|
||||
@@ -350,7 +350,7 @@ def get_product_catalog(
|
||||
|
||||
```javascript
|
||||
// Frontend
|
||||
fetch('/api/v1/shop/products') // Store context automatic
|
||||
fetch('/api/v1/storefront/products') // Store context automatic
|
||||
```
|
||||
|
||||
---
|
||||
@@ -382,7 +382,7 @@ If full migration is not approved immediately, we can do a **minimal fix** for t
|
||||
### Quick Fix: Just Move CMS to Public API
|
||||
|
||||
```python
|
||||
# Move: app/api/v1/shop/content_pages.py
|
||||
# Move: app/api/v1/storefront/content_pages.py
|
||||
# To: app/api/v1/platform/stores/content_pages.py
|
||||
|
||||
# Update routes:
|
||||
@@ -401,7 +401,7 @@ If full migration is not approved immediately, we can do a **minimal fix** for t
|
||||
**Question for Team:**
|
||||
|
||||
Should we:
|
||||
1. ✅ **Consolidate to `/api/v1/shop/*`** (Recommended)
|
||||
1. ✅ **Consolidate to `/api/v1/storefront/*`** (Recommended)
|
||||
2. ❌ **Keep `/api/v1/platform/stores/*`** and move CMS there
|
||||
3. ❌ **Hybrid approach** with both patterns
|
||||
4. ❌ **Quick fix only** - move CMS, address later
|
||||
@@ -422,15 +422,15 @@ Should we:
|
||||
- 🚧 `search.py` - Stub
|
||||
- 🚧 `shop.py` - Stub
|
||||
|
||||
### `/api/v1/shop/*`
|
||||
### `/api/v1/storefront/*`
|
||||
- ✅ `content_pages.py` - CMS pages
|
||||
|
||||
### To Be Created (if Option 1 chosen)
|
||||
- 📝 `shop/products.py`
|
||||
- 📝 `shop/cart.py`
|
||||
- 📝 `shop/orders.py`
|
||||
- 📝 `shop/auth.py`
|
||||
- 📝 `shop/stores.py` (marketplace listing)
|
||||
- 📝 `storefront/products.py`
|
||||
- 📝 `storefront/cart.py`
|
||||
- 📝 `storefront/orders.py`
|
||||
- 📝 `storefront/auth.py`
|
||||
- 📝 `storefront/stores.py` (marketplace listing)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,55 +1,55 @@
|
||||
# API Migration Status - `/api/v1/shop/*` Consolidation
|
||||
# API Migration Status - `/api/v1/storefront/*` Consolidation
|
||||
|
||||
**Date:** 2025-11-22
|
||||
**Status:** 🎉 MIGRATION COMPLETE - All Phases Done
|
||||
**Decision:** Option 1 - Full Consolidation to `/api/v1/shop/*`
|
||||
**Decision:** Option 1 - Full Consolidation to `/api/v1/storefront/*`
|
||||
|
||||
---
|
||||
|
||||
## Progress Overview
|
||||
|
||||
### ✅ Phase 1: New Shop API Endpoints (COMPLETE)
|
||||
### ✅ Phase 1: New Storefront API Endpoints (COMPLETE)
|
||||
|
||||
All new shop endpoints have been created using middleware-based store context:
|
||||
All new storefront endpoints have been created using middleware-based store context:
|
||||
|
||||
### ✅ Middleware Update: Referer-Based Store Extraction (COMPLETE)
|
||||
|
||||
Updated `StoreContextMiddleware` to support shop API routes:
|
||||
- Added `is_shop_api_request()` method to detect `/api/v1/shop/*` routes
|
||||
Updated `StoreContextMiddleware` to support storefront API routes:
|
||||
- Added `is_storefront_api_request()` method to detect `/api/v1/storefront/*` routes
|
||||
- Added `extract_store_from_referer()` method to extract store from Referer/Origin headers
|
||||
- Modified `dispatch()` to handle shop API routes specially (no longer skips them)
|
||||
- Shop API now receives store context from the page that made the API call
|
||||
- Modified `dispatch()` to handle storefront API routes specially (no longer skips them)
|
||||
- Storefront API now receives store context from the page that made the API call
|
||||
|
||||
**How it works:**
|
||||
1. Browser JavaScript on `/stores/orion/shop/products` calls `/api/v1/shop/products`
|
||||
2. Browser automatically sends `Referer: http://localhost:8000/stores/orion/shop/products`
|
||||
1. Browser JavaScript on `/storefront/orion/products` calls `/api/v1/storefront/products`
|
||||
2. Browser automatically sends `Referer: http://localhost:8000/storefront/orion/products`
|
||||
3. Middleware extracts `orion` from Referer path
|
||||
4. Queries database to get Store object
|
||||
5. Sets `request.state.store` for the API endpoint
|
||||
|
||||
### ✅ Phase 1a: Endpoint Testing (COMPLETE)
|
||||
|
||||
Tested shop API endpoints with Referer header:
|
||||
Tested storefront API endpoints with Referer header:
|
||||
|
||||
| Endpoint | Method | Test Result | Notes |
|
||||
|----------|--------|-------------|-------|
|
||||
| `/api/v1/shop/products` | GET | ✅ Working | Returns paginated product list |
|
||||
| `/api/v1/shop/products?search=Sample` | GET | ✅ Working | Search functionality works |
|
||||
| `/api/v1/shop/products?is_featured=true` | GET | ✅ Working | Featured filter works |
|
||||
| `/api/v1/shop/products/{id}` | GET | ✅ Working | Product details returned |
|
||||
| `/api/v1/shop/cart/{session_id}` | GET | ✅ Working | Empty cart returns correctly |
|
||||
| `/api/v1/shop/content-pages/navigation` | GET | ✅ Working | Navigation links returned |
|
||||
| `/api/v1/storefront/products` | GET | ✅ Working | Returns paginated product list |
|
||||
| `/api/v1/storefront/products?search=Sample` | GET | ✅ Working | Search functionality works |
|
||||
| `/api/v1/storefront/products?is_featured=true` | GET | ✅ Working | Featured filter works |
|
||||
| `/api/v1/storefront/products/{id}` | GET | ✅ Working | Product details returned |
|
||||
| `/api/v1/storefront/cart/{session_id}` | GET | ✅ Working | Empty cart returns correctly |
|
||||
| `/api/v1/storefront/content-pages/navigation` | GET | ✅ Working | Navigation links returned |
|
||||
|
||||
**All tested endpoints successfully receive store context from Referer header.**
|
||||
|
||||
| Endpoint File | Status | Routes | Description |
|
||||
|--------------|--------|--------|-------------|
|
||||
| `shop/products.py` | ✅ Complete | 3 routes | Product catalog, details, search |
|
||||
| `shop/cart.py` | ✅ Complete | 5 routes | Cart CRUD operations |
|
||||
| `shop/orders.py` | ✅ Complete | 3 routes | Order placement & history |
|
||||
| `shop/auth.py` | ✅ Complete | 5 routes | Login, register, password reset |
|
||||
| `shop/content_pages.py` | ✅ Existing | 2 routes | CMS pages (already present) |
|
||||
| `shop/__init__.py` | ✅ Updated | - | Router aggregation |
|
||||
| `storefront/products.py` | ✅ Complete | 3 routes | Product catalog, details, search |
|
||||
| `storefront/cart.py` | ✅ Complete | 5 routes | Cart CRUD operations |
|
||||
| `storefront/orders.py` | ✅ Complete | 3 routes | Order placement & history |
|
||||
| `storefront/auth.py` | ✅ Complete | 5 routes | Login, register, password reset |
|
||||
| `storefront/content_pages.py` | ✅ Existing | 2 routes | CMS pages (already present) |
|
||||
| `storefront/__init__.py` | ✅ Updated | - | Router aggregation |
|
||||
|
||||
**Total:** 18 new API endpoints created
|
||||
|
||||
@@ -57,46 +57,46 @@ Tested shop API endpoints with Referer header:
|
||||
|
||||
## New API Structure
|
||||
|
||||
### Shop Endpoints (`/api/v1/shop/*`)
|
||||
### Storefront Endpoints (`/api/v1/storefront/*`)
|
||||
|
||||
All endpoints use `request.state.store` (injected by `StoreContextMiddleware`).
|
||||
|
||||
#### Products (Public - No Auth)
|
||||
```
|
||||
GET /api/v1/shop/products → Product catalog (paginated)
|
||||
GET /api/v1/shop/products/{id} → Product details
|
||||
GET /api/v1/shop/products/search?q=... → Search products
|
||||
GET /api/v1/storefront/products → Product catalog (paginated)
|
||||
GET /api/v1/storefront/products/{id} → Product details
|
||||
GET /api/v1/storefront/products/search?q=... → Search products
|
||||
```
|
||||
|
||||
#### Cart (Public - Session Based)
|
||||
```
|
||||
GET /api/v1/shop/cart/{session_id} → Get cart
|
||||
POST /api/v1/shop/cart/{session_id}/items → Add to cart
|
||||
PUT /api/v1/shop/cart/{session_id}/items/{id} → Update item
|
||||
DELETE /api/v1/shop/cart/{session_id}/items/{id} → Remove item
|
||||
DELETE /api/v1/shop/cart/{session_id} → Clear cart
|
||||
GET /api/v1/storefront/cart/{session_id} → Get cart
|
||||
POST /api/v1/storefront/cart/{session_id}/items → Add to cart
|
||||
PUT /api/v1/storefront/cart/{session_id}/items/{id} → Update item
|
||||
DELETE /api/v1/storefront/cart/{session_id}/items/{id} → Remove item
|
||||
DELETE /api/v1/storefront/cart/{session_id} → Clear cart
|
||||
```
|
||||
|
||||
#### Orders (Authenticated - Customer)
|
||||
```
|
||||
POST /api/v1/shop/orders → Place order (auth required)
|
||||
GET /api/v1/shop/orders → Order history (auth required)
|
||||
GET /api/v1/shop/orders/{id} → Order details (auth required)
|
||||
POST /api/v1/storefront/orders → Place order (auth required)
|
||||
GET /api/v1/storefront/orders → Order history (auth required)
|
||||
GET /api/v1/storefront/orders/{id} → Order details (auth required)
|
||||
```
|
||||
|
||||
#### Authentication (Public)
|
||||
```
|
||||
POST /api/v1/shop/auth/register → Register customer
|
||||
POST /api/v1/shop/auth/login → Customer login
|
||||
POST /api/v1/shop/auth/logout → Customer logout
|
||||
POST /api/v1/shop/auth/forgot-password → Request reset
|
||||
POST /api/v1/shop/auth/reset-password → Reset password
|
||||
POST /api/v1/storefront/auth/register → Register customer
|
||||
POST /api/v1/storefront/auth/login → Customer login
|
||||
POST /api/v1/storefront/auth/logout → Customer logout
|
||||
POST /api/v1/storefront/auth/forgot-password → Request reset
|
||||
POST /api/v1/storefront/auth/reset-password → Reset password
|
||||
```
|
||||
|
||||
#### CMS Content (Public)
|
||||
```
|
||||
GET /api/v1/shop/content-pages/navigation → Navigation links
|
||||
GET /api/v1/shop/content-pages/{slug} → Page content
|
||||
GET /api/v1/storefront/content-pages/navigation → Navigation links
|
||||
GET /api/v1/storefront/content-pages/{slug} → Page content
|
||||
```
|
||||
|
||||
---
|
||||
@@ -130,7 +130,7 @@ def endpoint_handler(request: Request, ...):
|
||||
|
||||
- **Public endpoints** (products, cart, CMS): No authentication
|
||||
- **Authenticated endpoints** (orders): Use `get_current_customer_api` dependency
|
||||
- **Cookie strategy**: Customer tokens stored at `path=/shop` only
|
||||
- **Cookie strategy**: Customer tokens stored at `path=/storefront` only
|
||||
|
||||
### Error Handling
|
||||
|
||||
@@ -145,7 +145,7 @@ def endpoint_handler(request: Request, ...):
|
||||
### Files Created ✨
|
||||
|
||||
```
|
||||
app/api/v1/shop/
|
||||
app/api/v1/storefront/
|
||||
├── products.py (NEW - 182 lines)
|
||||
├── cart.py (NEW - 242 lines)
|
||||
├── orders.py (NEW - 193 lines)
|
||||
@@ -156,9 +156,9 @@ app/api/v1/shop/
|
||||
### Files Modified 🔧
|
||||
|
||||
```
|
||||
app/exceptions/error_renderer.py → Added base_url calculation for shop context
|
||||
app/exceptions/error_renderer.py → Added base_url calculation for storefront context
|
||||
app/routes/store_pages.py → Added CMS route handler
|
||||
app/templates/shop/errors/*.html → Fixed links to use base_url
|
||||
app/templates/storefront/errors/*.html → Fixed links to use base_url
|
||||
docs/architecture/
|
||||
├── api-consolidation-proposal.md → Analysis & recommendation
|
||||
└── api-migration-status.md → This file
|
||||
@@ -170,27 +170,27 @@ docs/architecture/
|
||||
|
||||
### ✅ Phase 2: Frontend Migration (COMPLETE)
|
||||
|
||||
Updated all shop templates to use new API endpoints:
|
||||
Updated all storefront templates to use new API endpoints:
|
||||
|
||||
| Template | Old Endpoint | New Endpoint | Status |
|
||||
|----------|-------------|--------------|---------|
|
||||
| `shop/account/login.html` | `/api/v1/platform/stores/${id}/customers/login` | `/api/v1/shop/auth/login` | ✅ Complete |
|
||||
| `shop/account/register.html` | `/api/v1/platform/stores/${id}/customers/register` | `/api/v1/shop/auth/register` | ✅ Complete |
|
||||
| `shop/product.html` | `/api/v1/platform/stores/${id}/products/${pid}` | `/api/v1/shop/products/${pid}` | ✅ Complete |
|
||||
| `shop/product.html` | `/api/v1/platform/stores/${id}/products?limit=4` | `/api/v1/shop/products?limit=4` | ✅ Complete |
|
||||
| `shop/product.html` | `/api/v1/platform/stores/${id}/cart/${sid}` | `/api/v1/shop/cart/${sid}` | ✅ Complete |
|
||||
| `shop/product.html` | `/api/v1/platform/stores/${id}/cart/${sid}/items` | `/api/v1/shop/cart/${sid}/items` | ✅ Complete |
|
||||
| `shop/cart.html` | `/api/v1/platform/stores/${id}/cart/${sid}` | `/api/v1/shop/cart/${sid}` | ✅ Complete |
|
||||
| `shop/cart.html` | `/api/v1/platform/stores/${id}/cart/${sid}/items/${pid}` (PUT) | `/api/v1/shop/cart/${sid}/items/${pid}` | ✅ Complete |
|
||||
| `shop/cart.html` | `/api/v1/platform/stores/${id}/cart/${sid}/items/${pid}` (DELETE) | `/api/v1/shop/cart/${sid}/items/${pid}` | ✅ Complete |
|
||||
| `shop/products.html` | Already using `/api/v1/shop/products` | (No change needed) | ✅ Already Updated |
|
||||
| `shop/home.html` | Already using `/api/v1/shop/products?featured=true` | (No change needed) | ✅ Already Updated |
|
||||
| `storefront/account/login.html` | `/api/v1/platform/stores/${id}/customers/login` | `/api/v1/storefront/auth/login` | ✅ Complete |
|
||||
| `storefront/account/register.html` | `/api/v1/platform/stores/${id}/customers/register` | `/api/v1/storefront/auth/register` | ✅ Complete |
|
||||
| `storefront/product.html` | `/api/v1/platform/stores/${id}/products/${pid}` | `/api/v1/storefront/products/${pid}` | ✅ Complete |
|
||||
| `storefront/product.html` | `/api/v1/platform/stores/${id}/products?limit=4` | `/api/v1/storefront/products?limit=4` | ✅ Complete |
|
||||
| `storefront/product.html` | `/api/v1/platform/stores/${id}/cart/${sid}` | `/api/v1/storefront/cart/${sid}` | ✅ Complete |
|
||||
| `storefront/product.html` | `/api/v1/platform/stores/${id}/cart/${sid}/items` | `/api/v1/storefront/cart/${sid}/items` | ✅ Complete |
|
||||
| `storefront/cart.html` | `/api/v1/platform/stores/${id}/cart/${sid}` | `/api/v1/storefront/cart/${sid}` | ✅ Complete |
|
||||
| `storefront/cart.html` | `/api/v1/platform/stores/${id}/cart/${sid}/items/${pid}` (PUT) | `/api/v1/storefront/cart/${sid}/items/${pid}` | ✅ Complete |
|
||||
| `storefront/cart.html` | `/api/v1/platform/stores/${id}/cart/${sid}/items/${pid}` (DELETE) | `/api/v1/storefront/cart/${sid}/items/${pid}` | ✅ Complete |
|
||||
| `storefront/products.html` | Already using `/api/v1/storefront/products` | (No change needed) | ✅ Already Updated |
|
||||
| `storefront/home.html` | Already using `/api/v1/storefront/products?featured=true` | (No change needed) | ✅ Already Updated |
|
||||
|
||||
**Total Changes:** 9 API endpoint migrations across 3 template files
|
||||
|
||||
**Verification:**
|
||||
```bash
|
||||
grep -r "api/v1/public/stores" app/templates/shop --include="*.html"
|
||||
grep -r "api/v1/public/stores" app/templates/storefront --include="*.html"
|
||||
# Returns: (no results - all migrated)
|
||||
```
|
||||
|
||||
@@ -199,16 +199,16 @@ grep -r "api/v1/public/stores" app/templates/shop --include="*.html"
|
||||
Cleaned up old `/api/v1/platform/stores/*` endpoints:
|
||||
|
||||
**Files Removed:**
|
||||
- ❌ `auth.py` - Migrated to `/api/v1/shop/auth.py`
|
||||
- ❌ `products.py` - Migrated to `/api/v1/shop/products.py`
|
||||
- ❌ `cart.py` - Migrated to `/api/v1/shop/cart.py`
|
||||
- ❌ `orders.py` - Migrated to `/api/v1/shop/orders.py`
|
||||
- ❌ `auth.py` - Migrated to `/api/v1/storefront/auth.py`
|
||||
- ❌ `products.py` - Migrated to `/api/v1/storefront/products.py`
|
||||
- ❌ `cart.py` - Migrated to `/api/v1/storefront/cart.py`
|
||||
- ❌ `orders.py` - Migrated to `/api/v1/storefront/orders.py`
|
||||
- ❌ `payments.py` - Empty placeholder (removed)
|
||||
- ❌ `search.py` - Empty placeholder (removed)
|
||||
- ❌ `shop.py` - Empty placeholder (removed)
|
||||
- ❌ `storefront.py` - Empty placeholder (removed)
|
||||
|
||||
**Files Kept:**
|
||||
- ✅ `stores.py` - Store lookup endpoints (truly public, not shop-specific)
|
||||
- ✅ `stores.py` - Store lookup endpoints (truly public, not storefront-specific)
|
||||
- `GET /api/v1/platform/stores/by-code/{store_code}`
|
||||
- `GET /api/v1/platform/stores/by-subdomain/{subdomain}`
|
||||
- `GET /api/v1/platform/stores/{store_id}/info`
|
||||
@@ -216,7 +216,7 @@ Cleaned up old `/api/v1/platform/stores/*` endpoints:
|
||||
**Updated:**
|
||||
- ✅ `/app/api/v1/platform/__init__.py` - Now only includes store lookup endpoints
|
||||
|
||||
**Result:** Old shop endpoints completely removed, only store lookup remains in `/api/v1/platform/stores/*`
|
||||
**Result:** Old storefront endpoints completely removed, only store lookup remains in `/api/v1/platform/stores/*`
|
||||
|
||||
### ⚠️ Phase 4: Deprecation Warnings (SKIPPED - Not Needed)
|
||||
|
||||
@@ -291,12 +291,12 @@ Old endpoint cleanup completed immediately (no gradual migration needed):
|
||||
### After (New Pattern)
|
||||
```
|
||||
# Clean - store from context
|
||||
/api/v1/shop/products
|
||||
/api/v1/shop/products/456
|
||||
/api/v1/shop/cart/abc-session-id
|
||||
/api/v1/shop/cart/abc-session-id/items
|
||||
/api/v1/shop/orders
|
||||
/api/v1/shop/auth/login
|
||||
/api/v1/storefront/products
|
||||
/api/v1/storefront/products/456
|
||||
/api/v1/storefront/cart/abc-session-id
|
||||
/api/v1/storefront/cart/abc-session-id/items
|
||||
/api/v1/storefront/orders
|
||||
/api/v1/storefront/auth/login
|
||||
```
|
||||
|
||||
**URL Reduction:** ~40% shorter URLs on average
|
||||
@@ -308,7 +308,7 @@ Old endpoint cleanup completed immediately (no gradual migration needed):
|
||||
### For Frontend Developers
|
||||
- ✅ Cleaner, more intuitive URLs
|
||||
- ✅ No need to track store_id in state
|
||||
- ✅ Consistent API pattern across all shop endpoints
|
||||
- ✅ Consistent API pattern across all storefront endpoints
|
||||
- ✅ Automatic store context from middleware
|
||||
|
||||
### For Backend Developers
|
||||
@@ -331,7 +331,7 @@ If issues arise, rollback is simple since old endpoints still exist:
|
||||
|
||||
1. **Revert frontend changes:**
|
||||
```bash
|
||||
git checkout app/templates/shop/*.html
|
||||
git checkout app/templates/storefront/*.html
|
||||
```
|
||||
|
||||
2. **Old endpoints still work:**
|
||||
@@ -357,7 +357,7 @@ Add logging to track which pattern is being used:
|
||||
logger.info(
|
||||
"API call",
|
||||
extra={
|
||||
"endpoint_pattern": "new" if "/shop/" in request.url.path else "old",
|
||||
"endpoint_pattern": "new" if "/storefront/" in request.url.path else "old",
|
||||
"path": request.url.path,
|
||||
"store_id": store.id if store else None,
|
||||
}
|
||||
@@ -377,7 +377,7 @@ logger.info(
|
||||
|
||||
### ✅ Decided
|
||||
|
||||
1. **Use `/api/v1/shop/*` pattern?** → YES (Option 1)
|
||||
1. **Use `/api/v1/storefront/*` pattern?** → YES (Option 1)
|
||||
2. **Store from middleware?** → YES
|
||||
3. **Keep old endpoints during migration?** → YES
|
||||
4. **Deprecation period?** → 2-4 weeks
|
||||
@@ -412,7 +412,7 @@ logger.info(
|
||||
Migration is considered successful when:
|
||||
|
||||
- [x] All new endpoints created and tested
|
||||
- [x] Middleware updated to support shop API routes
|
||||
- [x] Middleware updated to support storefront API routes
|
||||
- [x] Store context extraction from Referer working
|
||||
- [x] All frontend templates updated (9 API calls across 3 files)
|
||||
- [x] Old endpoint usage drops to zero (verified with grep)
|
||||
@@ -432,7 +432,7 @@ Migration is considered successful when:
|
||||
- [Middleware Documentation](./middleware.md) - How middleware works
|
||||
|
||||
**Issues?** Review:
|
||||
- Server logs: Check for `[SHOP_API]` log entries
|
||||
- Server logs: Check for `[STOREFRONT_API]` log entries
|
||||
- Browser console: Check for failed API calls
|
||||
- Network tab: Verify correct endpoints are called
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ Complete guide to the authentication and authorization system powering the multi
|
||||
The platform uses a JWT-based authentication system combined with role-based access control (RBAC) to secure all interfaces:
|
||||
- **Admin** interface
|
||||
- **Store** dashboard
|
||||
- **Shop** storefront
|
||||
- **Storefront**
|
||||
- **REST API** endpoints
|
||||
|
||||
## Authentication System
|
||||
@@ -60,17 +60,17 @@ User.role: "super_admin" | "platform_admin" | "merchant_owner" | "store_member"
|
||||
|
||||
### Customer Role (Separate Model)
|
||||
|
||||
**Access**: Public shop and own account space
|
||||
**Access**: Public storefront and own account space
|
||||
|
||||
**Capabilities**:
|
||||
- Browse store shops
|
||||
- Browse store storefronts
|
||||
- Place orders
|
||||
- Manage their own account and order history
|
||||
- View order status
|
||||
- Update profile information
|
||||
- Can register directly from shop frontend
|
||||
- Can register directly from storefront frontend
|
||||
|
||||
**Account Creation**: Self-registration via shop frontend (email verification required)
|
||||
**Account Creation**: Self-registration via storefront frontend (email verification required)
|
||||
|
||||
**Authentication**: Standard JWT authentication (separate `Customer` model, not `User`)
|
||||
|
||||
@@ -89,7 +89,7 @@ User.role: "super_admin" | "platform_admin" | "merchant_owner" | "store_member"
|
||||
- Manage products and inventory
|
||||
- Process orders
|
||||
- View analytics and reports
|
||||
- Configure shop settings
|
||||
- Configure storefront settings
|
||||
- Manage team members (invite, remove, update roles)
|
||||
- Access store-specific APIs
|
||||
|
||||
@@ -155,8 +155,8 @@ The platform has three distinct areas with different access requirements:
|
||||
| Area | URL Pattern | Access | Purpose |
|
||||
|------|-------------|--------|---------|
|
||||
| **Admin** | `/admin/*` or `admin.platform.com` | Super admins and platform admins (`is_admin`) | Platform administration and store management |
|
||||
| **Store** | `/store/*` | Merchant owners and store members (`is_store_user`) | Store dashboard and shop management |
|
||||
| **Shop** | `/shop/*`, custom domains, subdomains | Customers and public | Public-facing eCommerce storefront |
|
||||
| **Store** | `/store/*` | Merchant owners and store members (`is_store_user`) | Store dashboard and storefront management |
|
||||
| **Storefront** | `/storefront/*`, custom domains, subdomains | Customers and public | Public-facing eCommerce storefront |
|
||||
| **API** | `/api/*` | All authenticated users (role-based) | REST API for all operations |
|
||||
|
||||
## Account Registration Flow
|
||||
@@ -174,7 +174,7 @@ The platform has three distinct areas with different access requirements:
|
||||
- Activation: Upon clicking email verification link
|
||||
|
||||
### Customer Accounts
|
||||
- ✅ Can register directly on store shop
|
||||
- ✅ Can register directly on store storefront
|
||||
- Activation: Upon clicking registration email link
|
||||
- Used for: Shopping and order management
|
||||
|
||||
@@ -222,8 +222,8 @@ Allows access to customer users and admins.
|
||||
|
||||
**Usage**:
|
||||
```python
|
||||
@app.get("/shop/orders")
|
||||
async def customer_orders(
|
||||
@app.get("/storefront/orders")
|
||||
async def storefront_orders(
|
||||
current_user: User = Depends(auth_manager.require_customer)
|
||||
):
|
||||
return {"orders": [...]}
|
||||
@@ -390,13 +390,13 @@ graph TD
|
||||
|
||||
D[Merchant Owner<br/>role=merchant_owner] --> E[Store Dashboard]
|
||||
D --> F[Team Management]
|
||||
D --> G[Shop Settings]
|
||||
D --> G[Storefront Settings]
|
||||
D --> H[All Store Permissions - 75]
|
||||
|
||||
I[Store Member<br/>role=store_member] --> E
|
||||
I --> J[Role-Based Permissions]
|
||||
|
||||
K[Customer<br/>separate model] --> L[Shop Access]
|
||||
K[Customer<br/>separate model] --> L[Storefront Access]
|
||||
K --> M[Own Orders]
|
||||
K --> N[Own Profile]
|
||||
```
|
||||
|
||||
@@ -68,7 +68,7 @@ The `FrontendDetector` uses the following priority order:
|
||||
2. Path-based detection:
|
||||
- /admin/* or /api/v1/admin/* → ADMIN
|
||||
- /store/* or /api/v1/store/* → STORE
|
||||
- /storefront/*, /shop/*, /stores/* → STOREFRONT
|
||||
- /storefront/*, /stores/* → STOREFRONT
|
||||
- /api/v1/platform/* → PLATFORM
|
||||
3. Store subdomain (orion.omsflow.lu) → STOREFRONT
|
||||
4. Store context set by middleware → STOREFRONT
|
||||
@@ -88,8 +88,8 @@ STORE_PATH_PREFIXES = ("/store/", "/api/v1/store")
|
||||
STOREFRONT_PATH_PREFIXES = (
|
||||
"/storefront",
|
||||
"/api/v1/storefront",
|
||||
"/shop", # Legacy support
|
||||
"/api/v1/shop", # Legacy support
|
||||
"/shop", # Legacy support (redirects to /storefront)
|
||||
"/api/v1/shop", # Legacy support (redirects to /api/v1/storefront)
|
||||
"/stores/", # Path-based store access
|
||||
)
|
||||
|
||||
|
||||
@@ -186,7 +186,7 @@ function languageSelector(currentLang, enabledLanguages) {
|
||||
### Rule FE-003: Language Selector Must Be In Shared JavaScript
|
||||
|
||||
**Language selector function MUST be defined in:**
|
||||
- `static/shop/js/shop-layout.js` for storefront
|
||||
- `static/storefront/js/storefront-layout.js` for storefront
|
||||
- `static/store/js/init-alpine.js` for store dashboard
|
||||
|
||||
```javascript
|
||||
@@ -444,7 +444,7 @@ static/locales/
|
||||
"title": "Tableau de bord"
|
||||
}
|
||||
},
|
||||
"shop": {
|
||||
"storefront": {
|
||||
"cart": {
|
||||
"empty": "Votre panier est vide"
|
||||
}
|
||||
@@ -466,7 +466,7 @@ static/locales/
|
||||
|
||||
### Language Selector Implementation Checklist
|
||||
|
||||
- [ ] Function defined in appropriate JS file (`shop-layout.js` or `init-alpine.js`)
|
||||
- [ ] Function defined in appropriate JS file (`storefront-layout.js` or `init-alpine.js`)
|
||||
- [ ] Function exported to `window.languageSelector`
|
||||
- [ ] Uses `tojson|safe` filter for language array
|
||||
- [ ] Provides default values for both parameters
|
||||
@@ -492,14 +492,14 @@ static/locales/
|
||||
|
||||
| File | Type | Notes |
|
||||
|------|------|-------|
|
||||
| `static/shop/js/shop-layout.js` | JS | `languageSelector()` for storefront |
|
||||
| `static/storefront/js/storefront-layout.js` | JS | `languageSelector()` for storefront |
|
||||
| `app/modules/core/static/store/js/init-alpine.js` | JS | `languageSelector()` for store dashboard |
|
||||
| `app/modules/core/static/admin/js/init-alpine.js` | JS | `languageSelector()` for admin |
|
||||
| `app/modules/core/static/merchant/js/init-alpine.js` | JS | `languageSelector()` for merchant |
|
||||
| `app/templates/store/partials/header.html` | Template | Store dashboard language selector |
|
||||
| `app/templates/admin/partials/header.html` | Template | Admin language selector |
|
||||
| `app/templates/merchant/partials/header.html` | Template | Merchant language selector |
|
||||
| `app/templates/shop/base.html` | Template | Storefront language selector |
|
||||
| `app/templates/storefront/base.html` | Template | Storefront language selector |
|
||||
| `app/modules/core/routes/api/platform.py` | API | Language endpoints (`/api/v1/platform/language/*`) |
|
||||
| `middleware/language.py` | Middleware | Language detection per frontend type |
|
||||
| `static/locales/*.json` | JSON | Translation files |
|
||||
|
||||
@@ -8,7 +8,7 @@ The application uses a custom middleware stack that processes **every request**
|
||||
- REST API calls (`/api/*`)
|
||||
- Admin interface pages (`/admin/*`)
|
||||
- Store dashboard pages (`/store/*`)
|
||||
- Shop pages (`/shop/*` or custom domains)
|
||||
- Storefront pages (`/storefront/*` or custom domains)
|
||||
|
||||
This middleware layer is **system-wide** and enables the multi-tenant architecture to function seamlessly.
|
||||
|
||||
@@ -89,7 +89,7 @@ INFO Response: 200 for GET /admin/dashboard (0.143s)
|
||||
|
||||
### 2. Store Context Middleware
|
||||
|
||||
**Purpose**: Detect which store's shop the request is for (multi-tenant core)
|
||||
**Purpose**: Detect which store's storefront the request is for (multi-tenant core)
|
||||
|
||||
**What it does**:
|
||||
- Detects store from:
|
||||
@@ -103,7 +103,7 @@ INFO Response: 200 for GET /admin/dashboard (0.143s)
|
||||
|
||||
**Example**:
|
||||
```
|
||||
Request: https://orion.platform.com/shop/products
|
||||
Request: https://orion.platform.com/storefront/products
|
||||
↓
|
||||
Middleware detects: store_code = "orion"
|
||||
↓
|
||||
@@ -111,14 +111,14 @@ Queries database: SELECT * FROM stores WHERE code = 'orion'
|
||||
↓
|
||||
Injects: request.state.store = <Store object>
|
||||
request.state.store_id = 1
|
||||
request.state.clean_path = "/shop/products"
|
||||
request.state.clean_path = "/storefront/products"
|
||||
```
|
||||
|
||||
**Why it's critical**: Without this, the system wouldn't know which store's data to show
|
||||
|
||||
**See**: [Multi-Tenant System](multi-tenant.md) for routing modes
|
||||
|
||||
**Note on Path-Based Routing:** Previous implementations used a `PathRewriteMiddleware` to rewrite paths at runtime. This has been replaced with **double router mounting** in `main.py`, where shop routes are registered twice with different prefixes (`/shop` and `/stores/{store_code}/shop`). This approach is simpler and uses FastAPI's native routing capabilities.
|
||||
**Note on Path-Based Routing:** Previous implementations used a `PathRewriteMiddleware` to rewrite paths at runtime. This has been replaced with **double router mounting** in `main.py`, where storefront routes are registered twice with different prefixes (`/storefront` and `/stores/{store_code}/storefront`). This approach is simpler and uses FastAPI's native routing capabilities.
|
||||
|
||||
### 3. Frontend Type Detection Middleware
|
||||
|
||||
@@ -139,7 +139,7 @@ Injects: request.state.store = <Store object>
|
||||
2. Path-based detection:
|
||||
- /admin/* or /api/v1/admin/* → ADMIN
|
||||
- /store/* or /api/v1/store/* → STORE
|
||||
- /storefront/*, /shop/*, /stores/* → STOREFRONT
|
||||
- /storefront/*, /stores/* → STOREFRONT
|
||||
- /api/v1/platform/* → PLATFORM
|
||||
3. Store subdomain (orion.omsflow.lu) → STOREFRONT
|
||||
4. Store context set by middleware → STOREFRONT
|
||||
@@ -171,7 +171,7 @@ Injects: request.state.store = <Store object>
|
||||
}
|
||||
```
|
||||
|
||||
**Why it's needed**: Each store shop can have custom branding
|
||||
**Why it's needed**: Each store storefront can have custom branding
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
@@ -311,7 +311,7 @@ graph TD
|
||||
|
||||
**Breaking this order will break the application!**
|
||||
|
||||
**Note:** Path-based routing (e.g., `/stores/{code}/shop/*`) is handled by double router mounting in `main.py`, not by middleware. Platform path-based routing (e.g., `/platforms/oms/`) IS handled by PlatformContextMiddleware which rewrites the path.
|
||||
**Note:** Path-based routing (e.g., `/stores/{code}/storefront/*`) is handled by double router mounting in `main.py`, not by middleware. Platform path-based routing (e.g., `/platforms/oms/`) IS handled by PlatformContextMiddleware which rewrites the path.
|
||||
|
||||
## Request State Variables
|
||||
|
||||
@@ -332,7 +332,7 @@ Middleware components inject these variables into `request.state`:
|
||||
```python
|
||||
from fastapi import Request
|
||||
|
||||
@app.get("/shop/products")
|
||||
@app.get("/storefront/products")
|
||||
async def get_products(request: Request):
|
||||
# Access store
|
||||
store = request.state.store
|
||||
@@ -374,26 +374,26 @@ async def get_products(request: Request):
|
||||
|
||||
## Request Flow Example
|
||||
|
||||
### Example: Shop Product Page Request
|
||||
### Example: Storefront Product Page Request
|
||||
|
||||
**URL**: `https://orion.myplatform.com/shop/products`
|
||||
**URL**: `https://orion.myplatform.com/storefront/products`
|
||||
|
||||
**Middleware Processing**:
|
||||
|
||||
```
|
||||
1. LoggingMiddleware
|
||||
↓ Starts timer
|
||||
↓ Logs: "Request: GET /shop/products from 192.168.1.100"
|
||||
↓ Logs: "Request: GET /storefront/products from 192.168.1.100"
|
||||
|
||||
2. StoreContextMiddleware
|
||||
↓ Detects subdomain: "orion"
|
||||
↓ Queries DB: store = get_store_by_code("orion")
|
||||
↓ Sets: request.state.store = <Store: Orion>
|
||||
↓ Sets: request.state.store_id = 1
|
||||
↓ Sets: request.state.clean_path = "/shop/products"
|
||||
↓ Sets: request.state.clean_path = "/storefront/products"
|
||||
|
||||
3. FrontendTypeMiddleware
|
||||
↓ Uses FrontendDetector with path: "/shop/products"
|
||||
↓ Uses FrontendDetector with path: "/storefront/products"
|
||||
↓ Has store context: Yes
|
||||
↓ Detects storefront frontend
|
||||
↓ Sets: request.state.frontend_type = FrontendType.STOREFRONT
|
||||
@@ -403,7 +403,7 @@ async def get_products(request: Request):
|
||||
↓ Sets: request.state.theme = {...theme config...}
|
||||
|
||||
5. FastAPI Router
|
||||
↓ Matches route: @app.get("/shop/products")
|
||||
↓ Matches route: @app.get("/storefront/products")
|
||||
↓ Calls handler function
|
||||
|
||||
6. Route Handler
|
||||
@@ -415,7 +415,7 @@ async def get_products(request: Request):
|
||||
↓ Returns HTML with store theme
|
||||
|
||||
9. LoggingMiddleware (response phase)
|
||||
↓ Logs: "Response: 200 for GET /shop/products (0.143s)"
|
||||
↓ Logs: "Response: 200 for GET /storefront/products (0.143s)"
|
||||
↓ Adds header: X-Process-Time: 0.143
|
||||
```
|
||||
|
||||
@@ -510,9 +510,9 @@ def test_store_detection_subdomain():
|
||||
Test the full middleware stack:
|
||||
|
||||
```python
|
||||
def test_shop_request_flow(client):
|
||||
def test_storefront_request_flow(client):
|
||||
response = client.get(
|
||||
"/shop/products",
|
||||
"/storefront/products",
|
||||
headers={"Host": "orion.platform.com"}
|
||||
)
|
||||
|
||||
|
||||
@@ -4,14 +4,14 @@ Complete guide to the multi-tenant architecture supporting custom domains, subdo
|
||||
|
||||
## Overview
|
||||
|
||||
The Orion platform supports **three deployment modes** for multi-tenancy, allowing each store to have their own isolated shop while sharing the same application instance and database.
|
||||
The Orion platform supports **three deployment modes** for multi-tenancy, allowing each store to have their own isolated storefront while sharing the same application instance and database.
|
||||
|
||||
**Key Concept**: One application, multiple isolated store shops, each accessible via different URLs.
|
||||
**Key Concept**: One application, multiple isolated store storefronts, each accessible via different URLs.
|
||||
|
||||
**Important Distinction:**
|
||||
- **Store Dashboard** (all modes): `/store/{code}/*` (singular) - Management interface for stores
|
||||
- **Shop Storefront** (path-based only): `/stores/{code}/shop/*` (plural) - Customer-facing shop
|
||||
- This naming distinction helps separate administrative routes from public-facing shop routes
|
||||
- **Storefront** (path-based only): `/storefront/{code}/*` - Customer-facing storefront
|
||||
- This naming distinction helps separate administrative routes from public-facing storefront routes
|
||||
|
||||
## The Three Routing Modes
|
||||
|
||||
@@ -21,16 +21,16 @@ The Orion platform supports **three deployment modes** for multi-tenancy, allowi
|
||||
|
||||
**Example**:
|
||||
```
|
||||
customdomain1.com → Store 1 Shop
|
||||
anothershop.com → Store 2 Shop
|
||||
beststore.net → Store 3 Shop
|
||||
customdomain1.com → Store 1 Storefront
|
||||
anothershop.com → Store 2 Storefront
|
||||
beststore.net → Store 3 Storefront
|
||||
```
|
||||
|
||||
**How it works**:
|
||||
1. Store registers a custom domain
|
||||
2. Domain's DNS is configured to point to the platform
|
||||
3. Platform detects store by matching domain in database
|
||||
4. Store's shop is displayed with their theme/branding
|
||||
4. Store's storefront is displayed with their theme/branding
|
||||
|
||||
**Use Case**: Professional stores who want their own branded domain
|
||||
|
||||
@@ -50,9 +50,9 @@ store_id | domain
|
||||
|
||||
**Example**:
|
||||
```
|
||||
store1.platform.com → Store 1 Shop
|
||||
store2.platform.com → Store 2 Shop
|
||||
store3.platform.com → Store 3 Shop
|
||||
store1.platform.com → Store 1 Storefront
|
||||
store2.platform.com → Store 2 Storefront
|
||||
store3.platform.com → Store 3 Storefront
|
||||
admin.platform.com → Admin Interface
|
||||
```
|
||||
|
||||
@@ -69,9 +69,9 @@ admin.platform.com → Admin Interface
|
||||
# Stores table
|
||||
id | code | name
|
||||
---|---------|----------
|
||||
1 | store1 | Store One Shop
|
||||
2 | store2 | Store Two Shop
|
||||
3 | store3 | Store Three Shop
|
||||
1 | store1 | Store One Storefront
|
||||
2 | store2 | Store Two Storefront
|
||||
3 | store3 | Store Three Storefront
|
||||
```
|
||||
|
||||
### 3. Path-Based Mode
|
||||
@@ -80,9 +80,9 @@ id | code | name
|
||||
|
||||
**Example**:
|
||||
```
|
||||
platform.com/stores/store1/shop → Store 1 Shop
|
||||
platform.com/stores/store2/shop → Store 2 Shop
|
||||
platform.com/stores/store3/shop → Store 3 Shop
|
||||
platform.com/storefront/store1 → Store 1 Storefront
|
||||
platform.com/storefront/store2 → Store 2 Storefront
|
||||
platform.com/storefront/store3 → Store 3 Storefront
|
||||
```
|
||||
|
||||
**How it works**:
|
||||
@@ -94,7 +94,7 @@ platform.com/stores/store3/shop → Store 3 Shop
|
||||
**Use Case**: Development and testing environments only
|
||||
|
||||
**Path Patterns**:
|
||||
- `/stores/{code}/shop/*` - Storefront pages (correct pattern)
|
||||
- `/storefront/{code}/*` - Storefront pages (correct pattern)
|
||||
- `/store/{code}/*` - Store dashboard pages (different context)
|
||||
|
||||
## Routing Mode Comparison
|
||||
@@ -107,7 +107,7 @@ platform.com/stores/store3/shop → Store 3 Shop
|
||||
| **SEO Benefits** | Best (own domain) | Good | Limited |
|
||||
| **Cost** | High (domain + SSL) | Low (wildcard SSL) | Lowest |
|
||||
| **Isolation** | Best (separate domain) | Good | Good |
|
||||
| **URL Appearance** | `shop.com` | `shop.platform.com` | `platform.com/store/shop` |
|
||||
| **URL Appearance** | `shop.com` | `shop.platform.com` | `platform.com/storefront/store` |
|
||||
|
||||
## Implementation Details
|
||||
|
||||
@@ -133,7 +133,7 @@ def detect_store(request):
|
||||
|
||||
# 3. Try path-based
|
||||
path = request.url.path
|
||||
if path.startswith("/store/") or path.startswith("/stores/"):
|
||||
if path.startswith("/store/") or path.startswith("/storefront/"):
|
||||
store_code = extract_code_from_path(path)
|
||||
store = find_by_code(store_code)
|
||||
if store:
|
||||
@@ -146,11 +146,11 @@ def detect_store(request):
|
||||
|
||||
For path-based routing, clean paths are extracted:
|
||||
|
||||
**Path-Based Shop Routes** (Development):
|
||||
**Path-Based Storefront Routes** (Development):
|
||||
```
|
||||
Original: /stores/ORION/shop/products
|
||||
Original: /storefront/ORION/products
|
||||
Extracted: store_code = "ORION"
|
||||
Clean: /shop/products
|
||||
Clean: /storefront/products
|
||||
```
|
||||
|
||||
**Store Dashboard Routes** (All environments):
|
||||
@@ -160,11 +160,11 @@ Extracted: store_code = "ORION"
|
||||
Clean: /dashboard
|
||||
```
|
||||
|
||||
**Note**: The shop storefront uses `/stores/` (plural) while the store dashboard uses `/store/` (singular). This distinction helps separate customer-facing shop routes from store management routes.
|
||||
**Note**: The storefront uses `/storefront/` while the store dashboard uses `/store/` (singular). This distinction helps separate customer-facing storefront routes from store management routes.
|
||||
|
||||
**Why Clean Path?**
|
||||
- FastAPI routes don't include store prefix
|
||||
- Routes defined as: `@app.get("/shop/products")`
|
||||
- Routes defined as: `@app.get("/storefront/products")`
|
||||
- Path must be rewritten to match routes
|
||||
|
||||
## Database Schema
|
||||
@@ -197,7 +197,7 @@ CREATE TABLE store_domains (
|
||||
```sql
|
||||
-- Stores
|
||||
INSERT INTO stores (code, name) VALUES
|
||||
('orion', 'Orion Shop'),
|
||||
('orion', 'Orion Storefront'),
|
||||
('techstore', 'Tech Store'),
|
||||
('fashionhub', 'Fashion Hub');
|
||||
|
||||
@@ -220,9 +220,9 @@ INSERT INTO store_domains (store_id, domain) VALUES
|
||||
**URLs**:
|
||||
```
|
||||
myplatform.com/admin
|
||||
myplatform.com/stores/shop1/shop
|
||||
myplatform.com/stores/shop2/shop
|
||||
myplatform.com/stores/shop3/shop
|
||||
myplatform.com/storefront/shop1
|
||||
myplatform.com/storefront/shop2
|
||||
myplatform.com/storefront/shop3
|
||||
```
|
||||
|
||||
**Infrastructure**:
|
||||
@@ -272,8 +272,8 @@ shop3.myplatform.com → Store 3
|
||||
shop4.myplatform.com → Store 4
|
||||
|
||||
# Path-based (free tier)
|
||||
myplatform.com/stores/shop5/shop → Store 5
|
||||
myplatform.com/stores/shop6/shop → Store 6
|
||||
myplatform.com/storefront/shop5 → Store 5
|
||||
myplatform.com/storefront/shop6 → Store 6
|
||||
```
|
||||
|
||||
**Infrastructure**:
|
||||
@@ -391,7 +391,7 @@ static/
|
||||
|
||||
**Request**:
|
||||
```http
|
||||
GET /shop/products HTTP/1.1
|
||||
GET /storefront/products HTTP/1.1
|
||||
Host: customdomain.com
|
||||
```
|
||||
|
||||
@@ -404,8 +404,8 @@ Host: customdomain.com
|
||||
- Sets: request.state.store = <Store 1>
|
||||
|
||||
2. ContextDetectionMiddleware
|
||||
- Analyzes: path = "/shop/products"
|
||||
- Sets: context_type = SHOP
|
||||
- Analyzes: path = "/storefront/products"
|
||||
- Sets: context_type = STOREFRONT
|
||||
|
||||
3. ThemeContextMiddleware
|
||||
- Queries: store_themes WHERE store_id = 1
|
||||
@@ -420,7 +420,7 @@ Host: customdomain.com
|
||||
|
||||
**Request**:
|
||||
```http
|
||||
GET /shop/products HTTP/1.1
|
||||
GET /storefront/products HTTP/1.1
|
||||
Host: orion.myplatform.com
|
||||
```
|
||||
|
||||
@@ -439,22 +439,22 @@ Host: orion.myplatform.com
|
||||
|
||||
**Request**:
|
||||
```http
|
||||
GET /stores/ORION/shop/products HTTP/1.1
|
||||
GET /storefront/ORION/products HTTP/1.1
|
||||
Host: myplatform.com
|
||||
```
|
||||
|
||||
**Processing**:
|
||||
```
|
||||
1. StoreContextMiddleware
|
||||
- Checks: path starts with "/store/"
|
||||
- Checks: path starts with "/storefront/"
|
||||
- Extracts: code = "ORION"
|
||||
- Queries: stores WHERE code = "ORION"
|
||||
- Sets: request.state.store = <Store>
|
||||
- Sets: request.state.clean_path = "/shop/products"
|
||||
- Sets: request.state.clean_path = "/storefront/products"
|
||||
|
||||
2. FastAPI Router
|
||||
- Routes registered with prefix: /stores/{store_code}/shop
|
||||
- Matches: /stores/ORION/shop/products
|
||||
- Routes registered with prefix: /storefront/{store_code}
|
||||
- Matches: /storefront/ORION/products
|
||||
- store_code path parameter = "ORION"
|
||||
|
||||
3-4. Same as previous examples (Context, Theme middleware)
|
||||
@@ -487,17 +487,17 @@ def test_store_detection_subdomain():
|
||||
### Integration Tests
|
||||
|
||||
```python
|
||||
def test_shop_page_multi_tenant(client):
|
||||
def test_storefront_page_multi_tenant(client):
|
||||
# Test subdomain routing
|
||||
response = client.get(
|
||||
"/shop/products",
|
||||
"/storefront/products",
|
||||
headers={"Host": "orion.platform.com"}
|
||||
)
|
||||
assert "Orion" in response.text
|
||||
|
||||
# Test different store
|
||||
response = client.get(
|
||||
"/shop/products",
|
||||
"/storefront/products",
|
||||
headers={"Host": "techstore.platform.com"}
|
||||
)
|
||||
assert "Tech Store" in response.text
|
||||
|
||||
@@ -49,8 +49,8 @@ admin.platform.com → Admin Interface
|
||||
|
||||
#### Path-Based Mode (Development Only)
|
||||
```
|
||||
platform.com/stores/store1/shop → Store 1 Shop
|
||||
platform.com/stores/store2/shop → Store 2 Shop
|
||||
platform.com/storefront/store1 → Store 1 Shop
|
||||
platform.com/storefront/store2 → Store 2 Shop
|
||||
platform.com/admin → Admin Interface
|
||||
```
|
||||
|
||||
@@ -124,7 +124,7 @@ graph TB
|
||||
F -->|API /api/*| G[API Router]
|
||||
F -->|Admin /admin/*| H[Admin Page Router]
|
||||
F -->|Store /store/*| I[Store Page Router]
|
||||
F -->|Shop /shop/*| J[Shop Page Router]
|
||||
F -->|Shop /storefront/*| J[Shop Page Router]
|
||||
|
||||
G --> K[JSON Response]
|
||||
H --> L[Admin HTML]
|
||||
@@ -163,7 +163,7 @@ graph TB
|
||||
|
||||
**Access**: Store users (owners and team members)
|
||||
|
||||
### Shop Interface (`/shop/*` or custom domains)
|
||||
### Shop Interface (`/storefront/*` or custom domains)
|
||||
|
||||
**Purpose**: Customer-facing storefront
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ graph TB
|
||||
|
||||
```http
|
||||
# Shop page request (subdomain mode)
|
||||
GET https://orion.platform.com/shop/products
|
||||
GET https://orion.platform.com/storefront/products
|
||||
Host: orion.platform.com
|
||||
|
||||
# API request
|
||||
@@ -69,7 +69,7 @@ Host: platform.com
|
||||
start_time = time.time()
|
||||
|
||||
# Log entry
|
||||
logger.info(f"Request: GET /shop/products from 192.168.1.100")
|
||||
logger.info(f"Request: GET /storefront/products from 192.168.1.100")
|
||||
```
|
||||
|
||||
**Output**: Nothing added to `request.state` yet
|
||||
@@ -87,7 +87,7 @@ logger.info(f"Request: GET /shop/products from 192.168.1.100")
|
||||
```python
|
||||
# Input
|
||||
host = "orion.platform.com"
|
||||
path = "/shop/products"
|
||||
path = "/storefront/products"
|
||||
|
||||
# Detection logic
|
||||
if host != settings.platform_domain:
|
||||
@@ -102,14 +102,14 @@ if host != settings.platform_domain:
|
||||
# Set request state
|
||||
request.state.store = store
|
||||
request.state.store_id = store.id
|
||||
request.state.clean_path = "/shop/products" # Already clean
|
||||
request.state.clean_path = "/storefront/products" # Already clean
|
||||
```
|
||||
|
||||
**Request State After**:
|
||||
```python
|
||||
request.state.store = <Store: Orion>
|
||||
request.state.store_id = 1
|
||||
request.state.clean_path = "/shop/products"
|
||||
request.state.clean_path = "/storefront/products"
|
||||
```
|
||||
|
||||
### 4. Router Matching (FastAPI Native)
|
||||
@@ -117,18 +117,18 @@ request.state.clean_path = "/shop/products"
|
||||
**What happens**:
|
||||
- FastAPI matches the request path against registered routes
|
||||
- For path-based development mode, routes are registered with two prefixes:
|
||||
- `/shop/*` for subdomain/custom domain
|
||||
- `/stores/{store_code}/shop/*` for path-based development
|
||||
- `/storefront/*` for subdomain/custom domain
|
||||
- `/stores/{store_code}/storefront/*` for path-based development
|
||||
|
||||
**Example** (Path-Based Mode):
|
||||
|
||||
```python
|
||||
# In main.py - Double router mounting
|
||||
app.include_router(shop_pages.router, prefix="/shop")
|
||||
app.include_router(shop_pages.router, prefix="/stores/{store_code}/shop")
|
||||
app.include_router(storefront_pages.router, prefix="/storefront")
|
||||
app.include_router(storefront_pages.router, prefix="/stores/{store_code}/storefront")
|
||||
|
||||
# Request: /stores/ORION/shop/products
|
||||
# Matches: Second router (/stores/{store_code}/shop)
|
||||
# Request: /stores/ORION/storefront/products
|
||||
# Matches: Second router (/stores/{store_code}/storefront)
|
||||
# Route: @router.get("/products")
|
||||
# store_code available as path parameter = "ORION"
|
||||
```
|
||||
@@ -148,7 +148,7 @@ app.include_router(shop_pages.router, prefix="/stores/{store_code}/shop")
|
||||
|
||||
```python
|
||||
host = request.headers.get("host", "")
|
||||
path = request.state.clean_path # "/shop/products"
|
||||
path = request.state.clean_path # "/storefront/products"
|
||||
has_store = hasattr(request.state, 'store') and request.state.store
|
||||
|
||||
# FrontendDetector handles all detection logic centrally
|
||||
@@ -210,11 +210,11 @@ request.state.theme = {
|
||||
**Route Matching**:
|
||||
|
||||
```python
|
||||
# Request path (after rewrite): "/shop/products"
|
||||
# Request path (after rewrite): "/storefront/products"
|
||||
|
||||
# Matches this route
|
||||
@app.get("/shop/products")
|
||||
async def get_shop_products(request: Request):
|
||||
@app.get("/storefront/products")
|
||||
async def get_storefront_products(request: Request):
|
||||
# Handler code
|
||||
pass
|
||||
```
|
||||
@@ -254,7 +254,7 @@ async def shop_products_page(
|
||||
|
||||
### 9. Template Rendering (Jinja2)
|
||||
|
||||
**Template** (`templates/shop/products.html`):
|
||||
**Template** (`templates/storefront/products.html`):
|
||||
|
||||
```jinja2
|
||||
<!DOCTYPE html>
|
||||
@@ -322,7 +322,7 @@ async def shop_products_page(
|
||||
duration = time.time() - start_time # 0.143 seconds
|
||||
|
||||
logger.info(
|
||||
f"Response: 200 for GET /shop/products (0.143s)"
|
||||
f"Response: 200 for GET /storefront/products (0.143s)"
|
||||
)
|
||||
|
||||
# Add header
|
||||
@@ -397,7 +397,7 @@ sequenceDiagram
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant Client
|
||||
participant Logging
|
||||
participant Store
|
||||
participant Path
|
||||
@@ -412,7 +412,7 @@ sequenceDiagram
|
||||
Logging->>Store: Pass request
|
||||
Store->>DB: Query store by subdomain
|
||||
DB-->>Store: Store object
|
||||
Note over Store: Set store, store_id, clean_path
|
||||
Note over Store: Set store, store_id, clean_path
|
||||
Store->>Path: Pass request
|
||||
Note over Path: Path already clean
|
||||
Path->>Context: Pass request
|
||||
@@ -420,7 +420,7 @@ sequenceDiagram
|
||||
Note over Context: frontend_type = STOREFRONT
|
||||
Context->>Theme: Pass request
|
||||
Theme->>DB: Query theme
|
||||
DB-->>Theme: Theme config
|
||||
DB-->>Theme: Theme config
|
||||
Note over Theme: Set theme in request.state
|
||||
Theme->>Router: Route request
|
||||
Router->>Handler: Call handler
|
||||
@@ -431,7 +431,7 @@ sequenceDiagram
|
||||
```
|
||||
|
||||
## Request State Timeline
|
||||
|
||||
|
||||
Showing how `request.state` is built up through the middleware stack:
|
||||
|
||||
```
|
||||
@@ -445,14 +445,14 @@ After StoreContextMiddleware:
|
||||
}
|
||||
|
||||
After FrontendTypeMiddleware:
|
||||
{
|
||||
{
|
||||
store: <Store: Orion>,
|
||||
store_id: 1,
|
||||
clean_path: "/storefront/products",
|
||||
frontend_type: FrontendType.STOREFRONT
|
||||
}
|
||||
|
||||
After ThemeContextMiddleware:
|
||||
After ThemeContextMiddleware:
|
||||
{
|
||||
store: <Store: Orion>,
|
||||
store_id: 1,
|
||||
@@ -460,7 +460,7 @@ After ThemeContextMiddleware:
|
||||
frontend_type: FrontendType.STOREFRONT,
|
||||
theme: {
|
||||
primary_color: "#3B82F6",
|
||||
secondary_color: "#10B981",
|
||||
secondary_color: "#10B981",
|
||||
logo_url: "/static/stores/orion/logo.png",
|
||||
custom_css: "..."
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ CREATE TABLE store_themes (
|
||||
store_id INTEGER UNIQUE NOT NULL REFERENCES stores(id) ON DELETE CASCADE,
|
||||
theme_name VARCHAR(100) DEFAULT 'default',
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
|
||||
|
||||
-- Colors (JSON)
|
||||
colors JSONB DEFAULT '{
|
||||
"primary": "#6366f1",
|
||||
@@ -66,30 +66,30 @@ CREATE TABLE store_themes (
|
||||
"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()
|
||||
@@ -122,7 +122,7 @@ from sqlalchemy.orm import relationship
|
||||
|
||||
class Store(Base):
|
||||
# ... existing fields ...
|
||||
|
||||
|
||||
# Add theme relationship
|
||||
theme = relationship(
|
||||
"StoreTheme",
|
||||
@@ -130,7 +130,7 @@ class Store(Base):
|
||||
uselist=False, # One-to-one relationship
|
||||
cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
|
||||
@property
|
||||
def active_theme(self):
|
||||
"""Get store's active theme or return None"""
|
||||
@@ -161,7 +161,7 @@ app.middleware("http")(theme_context_middleware)
|
||||
|
||||
### Step 5: Create Shop Base Template
|
||||
|
||||
File: `app/templates/shop/base.html`
|
||||
File: `app/templates/storefront/base.html`
|
||||
|
||||
See complete template in `/home/claude/shop_base_template.html`
|
||||
|
||||
@@ -184,7 +184,7 @@ See complete template in `/home/claude/shop_base_template.html`
|
||||
|
||||
### Step 6: Create Shop Layout JavaScript
|
||||
|
||||
File: `static/shop/js/shop-layout.js`
|
||||
File: `static/storefront/js/shop-layout.js`
|
||||
|
||||
See complete code in `/home/claude/shop_layout.js`
|
||||
|
||||
@@ -208,13 +208,13 @@ from middleware.theme_context import get_current_theme
|
||||
async def shop_home(request: Request, db: Session = Depends(get_db)):
|
||||
store = request.state.store
|
||||
theme = get_current_theme(request) # or request.state.theme
|
||||
|
||||
|
||||
# Get products for store
|
||||
products = db.query(Product).filter(
|
||||
Product.store_id == store.id,
|
||||
Product.is_active == True
|
||||
).all()
|
||||
|
||||
|
||||
return templates.TemplateResponse("shop/home.html", {
|
||||
"request": request,
|
||||
"store": store,
|
||||
@@ -308,7 +308,7 @@ theme = {
|
||||
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);
|
||||
@@ -340,7 +340,7 @@ THEME_PRESETS = {
|
||||
"header": "fixed"
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
"classic": {
|
||||
"colors": {
|
||||
"primary": "#1e40af",
|
||||
@@ -356,7 +356,7 @@ THEME_PRESETS = {
|
||||
"header": "static"
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
"minimal": {
|
||||
"colors": {
|
||||
"primary": "#000000",
|
||||
@@ -372,7 +372,7 @@ THEME_PRESETS = {
|
||||
"header": "transparent"
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
"vibrant": {
|
||||
"colors": {
|
||||
"primary": "#f59e0b",
|
||||
@@ -394,16 +394,16 @@ def apply_preset(theme: StoreTheme, preset_name: str):
|
||||
"""Apply a preset to a store 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
|
||||
```
|
||||
|
||||
@@ -420,11 +420,11 @@ def get_store_theme(store_id: int, db: Session = Depends(get_db)):
|
||||
theme = db.query(StoreTheme).filter(
|
||||
StoreTheme.store_id == store_id
|
||||
).first()
|
||||
|
||||
|
||||
if not theme:
|
||||
# Return default theme
|
||||
return get_default_theme()
|
||||
|
||||
|
||||
return theme.to_dict()
|
||||
|
||||
|
||||
@@ -435,38 +435,38 @@ def update_store_theme(
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Update or create theme for store"""
|
||||
|
||||
|
||||
theme = db.query(StoreTheme).filter(
|
||||
StoreTheme.store_id == store_id
|
||||
).first()
|
||||
|
||||
|
||||
if not theme:
|
||||
theme = StoreTheme(store_id=store_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()
|
||||
|
||||
|
||||
@@ -478,22 +478,22 @@ def apply_theme_preset(
|
||||
):
|
||||
"""Apply a preset theme to store"""
|
||||
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(StoreTheme).filter(
|
||||
StoreTheme.store_id == store_id
|
||||
).first()
|
||||
|
||||
|
||||
if not theme:
|
||||
theme = StoreTheme(store_id=store_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()
|
||||
@@ -636,7 +636,7 @@ 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)
|
||||
@@ -650,7 +650,7 @@ Respect user's system preferences:
|
||||
|
||||
```javascript
|
||||
// Detect system dark mode preference
|
||||
if (window.matchMedia &&
|
||||
if (window.matchMedia &&
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
this.dark = true;
|
||||
}
|
||||
@@ -662,7 +662,7 @@ Track which themes perform best:
|
||||
```python
|
||||
class ThemeAnalytics(Base):
|
||||
__tablename__ = "theme_analytics"
|
||||
|
||||
|
||||
theme_id = Column(Integer, ForeignKey("store_themes.id"))
|
||||
conversion_rate = Column(Numeric(5, 2))
|
||||
avg_session_duration = Column(Integer)
|
||||
|
||||
@@ -42,6 +42,61 @@ http://localhost:8000/platforms/loyalty/storefront/TECHPRO/cart
|
||||
|
||||
---
|
||||
|
||||
## Development URL Quick Reference
|
||||
|
||||
All development URLs use `http://localhost:8000` as the base.
|
||||
|
||||
### Login Pages
|
||||
|
||||
| Panel | URL | Description |
|
||||
|-------|-----|-------------|
|
||||
| Admin | `/admin/login` | Platform-wide admin panel |
|
||||
| Merchant | `/merchants/login` | Merchant management panel |
|
||||
| Store Dashboard | `/platforms/{platform_code}/store/{store_code}/login` | Store staff login |
|
||||
| Storefront | `/platforms/{platform_code}/storefront/{store_code}/account/login` | Customer login |
|
||||
|
||||
### Key Entry Points
|
||||
|
||||
| Panel | URL | Description |
|
||||
|-------|-----|-------------|
|
||||
| Admin Dashboard | `/admin/` | Admin panel home |
|
||||
| Merchant Dashboard | `/merchants/dashboard` | Merchant panel home |
|
||||
| Store Dashboard | `/platforms/{platform_code}/store/{store_code}/dashboard` | Store management |
|
||||
| Storefront Homepage | `/platforms/{platform_code}/storefront/{store_code}/` | Customer-facing store |
|
||||
| Platform Homepage | `/platforms/{platform_code}/` | Platform marketing site |
|
||||
|
||||
### Full Example (OMS Platform, Store Code "ACME")
|
||||
|
||||
```
|
||||
Admin login: http://localhost:8000/admin/login
|
||||
Merchant login: http://localhost:8000/merchants/login
|
||||
Store login: http://localhost:8000/platforms/oms/store/ACME/login
|
||||
Store dashboard: http://localhost:8000/platforms/oms/store/ACME/dashboard
|
||||
Storefront login: http://localhost:8000/platforms/oms/storefront/ACME/account/login
|
||||
Storefront homepage: http://localhost:8000/platforms/oms/storefront/ACME/
|
||||
Storefront products: http://localhost:8000/platforms/oms/storefront/ACME/products
|
||||
Storefront cart: http://localhost:8000/platforms/oms/storefront/ACME/cart
|
||||
Storefront checkout: http://localhost:8000/platforms/oms/storefront/ACME/checkout
|
||||
Storefront account: http://localhost:8000/platforms/oms/storefront/ACME/account/dashboard
|
||||
```
|
||||
|
||||
### API Endpoints
|
||||
|
||||
```
|
||||
Admin API: http://localhost:8000/api/v1/admin/...
|
||||
Store API: http://localhost:8000/api/v1/store/...
|
||||
Storefront API: http://localhost:8000/api/v1/storefront/...
|
||||
```
|
||||
|
||||
### Notes
|
||||
|
||||
- **Admin and Merchant** panels are global — no platform prefix needed.
|
||||
- **Store and Storefront** panels require the `/platforms/{platform_code}/` prefix in development. This prefix is stripped by `PlatformContextMiddleware` before routing.
|
||||
- In **production**, storefronts are accessed via subdomain (`acme.omsflow.lu/`) or custom domain. The root path `/` is the storefront.
|
||||
- The storefront router is **double-mounted** at `/storefront/` and `/storefront/{store_code}/` to support both production and development modes transparently.
|
||||
|
||||
---
|
||||
|
||||
## Multi-Platform URL Routing
|
||||
|
||||
Orion supports multiple platforms (OMS, Loyalty, Site Builder), each with its own marketing site and store ecosystem.
|
||||
|
||||
Reference in New Issue
Block a user