docs: add consolidated dev URL reference and migrate /shop to /storefront
Some checks failed
CI / ruff (push) Successful in 10s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has been cancelled

- 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:
2026-02-25 13:23:44 +01:00
parent 3df75e2e78
commit d648c921b7
50 changed files with 1104 additions and 1049 deletions

View File

@@ -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)
---

View File

@@ -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

View File

@@ -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]
```

View File

@@ -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
)

View File

@@ -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 |

View File

@@ -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"}
)

View File

@@ -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

View File

@@ -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

View File

@@ -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: "..."
}

View File

@@ -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)

View File

@@ -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.