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:
@@ -7,12 +7,12 @@
|
||||
│ Browser │
|
||||
│ │
|
||||
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
|
||||
│ │ Admin Area │ │ Store Area │ │ Shop Area │ │
|
||||
│ │ /admin/* │ │ /store/* │ │ /shop/* │ │
|
||||
│ │ Admin Area │ │ Store Area │ │ Storefront Area │ │
|
||||
│ │ /admin/* │ │ /store/* │ │ /storefront/* │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ 🍪 admin_token │ │ 🍪 store_token │ │ 🍪 customer_ │ │
|
||||
│ │ Path: /admin │ │ Path: /store │ │ token │ │
|
||||
│ │ │ │ │ │ Path: /shop │ │
|
||||
│ │ │ │ │ │ Path: /storefront │ │
|
||||
│ └──────────────────┘ └──────────────────┘ └──────────────────┘ │
|
||||
│ │ │ │ │
|
||||
│ ├──────────────────────┼─────────────────────┤ │
|
||||
@@ -22,8 +22,8 @@
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
|
||||
│ Admin Backend │ │ Store Backend │ │ Shop Backend │
|
||||
│ /admin/* │ │ /store/* │ │ /shop/* │
|
||||
│ Admin Backend │ │ Store Backend │ │ Storefront Backend │
|
||||
│ /admin/* │ │ /store/* │ │ /storefront/* │
|
||||
│ │ │ │ │ │
|
||||
│ ✅ admin_token │ │ ✅ store_token │ │ ✅ customer_ │
|
||||
│ ❌ store_token │ │ ❌ admin_token │ │ token │
|
||||
@@ -128,14 +128,14 @@
|
||||
└────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Login Flow - Customer (Shop)
|
||||
## Login Flow - Customer (Storefront)
|
||||
|
||||
```
|
||||
┌──────────┐
|
||||
│ Browser │
|
||||
└──────────┘
|
||||
│
|
||||
│ POST /api/v1/shop/auth/login
|
||||
│ POST /api/v1/storefront/auth/login
|
||||
│ { email, password }
|
||||
▼
|
||||
┌─────────────────────────┐
|
||||
@@ -146,20 +146,20 @@
|
||||
│ 3. Generate JWT │
|
||||
└─────────────────────────┘
|
||||
│
|
||||
│ Set-Cookie: customer_token=<JWT>; Path=/shop; HttpOnly; SameSite=Lax
|
||||
│ Set-Cookie: customer_token=<JWT>; Path=/storefront; HttpOnly; SameSite=Lax
|
||||
│ Response: { access_token, user }
|
||||
▼
|
||||
┌──────────┐
|
||||
│ Browser │──────────────────────────────────────┐
|
||||
│ │ │
|
||||
│ 🍪 customer_token (Path=/shop) │
|
||||
│ 🍪 customer_token (Path=/storefront) │
|
||||
│ 💾 localStorage.access_token │
|
||||
└──────────┘ │
|
||||
│ │
|
||||
├── Navigate to /shop/account/dashboard ─────┤
|
||||
├── Navigate to /storefront/account/dashboard ─────┤
|
||||
│ (Cookie sent automatically) │
|
||||
│ │
|
||||
└── API call to /api/v1/shop/orders ─────────┤
|
||||
└── API call to /api/v1/storefront/orders ─────────┤
|
||||
(Authorization: Bearer <token>) │
|
||||
│
|
||||
┌────────────────────────────────────────┐
|
||||
@@ -189,7 +189,7 @@
|
||||
┌───────────────────────────┼───────────────────────────┐
|
||||
│ │ │ │ │
|
||||
Starts with Starts with Starts with Starts with Starts with
|
||||
/admin/* /store/* /shop/* /api/* (public)
|
||||
/admin/* /store/* /storefront/* /api/* (public)
|
||||
│ │ │ │ │
|
||||
▼ ▼ ▼ ▼ ▼
|
||||
┌────────────────┐┌────────────────┐┌────────────────┐┌────────────────┐┌────────────────┐
|
||||
@@ -307,17 +307,17 @@ Customer trying to access admin route:
|
||||
```
|
||||
|
||||
```
|
||||
Customer cookie sent to shop route (allowed):
|
||||
Customer cookie sent to storefront route (allowed):
|
||||
┌──────────────────────────────────────────┐
|
||||
│ Cookie: customer_token=<JWT> (Path=/shop)│
|
||||
│ Request: GET /shop/account/orders │
|
||||
│ Cookie: customer_token=<JWT> (Path=/storefront)│
|
||||
│ Request: GET /storefront/account/orders │
|
||||
└──────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
Browser checks cookie path
|
||||
│
|
||||
▼
|
||||
Path /shop matches /shop
|
||||
Path /storefront matches /storefront
|
||||
│
|
||||
▼
|
||||
✅ Cookie SENT automatically
|
||||
@@ -342,7 +342,7 @@ LOGIN
|
||||
├── Server sets cookie:
|
||||
│ • Name: admin_token, store_token, or customer_token
|
||||
│ • Value: JWT
|
||||
│ • Path: /admin, /store, or /shop (context-specific)
|
||||
│ • Path: /admin, /store, or /storefront (context-specific)
|
||||
│ • HttpOnly: true
|
||||
│ • Secure: true (production)
|
||||
│ • SameSite: Lax
|
||||
@@ -386,5 +386,5 @@ LOGOUT
|
||||
2. **Role Checking** = Strict role validation at each boundary (admin, store, customer)
|
||||
3. **Dual Auth Support** = Cookies for HTML pages, headers for API endpoints
|
||||
4. **Security First** = HttpOnly, Secure, SameSite protection on all cookies
|
||||
5. **Clear Boundaries** = Each context (admin/store/shop) is completely isolated
|
||||
5. **Clear Boundaries** = Each context (admin/store/storefront) is completely isolated
|
||||
6. **Three User Types** = Admins manage platform, stores manage stores, customers shop
|
||||
|
||||
@@ -27,7 +27,7 @@ def store_page(user: User = Depends(get_current_store_from_cookie_or_header)):
|
||||
pass
|
||||
|
||||
# Customer page - NOTE: Returns Customer, not User!
|
||||
@router.get("/shop/account/dashboard")
|
||||
@router.get("/storefront/account/dashboard")
|
||||
def customer_page(customer: Customer = Depends(get_current_customer_from_cookie_or_header)):
|
||||
pass # customer.id, customer.email, customer.store_id
|
||||
```
|
||||
@@ -53,7 +53,7 @@ def store_api(user: User = Depends(get_current_store_api)):
|
||||
pass # user.token_store_id for store context
|
||||
|
||||
# Customer API - NOTE: Returns Customer, not User!
|
||||
@router.post("/api/v1/shop/orders")
|
||||
@router.post("/api/v1/storefront/orders")
|
||||
def customer_api(request: Request, customer: Customer = Depends(get_current_customer_api)):
|
||||
pass # customer.id, request.state.store validated to match
|
||||
```
|
||||
@@ -66,7 +66,7 @@ def customer_api(request: Request, customer: Customer = Depends(get_current_cust
|
||||
|---------|--------|------|------|--------|
|
||||
| **Admin** | `admin_token` | `/admin` | `admin` | `/admin/*` |
|
||||
| **Store** | `store_token` | `/store` | `store` | `/store/*` |
|
||||
| **Customer** | `customer_token` | `/shop` | `customer` | `/shop/account/*` |
|
||||
| **Customer** | `customer_token` | `/storefront` | `customer` | `/storefront/account/*` |
|
||||
|
||||
---
|
||||
|
||||
@@ -208,8 +208,8 @@ console.log(parseJwt(localStorage.getItem('token')));
|
||||
3. ✅ **Admins** cannot access store/customer portals
|
||||
4. ✅ **Stores** cannot access admin/customer portals
|
||||
5. ✅ **Customers** cannot access admin/store portals
|
||||
6. ✅ **Public shop** (`/shop/products`) needs no auth
|
||||
7. ✅ **Customer accounts** (`/shop/account/*`) need auth
|
||||
6. ✅ **Public storefront** (`/storefront/products`) needs no auth
|
||||
7. ✅ **Customer accounts** (`/storefront/account/*`) need auth
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
The Orion platform uses a **context-based authentication system** with three isolated security domains:
|
||||
|
||||
- **Admin Portal** - Platform administration and management
|
||||
- **Store Portal** - Multi-tenant shop management
|
||||
- **Customer Shop** - Public storefront and customer accounts
|
||||
- **Store Portal** - Multi-tenant storefront management
|
||||
- **Customer Storefront** - Public storefront and customer accounts
|
||||
|
||||
Each context uses **dual authentication** supporting both cookie-based (for HTML pages) and header-based (for API calls) authentication with complete isolation between contexts.
|
||||
|
||||
@@ -92,12 +92,12 @@ Each authentication context uses a separate cookie with path restrictions:
|
||||
|----------|------------------|-------------|--------------|
|
||||
| Admin | `admin_token` | `/admin` | Admin routes only |
|
||||
| Store | `store_token` | `/store` | Store routes only |
|
||||
| Customer | `customer_token` | `/shop` | Shop routes only |
|
||||
| Customer | `customer_token` | `/storefront` | Storefront routes only |
|
||||
|
||||
**Browser Behavior:**
|
||||
- When requesting `/admin/*`, browser sends `admin_token` cookie only
|
||||
- When requesting `/store/*`, browser sends `store_token` cookie only
|
||||
- When requesting `/shop/*`, browser sends `customer_token` cookie only
|
||||
- When requesting `/storefront/*`, browser sends `customer_token` cookie only
|
||||
|
||||
This prevents cookie leakage between contexts.
|
||||
|
||||
@@ -157,7 +157,7 @@ Set-Cookie: admin_token=<JWT>; Path=/admin; HttpOnly; Secure; SameSite=Lax
|
||||
**Role:** `store`
|
||||
**Cookie:** `store_token` (path=/store)
|
||||
|
||||
**Purpose:** Store shop management, product catalog, orders, team management.
|
||||
**Purpose:** Store management, product catalog, orders, team management.
|
||||
|
||||
**Access Control:**
|
||||
- ❌ Admin users blocked (admins use admin portal for store management)
|
||||
@@ -205,24 +205,24 @@ Set-Cookie: store_token=<JWT>; Path=/store; HttpOnly; Secure; SameSite=Lax
|
||||
|
||||
### 3. Customer Context
|
||||
|
||||
**Routes:** `/shop/account/*` (authenticated), `/shop/*` (public)
|
||||
**Routes:** `/storefront/account/*` (authenticated), `/storefront/*` (public)
|
||||
**Role:** `customer`
|
||||
**Cookie:** `customer_token` (path=/shop)
|
||||
**Cookie:** `customer_token` (path=/storefront)
|
||||
|
||||
**Purpose:** Product browsing (public), customer accounts, orders, profile management.
|
||||
|
||||
**Important - URL Pattern Context:**
|
||||
The `/shop/*` routes work differently depending on deployment mode:
|
||||
- **Subdomain Mode** (Production): `https://store.platform.com/shop/products`
|
||||
- **Custom Domain** (Production): `https://customdomain.com/shop/products`
|
||||
- **Path-Based** (Development): `http://localhost:8000/stores/{store_code}/shop/products`
|
||||
The `/storefront/*` routes work differently depending on deployment mode:
|
||||
- **Subdomain Mode** (Production): `https://store.platform.com/storefront/products`
|
||||
- **Custom Domain** (Production): `https://customdomain.com/storefront/products`
|
||||
- **Path-Based** (Development): `http://localhost:8000/storefront/{store_code}/products`
|
||||
|
||||
In path-based development mode, the full URL includes the store code (e.g., `/stores/acme/shop/products`), but the routes are still defined as `/shop/*` internally. See [URL Routing Guide](../architecture/url-routing/overview.md) for details.
|
||||
In path-based development mode, the full URL includes the store code (e.g., `/storefront/acme/products`), but the routes are still defined as `/storefront/*` internally. See [URL Routing Guide](../architecture/url-routing/overview.md) for details.
|
||||
|
||||
**Access Control:**
|
||||
- **Public Routes** (`/shop/products`, `/shop/cart`, etc.):
|
||||
- **Public Routes** (`/storefront/products`, `/storefront/cart`, etc.):
|
||||
- ✅ Anyone can access (no authentication)
|
||||
- **Account Routes** (`/shop/account/*`):
|
||||
- **Account Routes** (`/storefront/account/*`):
|
||||
- ❌ Admin users blocked
|
||||
- ❌ Store users blocked
|
||||
- ✅ Customer users only
|
||||
@@ -256,7 +256,7 @@ curl -X POST http://localhost:8000/api/v1/platform/stores/1/customers/login \
|
||||
|
||||
Additionally, sets cookie:
|
||||
```
|
||||
Set-Cookie: customer_token=<JWT>; Path=/shop; HttpOnly; Secure; SameSite=Lax
|
||||
Set-Cookie: customer_token=<JWT>; Path=/storefront; HttpOnly; Secure; SameSite=Lax
|
||||
```
|
||||
|
||||
---
|
||||
@@ -323,13 +323,13 @@ async def store_dashboard(
|
||||
})
|
||||
|
||||
# Customer account page
|
||||
@router.get("/shop/account/dashboard", response_class=HTMLResponse)
|
||||
@router.get("/storefront/account/dashboard", response_class=HTMLResponse)
|
||||
async def customer_dashboard(
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_customer_from_cookie_or_header),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
return templates.TemplateResponse("shop/account/dashboard.html", {
|
||||
return templates.TemplateResponse("storefront/account/dashboard.html", {
|
||||
"request": request,
|
||||
"user": current_user
|
||||
})
|
||||
@@ -375,7 +375,7 @@ def create_product(
|
||||
return {"message": "Product created"}
|
||||
|
||||
# Customer API
|
||||
@router.post("/api/v1/shop/orders")
|
||||
@router.post("/api/v1/storefront/orders")
|
||||
def create_order(
|
||||
order_data: OrderCreate,
|
||||
current_user: User = Depends(get_current_customer_api),
|
||||
@@ -389,10 +389,10 @@ def create_order(
|
||||
Simply don't use any authentication dependency:
|
||||
|
||||
```python
|
||||
@router.get("/shop/products")
|
||||
@router.get("/storefront/products")
|
||||
async def public_products(request: Request):
|
||||
# No authentication required
|
||||
return templates.TemplateResponse("shop/products.html", {
|
||||
return templates.TemplateResponse("storefront/products.html", {
|
||||
"request": request
|
||||
})
|
||||
```
|
||||
@@ -498,7 +498,7 @@ def get_orders(
|
||||
**Security Features:**
|
||||
- **Token type validation:** Only accepts tokens with `type: "customer"` - admin and store tokens are rejected
|
||||
- **Store validation:** Validates that `token.store_id` matches `request.state.store.id` (URL-based store)
|
||||
- Prevents cross-store token reuse (customer from Store A cannot use token on Store B's shop)
|
||||
- Prevents cross-store token reuse (customer from Store A cannot use token on Store B's storefront)
|
||||
|
||||
**Usage:**
|
||||
```python
|
||||
@@ -637,23 +637,23 @@ async def store_login_page(
|
||||
```python
|
||||
from models.database.customer import Customer
|
||||
|
||||
# Shop login page redirect
|
||||
@router.get("/shop/account/login")
|
||||
async def customer_login_page(
|
||||
# Storefront login page redirect
|
||||
@router.get("/storefront/account/login")
|
||||
async def storefront_login_page(
|
||||
request: Request,
|
||||
customer: Customer | None = Depends(get_current_customer_optional)
|
||||
):
|
||||
if customer:
|
||||
# Customer already logged in, redirect to account page
|
||||
return RedirectResponse(url="/shop/account", status_code=302)
|
||||
return RedirectResponse(url="/storefront/account", status_code=302)
|
||||
|
||||
# Not logged in, show login form
|
||||
return templates.TemplateResponse("shop/login.html", {"request": request})
|
||||
return templates.TemplateResponse("storefront/login.html", {"request": request})
|
||||
```
|
||||
|
||||
**Use Cases:**
|
||||
- Login pages (redirect if already authenticated)
|
||||
- Public shop pages with conditional customer content (e.g., "My Orders" link)
|
||||
- Public storefront pages with conditional customer content (e.g., "My Orders" link)
|
||||
- Root redirects based on authentication status
|
||||
|
||||
---
|
||||
@@ -744,7 +744,7 @@ All login endpoints return:
|
||||
Additionally, the response sets an HTTP-only cookie:
|
||||
- Admin: `admin_token` (path=/admin)
|
||||
- Store: `store_token` (path=/store)
|
||||
- Customer: `customer_token` (path=/shop)
|
||||
- Customer: `customer_token` (path=/storefront)
|
||||
|
||||
---
|
||||
|
||||
@@ -752,7 +752,7 @@ Additionally, the response sets an HTTP-only cookie:
|
||||
|
||||
### Role-Based Access Control Matrix
|
||||
|
||||
| User Role | Admin Portal | Store Portal | Shop Catalog | Customer Account |
|
||||
| User Role | Admin Portal | Store Portal | Storefront Catalog | Customer Account |
|
||||
|-----------|--------------|---------------|--------------|------------------|
|
||||
| Admin | ✅ Full | ❌ Blocked | ✅ View | ❌ Blocked |
|
||||
| Store | ❌ Blocked | ✅ Full | ✅ View | ❌ Blocked |
|
||||
@@ -863,7 +863,7 @@ Custom exceptions (such as those raised for missing claims) are preserved with t
|
||||
4. **Test navigation:**
|
||||
- Navigate to `/admin/dashboard` - Should work ✅
|
||||
- Navigate to `/store/TESTSTORE/dashboard` - Should fail (cookie not sent) ❌
|
||||
- Navigate to `/shop/account/dashboard` - Should fail (cookie not sent) ❌
|
||||
- Navigate to `/storefront/account/dashboard` - Should fail (cookie not sent) ❌
|
||||
|
||||
5. **Logout:**
|
||||
```
|
||||
@@ -886,23 +886,23 @@ Custom exceptions (such as those raised for missing claims) are preserved with t
|
||||
4. **Test navigation:**
|
||||
- Navigate to `/store/{STORE_CODE}/dashboard` - Should work ✅
|
||||
- Navigate to `/admin/dashboard` - Should fail ❌
|
||||
- Navigate to `/shop/account/dashboard` - Should fail ❌
|
||||
- Navigate to `/storefront/account/dashboard` - Should fail ❌
|
||||
|
||||
#### Test Customer Authentication
|
||||
|
||||
1. **Navigate to customer login:**
|
||||
```
|
||||
http://localhost:8000/shop/account/login
|
||||
http://localhost:8000/storefront/account/login
|
||||
```
|
||||
|
||||
2. **Login with customer credentials**
|
||||
|
||||
3. **Verify cookie in DevTools:**
|
||||
- Look for `customer_token` cookie
|
||||
- Verify `Path` is `/shop`
|
||||
- Verify `Path` is `/storefront`
|
||||
|
||||
4. **Test navigation:**
|
||||
- Navigate to `/shop/account/dashboard` - Should work ✅
|
||||
- Navigate to `/storefront/account/dashboard` - Should work ✅
|
||||
- Navigate to `/admin/dashboard` - Should fail ❌
|
||||
- Navigate to `/store/{CODE}/dashboard` - Should fail ❌
|
||||
|
||||
@@ -955,7 +955,7 @@ curl -X POST http://localhost:8000/api/v1/platform/stores/1/customers/login \
|
||||
-d '{"username":"customer","password":"customer123"}'
|
||||
|
||||
# Test authenticated endpoint with token
|
||||
curl http://localhost:8000/api/v1/shop/orders \
|
||||
curl http://localhost:8000/api/v1/storefront/orders \
|
||||
-H "Authorization: Bearer <customer_access_token>"
|
||||
|
||||
# Test cross-context blocking
|
||||
@@ -1101,8 +1101,8 @@ Admins should not log into store portal as this violates security boundaries.
|
||||
**Symptom:** Customer trying to access management interfaces
|
||||
|
||||
**Explanation:** Customers only have access to:
|
||||
- Public shop routes: `/shop/products`, etc.
|
||||
- Their account: `/shop/account/*`
|
||||
- Public storefront routes: `/storefront/products`, etc.
|
||||
- Their account: `/storefront/account/*`
|
||||
|
||||
Admin and store portals are not accessible to customers.
|
||||
|
||||
@@ -1169,7 +1169,7 @@ Look for:
|
||||
2. **Don't mix authentication contexts:**
|
||||
- Admin users should use admin portal
|
||||
- Store users should use store portal
|
||||
- Customers should use shop
|
||||
- Customers should use storefront
|
||||
|
||||
3. **Always check user.is_active:**
|
||||
```python
|
||||
|
||||
@@ -167,7 +167,7 @@ Returns JSON error responses suitable for API clients.
|
||||
### Admin/Store Dashboard (`/admin/*`, `/store/*`)
|
||||
Returns JSON errors or redirects to error pages based on accept headers.
|
||||
|
||||
### Shop Requests (`/shop/*`)
|
||||
### Storefront Requests (`/storefront/*`)
|
||||
Returns themed error pages matching the store's shop design.
|
||||
|
||||
## Logging
|
||||
|
||||
@@ -335,5 +335,5 @@ Customer → Store Portal Customer → Own Account
|
||||
Cookie Isolation:
|
||||
admin_token (path=/admin) ← Only sent to /admin/*
|
||||
store_token (path=/store) ← Only sent to /store/*
|
||||
customer_token (path=/shop) ← Only sent to /shop/*
|
||||
customer_token (path=/storefront) ← Only sent to /storefront/*
|
||||
```
|
||||
|
||||
@@ -86,7 +86,7 @@ The application operates in three isolated contexts:
|
||||
|---------|--------|----------------|------------|
|
||||
| **Admin** | `/admin/*` | `admin_token` cookie | `super_admin`, `platform_admin` |
|
||||
| **Store** | `/store/*` | `store_token` cookie | `merchant_owner`, `store_member` |
|
||||
| **Shop** | `/shop/account/*` | `customer_token` cookie | Customers |
|
||||
| **Storefront** | `/storefront/account/*` | `customer_token` cookie | Customers |
|
||||
|
||||
**Important:** These contexts are security boundaries. Admin users cannot access store routes, store users cannot access admin routes, and customers are entirely separate.
|
||||
|
||||
@@ -802,7 +802,7 @@ role = Role(
|
||||
┌─────────────┐
|
||||
│ Client │
|
||||
│ │
|
||||
│ 🍪 customer_token (path=/shop) │
|
||||
│ 🍪 customer_token (path=/storefront) │
|
||||
│ 💾 localStorage.token │
|
||||
└─────────────┘
|
||||
```
|
||||
@@ -838,7 +838,7 @@ response.set_cookie(
|
||||
response.set_cookie(
|
||||
key="customer_token",
|
||||
value=jwt_token,
|
||||
path="/shop", # Only sent to /shop/* routes
|
||||
path="/storefront", # Only sent to /storefront/* routes
|
||||
httponly=True,
|
||||
secure=True,
|
||||
samesite="lax"
|
||||
|
||||
@@ -92,7 +92,7 @@ Get paginated list of products for current store.
|
||||
|
||||
```http
|
||||
GET /api/v1/storefront/products?skip=0&limit=20&is_featured=true
|
||||
Referer: http://localhost:8000/stores/orion/shop/products
|
||||
Referer: http://localhost:8000/storefront/orion/products
|
||||
```
|
||||
|
||||
**Response (200 OK):**
|
||||
@@ -142,7 +142,7 @@ Get detailed information for a specific product.
|
||||
|
||||
```http
|
||||
GET /api/v1/storefront/products/1
|
||||
Referer: http://localhost:8000/stores/orion/shop/products
|
||||
Referer: http://localhost:8000/storefront/orion/products
|
||||
```
|
||||
|
||||
**Response (200 OK):**
|
||||
@@ -200,7 +200,7 @@ Retrieve cart contents for a session.
|
||||
|
||||
```http
|
||||
GET /api/v1/storefront/cart/session-abc-123
|
||||
Referer: http://localhost:8000/stores/orion/shop/cart
|
||||
Referer: http://localhost:8000/storefront/orion/cart
|
||||
```
|
||||
|
||||
**Response (200 OK):**
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -92,7 +92,7 @@ Centralized class for detecting which frontend a request targets based on URL pa
|
||||
2. Path-based detection:
|
||||
- `/admin/*`, `/api/v1/admin/*` → ADMIN
|
||||
- `/store/*`, `/api/v1/store/*` → STORE
|
||||
- `/storefront/*`, `/shop/*`, `/stores/*` → STOREFRONT
|
||||
- `/storefront/*`, `/stores/*` → STOREFRONT
|
||||
- `/api/v1/platform/*` → PLATFORM
|
||||
3. Store subdomain → STOREFRONT
|
||||
4. Store context set → STOREFRONT
|
||||
@@ -210,17 +210,17 @@ Middleware for request/response logging and performance monitoring.
|
||||
|
||||
**Modern Approach: Double Router Mounting**
|
||||
|
||||
Instead of using middleware to rewrite paths, the application registers shop routes **twice** with different prefixes:
|
||||
Instead of using middleware to rewrite paths, the application registers storefront routes **twice** with different prefixes:
|
||||
|
||||
```python
|
||||
# In main.py
|
||||
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")
|
||||
```
|
||||
|
||||
**How It Works:**
|
||||
- **Subdomain/Custom Domain Mode**: Routes match `/shop/*` prefix
|
||||
- **Path-Based Development Mode**: Routes match `/stores/{store_code}/shop/*` prefix
|
||||
- **Subdomain/Custom Domain Mode**: Routes match `/storefront/*` prefix
|
||||
- **Path-Based Development Mode**: Routes match `/stores/{store_code}/storefront/*` prefix
|
||||
- FastAPI handles routing naturally without path manipulation
|
||||
- Store code is available as a path parameter when needed
|
||||
|
||||
|
||||
@@ -14,13 +14,13 @@ app/
|
||||
│ ├── v1/ # Version 1 API
|
||||
│ │ ├── admin/ # Admin API endpoints
|
||||
│ │ ├── store/ # Store API endpoints
|
||||
│ │ └── shop/ # Shop API endpoints
|
||||
│ │ └── storefront/ # Storefront API endpoints
|
||||
│ └── main.py # API router configuration
|
||||
│
|
||||
├── routes/ # Page routes (HTML)
|
||||
│ ├── admin_pages.py # Admin page routes
|
||||
│ ├── store_pages.py # Store page routes
|
||||
│ └── shop_pages.py # Shop page routes
|
||||
│ └── storefront_pages.py # Storefront page routes
|
||||
│
|
||||
├── services/ # Business logic layer
|
||||
│ ├── admin_service.py
|
||||
@@ -84,7 +84,7 @@ class ProductService:
|
||||
|
||||
product_service = ProductService()
|
||||
|
||||
# Step 3: API Route (app/api/v1/shop/products.py)
|
||||
# Step 3: API Route (app/api/v1/storefront/products.py)
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
from app.core.database import get_db
|
||||
|
||||
@@ -365,12 +365,12 @@ The following permission dependencies now use token-based store context:
|
||||
- `require_all_store_permissions()` - Gets store from token, sets `request.state.store`
|
||||
- `get_user_permissions` - Gets store from token, sets `request.state.store`
|
||||
|
||||
### Shop Endpoints
|
||||
Shop endpoints (public, no authentication) still use `require_store_context()`:
|
||||
- `app/api/v1/shop/products.py` - Uses URL/subdomain/domain detection
|
||||
- `app/api/v1/shop/cart.py` - Uses URL/subdomain/domain detection
|
||||
### Storefront Endpoints
|
||||
Storefront endpoints (public, no authentication) still use `require_store_context()`:
|
||||
- `app/api/v1/storefront/products.py` - Uses URL/subdomain/domain detection
|
||||
- `app/api/v1/storefront/cart.py` - Uses URL/subdomain/domain detection
|
||||
|
||||
This is correct behavior - shop endpoints need to detect store from the request URL, not from JWT token.
|
||||
This is correct behavior - storefront endpoints need to detect store from the request URL, not from JWT token.
|
||||
|
||||
## Benefits of Store-in-Token
|
||||
|
||||
|
||||
@@ -215,7 +215,7 @@ COPY . .
|
||||
# Build Tailwind CSS
|
||||
RUN tailwindcss -i ./static/admin/css/tailwind.css -o ./static/admin/css/tailwind.output.css --minify \
|
||||
&& tailwindcss -i ./static/store/css/tailwind.css -o ./static/store/css/tailwind.output.css --minify \
|
||||
&& tailwindcss -i ./static/shop/css/tailwind.css -o ./static/shop/css/tailwind.output.css --minify \
|
||||
&& tailwindcss -i ./static/storefront/css/tailwind.css -o ./static/storefront/css/tailwind.output.css --minify \
|
||||
&& tailwindcss -i ./static/public/css/tailwind.css -o ./static/public/css/tailwind.output.css --minify
|
||||
|
||||
# Create non-root user
|
||||
|
||||
@@ -27,22 +27,22 @@ class StorePaymentConfig(Base, TimestampMixin):
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
store_id = Column(Integer, ForeignKey("stores.id"), nullable=False, unique=True)
|
||||
|
||||
|
||||
# Stripe Connect configuration
|
||||
stripe_account_id = Column(String(255)) # Stripe Connect account ID
|
||||
stripe_account_status = Column(String(50)) # pending, active, restricted, inactive
|
||||
stripe_onboarding_url = Column(Text) # Onboarding link for store
|
||||
stripe_dashboard_url = Column(Text) # Store's Stripe dashboard
|
||||
|
||||
|
||||
# Payment settings
|
||||
accepts_payments = Column(Boolean, default=False)
|
||||
currency = Column(String(3), default="EUR")
|
||||
platform_fee_percentage = Column(Numeric(5, 2), default=2.5) # Platform commission
|
||||
|
||||
|
||||
# Payout settings
|
||||
payout_schedule = Column(String(20), default="weekly") # daily, weekly, monthly
|
||||
minimum_payout = Column(Numeric(10, 2), default=20.00)
|
||||
|
||||
|
||||
# Relationships
|
||||
store = relationship("Store", back_populates="payment_config")
|
||||
|
||||
@@ -58,31 +58,31 @@ class Payment(Base, TimestampMixin):
|
||||
store_id = Column(Integer, ForeignKey("stores.id"), nullable=False)
|
||||
order_id = Column(Integer, ForeignKey("orders.id"), nullable=False)
|
||||
customer_id = Column(Integer, ForeignKey("customers.id"), nullable=False)
|
||||
|
||||
|
||||
# Stripe payment details
|
||||
stripe_payment_intent_id = Column(String(255), unique=True, index=True)
|
||||
stripe_charge_id = Column(String(255), index=True)
|
||||
stripe_transfer_id = Column(String(255)) # Transfer to store account
|
||||
|
||||
|
||||
# Payment amounts (in cents to avoid floating point issues)
|
||||
amount_total = Column(Integer, nullable=False) # Total customer payment
|
||||
amount_store = Column(Integer, nullable=False) # Amount to store
|
||||
amount_platform_fee = Column(Integer, nullable=False) # Platform commission
|
||||
currency = Column(String(3), default="EUR")
|
||||
|
||||
|
||||
# Payment status
|
||||
status = Column(String(50), nullable=False) # pending, succeeded, failed, refunded
|
||||
payment_method = Column(String(50)) # card, bank_transfer, etc.
|
||||
|
||||
|
||||
# Metadata
|
||||
stripe_metadata = Column(Text) # JSON string of Stripe metadata
|
||||
failure_reason = Column(Text)
|
||||
refund_reason = Column(Text)
|
||||
|
||||
|
||||
# Timestamps
|
||||
paid_at = Column(DateTime)
|
||||
refunded_at = Column(DateTime)
|
||||
|
||||
|
||||
# Relationships
|
||||
store = relationship("Store")
|
||||
order = relationship("Order", back_populates="payment")
|
||||
@@ -109,21 +109,21 @@ class PaymentMethod(Base, TimestampMixin):
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
store_id = Column(Integer, ForeignKey("stores.id"), nullable=False)
|
||||
customer_id = Column(Integer, ForeignKey("customers.id"), nullable=False)
|
||||
|
||||
|
||||
# Stripe payment method details
|
||||
stripe_payment_method_id = Column(String(255), nullable=False, index=True)
|
||||
payment_method_type = Column(String(50), nullable=False) # card, sepa_debit, etc.
|
||||
|
||||
|
||||
# Card details (if applicable)
|
||||
card_brand = Column(String(50)) # visa, mastercard, etc.
|
||||
card_last4 = Column(String(4))
|
||||
card_exp_month = Column(Integer)
|
||||
card_exp_year = Column(Integer)
|
||||
|
||||
|
||||
# Settings
|
||||
is_default = Column(Boolean, default=False)
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
|
||||
# Relationships
|
||||
store = relationship("Store")
|
||||
customer = relationship("Customer")
|
||||
@@ -138,15 +138,15 @@ class PaymentMethod(Base, TimestampMixin):
|
||||
# Update models/database/order.py
|
||||
class Order(Base, TimestampMixin):
|
||||
# ... existing fields ...
|
||||
|
||||
|
||||
# Payment integration
|
||||
payment_status = Column(String(50), default="pending") # pending, paid, failed, refunded
|
||||
payment_intent_id = Column(String(255)) # Stripe PaymentIntent ID
|
||||
total_amount_cents = Column(Integer, nullable=False) # Amount in cents
|
||||
|
||||
|
||||
# Relationships
|
||||
payment = relationship("Payment", back_populates="order", uselist=False)
|
||||
|
||||
|
||||
@property
|
||||
def total_amount_euros(self):
|
||||
"""Convert cents to euros for display."""
|
||||
@@ -185,25 +185,25 @@ class PaymentService:
|
||||
self.db = db
|
||||
|
||||
def create_payment_intent(
|
||||
self,
|
||||
store_id: int,
|
||||
order_id: int,
|
||||
self,
|
||||
store_id: int,
|
||||
order_id: int,
|
||||
amount_euros: Decimal,
|
||||
customer_email: str,
|
||||
metadata: Optional[Dict] = None
|
||||
) -> Dict:
|
||||
"""Create Stripe PaymentIntent for store order."""
|
||||
|
||||
|
||||
# Get store payment configuration
|
||||
payment_config = self.get_store_payment_config(store_id)
|
||||
if not payment_config.accepts_payments:
|
||||
raise PaymentNotConfiguredException(f"Store {store_id} not configured for payments")
|
||||
|
||||
|
||||
# Calculate amounts
|
||||
amount_cents = int(amount_euros * 100)
|
||||
platform_fee_cents = int(amount_cents * (payment_config.platform_fee_percentage / 100))
|
||||
store_amount_cents = amount_cents - platform_fee_cents
|
||||
|
||||
|
||||
try:
|
||||
# Create PaymentIntent with Stripe Connect
|
||||
payment_intent = stripe.PaymentIntent.create(
|
||||
@@ -222,7 +222,7 @@ class PaymentService:
|
||||
receipt_email=customer_email,
|
||||
description=f"Order payment for store {store_id}"
|
||||
)
|
||||
|
||||
|
||||
# Create payment record
|
||||
payment = Payment(
|
||||
store_id=store_id,
|
||||
@@ -236,17 +236,17 @@ class PaymentService:
|
||||
status='pending',
|
||||
stripe_metadata=json.dumps(payment_intent.metadata)
|
||||
)
|
||||
|
||||
|
||||
self.db.add(payment)
|
||||
|
||||
|
||||
# Update order
|
||||
order = self.db.query(Order).filter(Order.id == order_id).first()
|
||||
if order:
|
||||
order.payment_intent_id = payment_intent.id
|
||||
order.payment_status = 'pending'
|
||||
|
||||
|
||||
self.db.commit()
|
||||
|
||||
|
||||
return {
|
||||
'payment_intent_id': payment_intent.id,
|
||||
'client_secret': payment_intent.client_secret,
|
||||
@@ -255,58 +255,58 @@ class PaymentService:
|
||||
'platform_fee': platform_fee_cents / 100,
|
||||
'currency': payment_config.currency
|
||||
}
|
||||
|
||||
|
||||
except stripe.error.StripeError as e:
|
||||
logger.error(f"Stripe error creating PaymentIntent: {e}")
|
||||
raise PaymentProcessingException(f"Payment processing failed: {str(e)}")
|
||||
|
||||
def confirm_payment(self, payment_intent_id: str) -> Payment:
|
||||
"""Confirm payment and update records."""
|
||||
|
||||
|
||||
try:
|
||||
# Retrieve PaymentIntent from Stripe
|
||||
payment_intent = stripe.PaymentIntent.retrieve(payment_intent_id)
|
||||
|
||||
|
||||
# Find payment record
|
||||
payment = self.db.query(Payment).filter(
|
||||
Payment.stripe_payment_intent_id == payment_intent_id
|
||||
).first()
|
||||
|
||||
|
||||
if not payment:
|
||||
raise PaymentNotFoundException(f"Payment not found for intent {payment_intent_id}")
|
||||
|
||||
|
||||
# Update payment status based on Stripe status
|
||||
if payment_intent.status == 'succeeded':
|
||||
payment.status = 'succeeded'
|
||||
payment.stripe_charge_id = payment_intent.charges.data[0].id if payment_intent.charges.data else None
|
||||
payment.paid_at = datetime.utcnow()
|
||||
|
||||
|
||||
# Update order status
|
||||
order = self.db.query(Order).filter(Order.id == payment.order_id).first()
|
||||
if order:
|
||||
order.payment_status = 'paid'
|
||||
order.status = 'processing' # Move order to processing
|
||||
|
||||
|
||||
elif payment_intent.status == 'payment_failed':
|
||||
payment.status = 'failed'
|
||||
payment.failure_reason = payment_intent.last_payment_error.message if payment_intent.last_payment_error else "Unknown error"
|
||||
|
||||
|
||||
# Update order status
|
||||
order = self.db.query(Order).filter(Order.id == payment.order_id).first()
|
||||
if order:
|
||||
order.payment_status = 'failed'
|
||||
|
||||
|
||||
self.db.commit()
|
||||
|
||||
|
||||
return payment
|
||||
|
||||
|
||||
except stripe.error.StripeError as e:
|
||||
logger.error(f"Stripe error confirming payment: {e}")
|
||||
raise PaymentProcessingException(f"Payment confirmation failed: {str(e)}")
|
||||
|
||||
def create_store_stripe_account(self, store_id: int, store_data: Dict) -> str:
|
||||
"""Create Stripe Connect account for store."""
|
||||
|
||||
|
||||
try:
|
||||
# Create Stripe Connect Express account
|
||||
account = stripe.Account.create(
|
||||
@@ -333,27 +333,27 @@ class PaymentService:
|
||||
'platform': 'multi_tenant_ecommerce'
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# Update or create payment configuration
|
||||
payment_config = self.get_or_create_store_payment_config(store_id)
|
||||
payment_config.stripe_account_id = account.id
|
||||
payment_config.stripe_account_status = account.charges_enabled and account.payouts_enabled and 'active' or 'pending'
|
||||
|
||||
|
||||
self.db.commit()
|
||||
|
||||
|
||||
return account.id
|
||||
|
||||
|
||||
except stripe.error.StripeError as e:
|
||||
logger.error(f"Stripe error creating account: {e}")
|
||||
raise PaymentConfigurationException(f"Failed to create payment account: {str(e)}")
|
||||
|
||||
def create_onboarding_link(self, store_id: int) -> str:
|
||||
"""Create Stripe onboarding link for store."""
|
||||
|
||||
|
||||
payment_config = self.get_store_payment_config(store_id)
|
||||
if not payment_config.stripe_account_id:
|
||||
raise PaymentNotConfiguredException("Store does not have Stripe account")
|
||||
|
||||
|
||||
try:
|
||||
account_link = stripe.AccountLink.create(
|
||||
account=payment_config.stripe_account_id,
|
||||
@@ -361,13 +361,13 @@ class PaymentService:
|
||||
return_url=f"{settings.frontend_url}/store/admin/payments/success",
|
||||
type='account_onboarding',
|
||||
)
|
||||
|
||||
|
||||
# Update onboarding URL
|
||||
payment_config.stripe_onboarding_url = account_link.url
|
||||
self.db.commit()
|
||||
|
||||
|
||||
return account_link.url
|
||||
|
||||
|
||||
except stripe.error.StripeError as e:
|
||||
logger.error(f"Stripe error creating onboarding link: {e}")
|
||||
raise PaymentConfigurationException(f"Failed to create onboarding link: {str(e)}")
|
||||
@@ -377,28 +377,28 @@ class PaymentService:
|
||||
config = self.db.query(StorePaymentConfig).filter(
|
||||
StorePaymentConfig.store_id == store_id
|
||||
).first()
|
||||
|
||||
|
||||
if not config:
|
||||
raise PaymentNotConfiguredException(f"No payment configuration for store {store_id}")
|
||||
|
||||
|
||||
return config
|
||||
|
||||
def webhook_handler(self, event_type: str, event_data: Dict) -> None:
|
||||
"""Handle Stripe webhook events."""
|
||||
|
||||
|
||||
if event_type == 'payment_intent.succeeded':
|
||||
payment_intent_id = event_data['object']['id']
|
||||
self.confirm_payment(payment_intent_id)
|
||||
|
||||
|
||||
elif event_type == 'payment_intent.payment_failed':
|
||||
payment_intent_id = event_data['object']['id']
|
||||
self.confirm_payment(payment_intent_id)
|
||||
|
||||
|
||||
elif event_type == 'account.updated':
|
||||
# Update store account status
|
||||
account_id = event_data['object']['id']
|
||||
self.update_store_account_status(account_id, event_data['object'])
|
||||
|
||||
|
||||
# Add more webhook handlers as needed
|
||||
```
|
||||
|
||||
@@ -426,7 +426,7 @@ async def get_payment_config(
|
||||
):
|
||||
"""Get store payment configuration."""
|
||||
payment_service = PaymentService(db)
|
||||
|
||||
|
||||
try:
|
||||
config = payment_service.get_store_payment_config(store.id)
|
||||
return {
|
||||
@@ -454,17 +454,17 @@ async def setup_payments(
|
||||
):
|
||||
"""Set up Stripe payments for store."""
|
||||
payment_service = PaymentService(db)
|
||||
|
||||
|
||||
store_data = {
|
||||
"business_name": store.name,
|
||||
"business_email": store.business_email,
|
||||
"business_phone": store.business_phone,
|
||||
**setup_data
|
||||
}
|
||||
|
||||
|
||||
account_id = payment_service.create_store_stripe_account(store.id, store_data)
|
||||
onboarding_url = payment_service.create_onboarding_link(store.id)
|
||||
|
||||
|
||||
return {
|
||||
"stripe_account_id": account_id,
|
||||
"onboarding_url": onboarding_url,
|
||||
@@ -481,7 +481,7 @@ async def create_payment_intent(
|
||||
):
|
||||
"""Create payment intent for customer order."""
|
||||
payment_service = PaymentService(db)
|
||||
|
||||
|
||||
payment_intent = payment_service.create_payment_intent(
|
||||
store_id=store_id,
|
||||
order_id=payment_data['order_id'],
|
||||
@@ -489,7 +489,7 @@ async def create_payment_intent(
|
||||
customer_email=payment_data['customer_email'],
|
||||
metadata=payment_data.get('metadata', {})
|
||||
)
|
||||
|
||||
|
||||
return payment_intent
|
||||
|
||||
|
||||
@@ -500,10 +500,10 @@ async def stripe_webhook(
|
||||
):
|
||||
"""Handle Stripe webhook events."""
|
||||
import stripe
|
||||
|
||||
|
||||
payload = await request.body()
|
||||
sig_header = request.headers.get('stripe-signature')
|
||||
|
||||
|
||||
try:
|
||||
event = stripe.Webhook.construct_event(
|
||||
payload, sig_header, settings.stripe_webhook_secret
|
||||
@@ -512,10 +512,10 @@ async def stripe_webhook(
|
||||
raise HTTPException(status_code=400, detail="Invalid payload")
|
||||
except stripe.error.SignatureVerificationError:
|
||||
raise HTTPException(status_code=400, detail="Invalid signature")
|
||||
|
||||
|
||||
payment_service = PaymentService(db)
|
||||
payment_service.webhook_handler(event['type'], event['data'])
|
||||
|
||||
|
||||
return {"status": "success"}
|
||||
```
|
||||
|
||||
@@ -524,7 +524,7 @@ async def stripe_webhook(
|
||||
### Checkout Process
|
||||
|
||||
```javascript
|
||||
// frontend/js/shop/checkout.js
|
||||
// frontend/js/storefront/checkout.js
|
||||
class CheckoutManager {
|
||||
constructor(storeId) {
|
||||
this.storeId = storeId;
|
||||
@@ -562,7 +562,7 @@ class CheckoutManager {
|
||||
const { error } = await this.stripe.confirmPayment({
|
||||
elements: this.elements,
|
||||
confirmParams: {
|
||||
return_url: `${window.location.origin}/shop/order-confirmation`,
|
||||
return_url: `${window.location.origin}/storefront/order-confirmation`,
|
||||
receipt_email: orderData.customerEmail
|
||||
}
|
||||
});
|
||||
@@ -614,4 +614,4 @@ Webhook updates account status to 'active'
|
||||
Store can now accept payments
|
||||
```
|
||||
|
||||
This integration provides secure, compliant payment processing while maintaining store isolation and enabling proper revenue distribution between stores and the platform.
|
||||
This integration provides secure, compliant payment processing while maintaining store isolation and enabling proper revenue distribution between stores and the platform.
|
||||
|
||||
@@ -157,7 +157,7 @@ def get_categories(...):
|
||||
|
||||
### 1.3 Shop Auth API (Password Reset)
|
||||
|
||||
**File:** `app/api/v1/shop/auth.py`
|
||||
**File:** `app/api/v1/storefront/auth.py`
|
||||
|
||||
**Problem:** Business logic in `forgot_password()` and `reset_password()` endpoints.
|
||||
|
||||
@@ -391,7 +391,7 @@ async init() {
|
||||
|
||||
### 5.2 Shop Base Template
|
||||
|
||||
**File:** `app/templates/shop/base.html`
|
||||
**File:** `app/templates/storefront/base.html`
|
||||
|
||||
**Problem:** `request.state.language` used without default (LANG-009 violation).
|
||||
|
||||
@@ -442,12 +442,12 @@ if in_language_names_block and stripped in ("}", "]"):
|
||||
| Rule ID | Description | Fixed Files |
|
||||
|---------|-------------|-------------|
|
||||
| API-001 | Endpoint must use Pydantic models | admin/email_templates.py |
|
||||
| API-003 | Endpoint must NOT contain business logic | store/settings.py, shop/auth.py |
|
||||
| API-003 | Endpoint must NOT contain business logic | store/settings.py, storefront/auth.py |
|
||||
| JS-001 | Must use apiClient for API calls | email-templates.js (admin & store) |
|
||||
| JS-002 | Must use Utils.showToast() for notifications | email-templates.js (admin & store) |
|
||||
| JS-003 | Must call parent init for Alpine components | store/email-templates.js |
|
||||
| TPL-015 | Must use correct block names | admin/email-templates.html |
|
||||
| LANG-009 | Must provide language default | shop/base.html |
|
||||
| LANG-009 | Must provide language default | storefront/base.html |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -26,8 +26,8 @@ FastAPI routes use different authentication dependencies based on whether they s
|
||||
|
||||
| Dependency | Use Case | Accepts | Description |
|
||||
|------------|----------|---------|-------------|
|
||||
| `get_current_customer_from_cookie_or_header` | HTML pages | Cookie OR Header | For shop account pages like `/shop/account/dashboard` |
|
||||
| `get_current_customer_api` | API endpoints | Header ONLY | For shop API routes like `/api/v1/shop/cart` |
|
||||
| `get_current_customer_from_cookie_or_header` | HTML pages | Cookie OR Header | For storefront account pages like `/storefront/account/dashboard` |
|
||||
| `get_current_customer_api` | API endpoints | Header ONLY | For storefront API routes like `/api/v1/storefront/cart` |
|
||||
| `get_current_customer_optional` | Login pages | Cookie OR Header | Returns `None` instead of raising exception |
|
||||
|
||||
## When to Use Which?
|
||||
|
||||
@@ -16,16 +16,16 @@
|
||||
## Key Files
|
||||
|
||||
### Created
|
||||
- `app/templates/shop/account/login.html`
|
||||
- `app/templates/shop/account/register.html`
|
||||
- `app/templates/shop/account/forgot-password.html`
|
||||
- `app/templates/shop/account/dashboard.html`
|
||||
- `app/templates/storefront/account/login.html`
|
||||
- `app/templates/storefront/account/register.html`
|
||||
- `app/templates/storefront/account/forgot-password.html`
|
||||
- `app/templates/storefront/account/dashboard.html`
|
||||
|
||||
### Modified
|
||||
- `app/api/v1/shop/auth.py` - Dynamic cookie paths
|
||||
- `app/api/v1/storefront/auth.py` - Dynamic cookie paths
|
||||
- `app/api/deps.py` - Customer authentication dependency
|
||||
- `app/services/customer_service.py` - Direct JWT token creation
|
||||
- `app/routes/shop_pages.py` - Customer type hints
|
||||
- `app/routes/storefront_pages.py` - Customer type hints
|
||||
- `middleware/store_context.py` - Harmonized detection methods
|
||||
|
||||
## Critical Architecture Decision
|
||||
@@ -41,10 +41,10 @@ JWT tokens have `type: "customer"` to distinguish them.
|
||||
|
||||
```python
|
||||
# Domain/Subdomain access
|
||||
cookie_path = "/shop"
|
||||
cookie_path = "/storefront"
|
||||
|
||||
# Path-based access (/stores/orion/shop)
|
||||
cookie_path = f"/stores/{store_code}/shop"
|
||||
# Path-based access (/storefront/orion)
|
||||
cookie_path = f"/storefront/{store_code}"
|
||||
```
|
||||
|
||||
## Authentication Flow
|
||||
@@ -58,7 +58,7 @@ cookie_path = f"/stores/{store_code}/shop"
|
||||
## Logout Flow
|
||||
|
||||
1. User clicks "Logout" button → Custom Tailwind modal appears
|
||||
2. User confirms → API call to `/api/v1/shop/auth/logout`
|
||||
2. User confirms → API call to `/api/v1/storefront/auth/logout`
|
||||
3. Cookie deleted, localStorage cleared
|
||||
4. Success toast shown, redirect to login page
|
||||
|
||||
@@ -68,9 +68,9 @@ cookie_path = f"/stores/{store_code}/shop"
|
||||
|
||||
```
|
||||
# Path-based access
|
||||
http://localhost:8000/stores/orion/shop/account/login
|
||||
http://localhost:8000/stores/orion/shop/account/register
|
||||
http://localhost:8000/stores/orion/shop/account/dashboard
|
||||
http://localhost:8000/storefront/orion/account/login
|
||||
http://localhost:8000/storefront/orion/account/register
|
||||
http://localhost:8000/storefront/orion/account/dashboard
|
||||
```
|
||||
|
||||
## Next Steps (TODO)
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the implementation of customer authentication for the shop frontend, including login, registration, and account management pages. This work creates a complete separation between customer authentication and admin/store authentication systems.
|
||||
This document describes the implementation of customer authentication for the storefront frontend, including login, registration, and account management pages. This work creates a complete separation between customer authentication and admin/store authentication systems.
|
||||
|
||||
## Problem Statement
|
||||
|
||||
The shop frontend needed proper authentication pages (login, registration, forgot password) and a working customer authentication system. The initial implementation had several issues:
|
||||
The storefront frontend needed proper authentication pages (login, registration, forgot password) and a working customer authentication system. The initial implementation had several issues:
|
||||
|
||||
1. No styled authentication pages for customers
|
||||
2. Customer authentication was incorrectly trying to use the User model (admins/stores)
|
||||
@@ -27,7 +27,7 @@ The shop frontend needed proper authentication pages (login, registration, forgo
|
||||
- Have `username` field
|
||||
- Managed via `app/services/auth_service.py`
|
||||
|
||||
- **Customers** (`models/database/customer.py`): Shop customers
|
||||
- **Customers** (`models/database/customer.py`): Storefront customers
|
||||
- Store-scoped (each store has independent customers)
|
||||
- No `role` or `username` fields
|
||||
- Have `customer_number`, `total_orders`, store relationship
|
||||
@@ -56,9 +56,9 @@ Cookies must be set with paths that match how the store is accessed:
|
||||
|
||||
| Access Method | Example URL | Cookie Path |
|
||||
|--------------|-------------|-------------|
|
||||
| Domain | `orion.lu/shop/account/login` | `/shop` |
|
||||
| Subdomain | `orion.localhost/shop/account/login` | `/shop` |
|
||||
| Path-based | `localhost/stores/orion/shop/account/login` | `/stores/orion/shop` |
|
||||
| Domain | `orion.lu/storefront/account/login` | `/storefront` |
|
||||
| Subdomain | `orion.localhost/storefront/account/login` | `/storefront` |
|
||||
| Path-based | `localhost/stores/orion/storefront/account/login` | `/storefront/orion` |
|
||||
|
||||
This ensures cookies are only sent to the correct store's routes.
|
||||
|
||||
@@ -66,32 +66,32 @@ This ensures cookies are only sent to the correct store's routes.
|
||||
|
||||
### Files Created
|
||||
|
||||
1. **`app/templates/shop/account/login.html`**
|
||||
1. **`app/templates/storefront/account/login.html`**
|
||||
- Customer login page
|
||||
- Extends `shop/base.html` (follows design pattern)
|
||||
- Extends `storefront/base.html` (follows design pattern)
|
||||
- Uses Tailwind CSS and Alpine.js
|
||||
- Theme-aware styling with CSS variables
|
||||
- Two-column layout (branding + form)
|
||||
- Form validation and error handling
|
||||
|
||||
2. **`app/templates/shop/account/register.html`**
|
||||
2. **`app/templates/storefront/account/register.html`**
|
||||
- Customer registration page
|
||||
- Fields: first_name, last_name, email, phone (optional), password
|
||||
- Client-side validation
|
||||
- Marketing consent checkbox
|
||||
- Theme integration
|
||||
|
||||
3. **`app/templates/shop/account/forgot-password.html`**
|
||||
3. **`app/templates/storefront/account/forgot-password.html`**
|
||||
- Password reset request page
|
||||
- Two-state UI (form → success)
|
||||
- Email validation
|
||||
|
||||
4. **`app/templates/shop/account/dashboard.html`**
|
||||
4. **`app/templates/storefront/account/dashboard.html`**
|
||||
- Customer account dashboard
|
||||
- Displays account summary, order statistics
|
||||
- Quick links to orders, profile, addresses
|
||||
- Logout functionality
|
||||
- Follows shop design pattern (extends base.html)
|
||||
- Follows storefront design pattern (extends base.html)
|
||||
|
||||
### Important Schema Change
|
||||
|
||||
@@ -117,7 +117,7 @@ class UserLogin(BaseModel):
|
||||
**Impact**: This change affects all login endpoints:
|
||||
- Admin login: `/api/v1/admin/auth/login`
|
||||
- Store login: `/api/v1/store/auth/login`
|
||||
- Customer login: `/api/v1/shop/auth/login`
|
||||
- Customer login: `/api/v1/storefront/auth/login`
|
||||
|
||||
**Updated Files**:
|
||||
- `app/services/auth_service.py` - Changed `user_credentials.username` to `user_credentials.email_or_username`
|
||||
@@ -127,7 +127,7 @@ class UserLogin(BaseModel):
|
||||
|
||||
### Files Modified
|
||||
|
||||
#### 1. `app/api/v1/shop/auth.py`
|
||||
#### 1. `app/api/v1/storefront/auth.py`
|
||||
|
||||
**Changes**:
|
||||
- Added `CustomerLoginResponse` model (uses `CustomerResponse` instead of `UserResponse`)
|
||||
@@ -143,11 +143,11 @@ class UserLogin(BaseModel):
|
||||
store_context = getattr(request.state, 'store_context', None)
|
||||
access_method = store_context.get('detection_method', 'unknown') if store_context else 'unknown'
|
||||
|
||||
cookie_path = "/shop" # Default for domain/subdomain access
|
||||
cookie_path = "/storefront" # Default for domain/subdomain access
|
||||
if access_method == "path":
|
||||
# For path-based access like /stores/orion/shop
|
||||
# For path-based access like /storefront/orion
|
||||
full_prefix = store_context.get('full_prefix', '/store/') if store_context else '/store/'
|
||||
cookie_path = f"{full_prefix}{store.subdomain}/shop"
|
||||
cookie_path = f"/storefront/{store.subdomain}"
|
||||
|
||||
response.set_cookie(
|
||||
key="customer_token",
|
||||
@@ -232,7 +232,7 @@ def get_current_customer_from_cookie_or_header(
|
||||
return customer # Returns Customer, not User
|
||||
```
|
||||
|
||||
#### 4. `app/routes/shop_pages.py`
|
||||
#### 4. `app/routes/storefront_pages.py`
|
||||
|
||||
**Changes**:
|
||||
- Changed import from `User` to `Customer`
|
||||
@@ -300,35 +300,35 @@ The implementation properly supports all three store access methods:
|
||||
|
||||
#### Domain-based Access
|
||||
```
|
||||
URL: https://orion.lu/shop/account/login
|
||||
Cookie Path: /shop
|
||||
Cookie Sent To: https://orion.lu/shop/*
|
||||
URL: https://orion.lu/storefront/account/login
|
||||
Cookie Path: /storefront
|
||||
Cookie Sent To: https://orion.lu/storefront/*
|
||||
```
|
||||
|
||||
#### Subdomain-based Access
|
||||
```
|
||||
URL: https://orion.myplatform.com/shop/account/login
|
||||
Cookie Path: /shop
|
||||
Cookie Sent To: https://orion.myplatform.com/shop/*
|
||||
URL: https://orion.myplatform.com/storefront/account/login
|
||||
Cookie Path: /storefront
|
||||
Cookie Sent To: https://orion.myplatform.com/storefront/*
|
||||
```
|
||||
|
||||
#### Path-based Access
|
||||
```
|
||||
URL: https://myplatform.com/stores/orion/shop/account/login
|
||||
Cookie Path: /stores/orion/shop
|
||||
Cookie Sent To: https://myplatform.com/stores/orion/shop/*
|
||||
URL: https://myplatform.com/storefront/orion/account/login
|
||||
Cookie Path: /storefront/orion
|
||||
Cookie Sent To: https://myplatform.com/storefront/orion/*
|
||||
```
|
||||
|
||||
## Authentication Flow
|
||||
|
||||
### Login Flow
|
||||
|
||||
1. **User loads login page** → `GET /stores/orion/shop/account/login`
|
||||
1. **User loads login page** → `GET /storefront/orion/account/login`
|
||||
- Middleware detects store from path
|
||||
- Sets `detection_method = "path"` in store_context
|
||||
- Renders login template
|
||||
|
||||
2. **User submits credentials** → `POST /api/v1/shop/auth/login`
|
||||
2. **User submits credentials** → `POST /api/v1/storefront/auth/login`
|
||||
- Middleware detects store from Referer header
|
||||
- Sets `detection_method = "path"` (harmonized!)
|
||||
- Validates credentials via `customer_service.login_customer()`
|
||||
@@ -337,7 +337,7 @@ Cookie Sent To: https://myplatform.com/stores/orion/shop/*
|
||||
- Sets `customer_token` cookie with correct path
|
||||
- Returns token + customer data
|
||||
|
||||
3. **Browser redirects to dashboard** → `GET /stores/orion/shop/account/dashboard`
|
||||
3. **Browser redirects to dashboard** → `GET /storefront/orion/account/dashboard`
|
||||
- Browser sends `customer_token` cookie (path matches!)
|
||||
- Dependency `get_current_customer_from_cookie_or_header` extracts token
|
||||
- Decodes JWT, validates `type == "customer"`
|
||||
@@ -352,7 +352,7 @@ Cookie Sent To: https://myplatform.com/stores/orion/shop/*
|
||||
- Smooth animations with transitions
|
||||
- Dark mode support
|
||||
|
||||
2. **User confirms logout** → `POST /api/v1/shop/auth/logout`
|
||||
2. **User confirms logout** → `POST /api/v1/storefront/auth/logout`
|
||||
- Calculates cookie path (same logic as login)
|
||||
- Deletes cookie with matching path
|
||||
- Returns success message
|
||||
@@ -374,7 +374,7 @@ response.set_cookie(
|
||||
secure=True, # HTTPS only (production/staging)
|
||||
samesite="lax", # CSRF protection
|
||||
max_age=1800, # 30 minutes (matches JWT expiry)
|
||||
path=cookie_path, # Restricted to store's shop routes
|
||||
path=cookie_path, # Restricted to store's storefront routes
|
||||
)
|
||||
```
|
||||
|
||||
@@ -395,10 +395,10 @@ response.set_cookie(
|
||||
|
||||
### Frontend Templates
|
||||
|
||||
All authentication pages follow the shop template pattern:
|
||||
All authentication pages follow the storefront template pattern:
|
||||
|
||||
```jinja2
|
||||
{% extends "shop/base.html" %}
|
||||
{% extends "storefront/base.html" %}
|
||||
|
||||
{% block title %}Page Title{% endblock %}
|
||||
|
||||
@@ -540,9 +540,9 @@ function accountDashboard() {
|
||||
3. **Session Management**: No refresh tokens, single JWT with 30-minute expiry
|
||||
|
||||
4. **Account Pages Are Placeholders**:
|
||||
- `/account/orders` - needs order history implementation
|
||||
- `/account/profile` - needs profile editing implementation
|
||||
- `/account/addresses` - needs address management implementation
|
||||
- `/storefront/account/orders` - needs order history implementation
|
||||
- `/storefront/account/profile` - needs profile editing implementation
|
||||
- `/storefront/account/addresses` - needs address management implementation
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
@@ -580,11 +580,11 @@ function accountDashboard() {
|
||||
|
||||
- **Customer Model**: `models/database/customer.py`
|
||||
- **Customer Service**: `app/services/customer_service.py`
|
||||
- **Auth Endpoints**: `app/api/v1/shop/auth.py`
|
||||
- **Auth Endpoints**: `app/api/v1/storefront/auth.py`
|
||||
- **Auth Dependencies**: `app/api/deps.py`
|
||||
- **Shop Routes**: `app/routes/shop_pages.py`
|
||||
- **Storefront Routes**: `app/routes/storefront_pages.py`
|
||||
- **Store Context**: `middleware/store_context.py`
|
||||
- **Templates**: `app/templates/shop/account/`
|
||||
- **Templates**: `app/templates/storefront/account/`
|
||||
|
||||
## Deployment Notes
|
||||
|
||||
@@ -604,10 +604,10 @@ No migrations required. Uses existing `customer` table.
|
||||
Ensure these files exist:
|
||||
- `static/shared/js/log-config.js`
|
||||
- `static/shared/js/icons.js`
|
||||
- `static/shop/js/shop-layout.js`
|
||||
- `static/storefront/js/storefront-layout.js`
|
||||
- `static/shared/js/utils.js`
|
||||
- `static/shared/js/api-client.js`
|
||||
- `static/shop/css/shop.css`
|
||||
- `static/storefront/css/storefront.css`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@@ -217,10 +217,10 @@ python scripts/seed_database.py [--reset] [--minimal]
|
||||
- Username: `admin`
|
||||
- Password: `admin123`
|
||||
|
||||
### Store Shops
|
||||
- ORION: `http://localhost:8000/shop/ORION`
|
||||
- FASHIONHUB: `http://localhost:8000/shop/FASHIONHUB`
|
||||
- BOOKSTORE: `http://localhost:8000/shop/BOOKSTORE`
|
||||
### Store Storefronts
|
||||
- ORION: `http://localhost:8000/storefront/ORION`
|
||||
- FASHIONHUB: `http://localhost:8000/storefront/FASHIONHUB`
|
||||
- BOOKSTORE: `http://localhost:8000/storefront/BOOKSTORE`
|
||||
|
||||
### Theme Editors
|
||||
- ORION Theme: `http://localhost:8000/admin/stores/ORION/theme`
|
||||
@@ -383,7 +383,7 @@ This creates 7 platform default pages that all stores inherit:
|
||||
- **Platform Defaults**: Created with `store_id=NULL`, available to all stores
|
||||
- **Store Overrides**: Stores can create custom versions with the same slug
|
||||
- **Automatic Fallback**: System checks store override first, falls back to platform default
|
||||
- **Navigation**: Pages marked with `show_in_footer` appear in shop footer automatically
|
||||
- **Navigation**: Pages marked with `show_in_footer` appear in storefront footer automatically
|
||||
- **Idempotent**: Script skips pages that already exist
|
||||
|
||||
**Access URLs:**
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
**Version:** 1.0.0
|
||||
**Last Updated:** 2025
|
||||
**Status:** Phase 1 Complete (Admin), Phase 2-3 Pending (Store, Shop)
|
||||
**Status:** Phase 1 Complete (Admin), Phase 2-3 Pending (Store, Storefront)
|
||||
|
||||
---
|
||||
|
||||
@@ -27,16 +27,16 @@
|
||||
|
||||
### Purpose
|
||||
|
||||
The error handling system provides context-aware error responses throughout the application. It automatically determines whether to return JSON (for API calls) or HTML error pages (for browser requests), and renders appropriate error pages based on the request context (Admin, Store Dashboard, or Shop).
|
||||
The error handling system provides context-aware error responses throughout the application. It automatically determines whether to return JSON (for API calls) or HTML error pages (for browser requests), and renders appropriate error pages based on the request context (Admin, Store Dashboard, or Storefront).
|
||||
|
||||
### Key Features
|
||||
|
||||
- **Context-Aware**: Different error pages for Admin, Store, and Shop areas
|
||||
- **Context-Aware**: Different error pages for Admin, Store, and Storefront areas
|
||||
- **Automatic Detection**: Distinguishes between API and HTML page requests
|
||||
- **Consistent JSON API**: API endpoints always return standardized JSON errors
|
||||
- **Fallback Mechanism**: Gracefully handles missing templates
|
||||
- **Debug Mode**: Shows technical details to admin users only
|
||||
- **Theme Integration**: Shop error pages support store theming (Phase 3)
|
||||
- **Theme Integration**: Storefront error pages support store theming (Phase 3)
|
||||
- **Security**: 401 errors automatically redirect to appropriate login pages
|
||||
|
||||
### Design Principles
|
||||
@@ -58,7 +58,7 @@ HTTP Request
|
||||
↓
|
||||
Store Context Middleware (detects store from domain/subdomain/path)
|
||||
↓
|
||||
Context Detection Middleware (detects API/Admin/Store/Shop)
|
||||
Context Detection Middleware (detects API/Admin/Store/Storefront)
|
||||
↓
|
||||
Route Handler (processes request, may throw exception)
|
||||
↓
|
||||
@@ -84,7 +84,7 @@ app/exceptions/
|
||||
app/templates/
|
||||
├── admin/errors/ # Admin error pages (Phase 1 - COMPLETE)
|
||||
├── store/errors/ # Store error pages (Phase 2 - PENDING)
|
||||
├── shop/errors/ # Shop error pages (Phase 3 - PENDING)
|
||||
├── storefront/errors/ # Storefront error pages (Phase 3 - PENDING)
|
||||
└── shared/ # Shared fallback error pages (COMPLETE)
|
||||
```
|
||||
|
||||
@@ -169,8 +169,8 @@ ErrorPageRenderer.render_error_page(
|
||||
"show_debug": bool, # Whether to show debug info
|
||||
"context_type": str, # Request context type
|
||||
"path": str, # Request path
|
||||
"store": dict, # Store info (shop context only)
|
||||
"theme": dict, # Theme data (shop context only)
|
||||
"store": dict, # Store info (storefront context only)
|
||||
"theme": dict, # Theme data (storefront context only)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -223,7 +223,7 @@ if path.startswith("/admin") or host.startswith("admin."):
|
||||
if path.startswith("/store/"):
|
||||
return RequestContext.STORE_DASHBOARD
|
||||
|
||||
# Priority 4: Shop Context
|
||||
# Priority 4: Storefront Context
|
||||
if hasattr(request.state, 'store') and request.state.store:
|
||||
return RequestContext.SHOP
|
||||
|
||||
@@ -255,7 +255,7 @@ admin.platform.com/dashboard → ADMIN context
|
||||
store1.platform.com/store/dashboard → STORE_DASHBOARD context
|
||||
```
|
||||
|
||||
**Shop Access:**
|
||||
**Storefront Access:**
|
||||
```
|
||||
store1.platform.com/ → SHOP context
|
||||
customdomain.com/ → SHOP context (if store verified)
|
||||
@@ -320,7 +320,7 @@ API endpoints MUST always return JSON, even if the client sends `Accept: text/ht
|
||||
|---------|-------------|
|
||||
| ADMIN | `/admin/login` |
|
||||
| STORE_DASHBOARD | `/store/login` |
|
||||
| SHOP | `/shop/login` |
|
||||
| SHOP | `/storefront/login` |
|
||||
| FALLBACK | `/admin/login` |
|
||||
|
||||
---
|
||||
@@ -350,7 +350,7 @@ app/templates/
|
||||
│ └── errors/
|
||||
│ └── (same structure as admin)
|
||||
│
|
||||
├── shop/
|
||||
├── storefront/
|
||||
│ └── errors/
|
||||
│ └── (same structure as admin)
|
||||
│
|
||||
@@ -446,7 +446,7 @@ Each context has its own `base.html` template:
|
||||
- Primary: "Go to Dashboard" → `/store/dashboard`
|
||||
- Secondary: "Go Back" → `javascript:history.back()`
|
||||
|
||||
#### Shop Error Pages
|
||||
#### Storefront Error Pages
|
||||
|
||||
**Purpose:** Error pages for customers on store storefronts
|
||||
|
||||
@@ -454,16 +454,16 @@ Each context has its own `base.html` template:
|
||||
- **Uses store theme** (colors, logo, fonts)
|
||||
- Customer-friendly language
|
||||
- No technical jargon
|
||||
- Links to shop homepage
|
||||
- Links to storefront homepage
|
||||
- Customer support contact
|
||||
|
||||
**Action Buttons:**
|
||||
- Primary: "Continue Shopping" → Shop homepage
|
||||
- Primary: "Continue Shopping" → Storefront homepage
|
||||
- Secondary: "Contact Support" → Store support page
|
||||
|
||||
**Theme Integration:**
|
||||
```html
|
||||
<!-- Shop error pages use store theme variables -->
|
||||
<!-- Storefront error pages use store theme variables -->
|
||||
<style>
|
||||
:root {
|
||||
--color-primary: {{ theme.colors.primary }};
|
||||
@@ -517,12 +517,12 @@ Each context has its own `base.html` template:
|
||||
|
||||
**Priority:** Medium (stores currently see fallback pages)
|
||||
|
||||
### Phase 3: Shop Error Handling ⏳ PENDING
|
||||
### Phase 3: Storefront Error Handling ⏳ PENDING
|
||||
|
||||
**Status:** Not yet implemented
|
||||
|
||||
**Required Tasks:**
|
||||
1. Create `/app/templates/shop/errors/` directory
|
||||
1. Create `/app/templates/storefront/errors/` directory
|
||||
2. Create customer-facing error templates:
|
||||
- Use customer-friendly language
|
||||
- Integrate store theme variables
|
||||
@@ -675,7 +675,7 @@ raise ValidationException(
|
||||
Determine which context needs the new error page:
|
||||
- Admin Portal → `admin/errors/`
|
||||
- Store Dashboard → `store/errors/`
|
||||
- Customer Shop → `shop/errors/`
|
||||
- Customer Storefront → `storefront/errors/`
|
||||
|
||||
### Step 2: Choose Template Type
|
||||
|
||||
@@ -734,7 +734,7 @@ Determine which context needs the new error page:
|
||||
**Option B: Standalone Template** (For custom designs)
|
||||
|
||||
```html
|
||||
<!-- app/templates/shop/errors/custom.html -->
|
||||
<!-- app/templates/storefront/errors/custom.html -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
@@ -930,16 +930,16 @@ curl -H "Accept: text/html" http://localhost:8000/store/dashboard
|
||||
# Expected: 302 redirect to /store/login
|
||||
```
|
||||
|
||||
**Shop Context (Phase 3):**
|
||||
**Storefront Context (Phase 3):**
|
||||
|
||||
```bash
|
||||
# Test 404 on store subdomain
|
||||
curl -H "Accept: text/html" http://store1.localhost:8000/nonexistent
|
||||
# Expected: Shop 404 page with store theme
|
||||
# Expected: Storefront 404 page with store theme
|
||||
|
||||
# Test 500 error
|
||||
# Trigger server error on shop
|
||||
# Expected: Shop 500 page with store branding
|
||||
# Trigger server error on storefront
|
||||
# Expected: Storefront 500 page with store branding
|
||||
```
|
||||
|
||||
### Performance Testing
|
||||
@@ -1089,7 +1089,7 @@ templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
|
||||
### Issue: Wrong Context Detected
|
||||
|
||||
**Symptoms:**
|
||||
Admin page shows shop error page, or vice versa
|
||||
Admin page shows storefront error page, or vice versa
|
||||
|
||||
**Diagnosis:**
|
||||
Add context logging:
|
||||
@@ -1154,10 +1154,10 @@ Ensure all conditions for HTML page request are met:
|
||||
- Accept header includes `text/html`
|
||||
- NOT already on login page
|
||||
|
||||
### Issue: Store Theme Not Applied to Shop Errors
|
||||
### Issue: Store Theme Not Applied to Storefront Errors
|
||||
|
||||
**Symptoms:**
|
||||
Shop error pages don't use store colors/branding (Phase 3 issue)
|
||||
Storefront error pages don't use store colors/branding (Phase 3 issue)
|
||||
|
||||
**Diagnosis:**
|
||||
1. Check if theme is in request state:
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ Admin │ │ Store │ │ Shop │
|
||||
│ Admin │ │ Store │ │ Storefront │
|
||||
│ Error │ │ Error │ │ Error │
|
||||
│ Page │ │ Page │ │ Page │
|
||||
│ │ │ │ │ (Themed) │
|
||||
@@ -159,8 +159,8 @@ For a 404 error in ADMIN context:
|
||||
For a 429 error in SHOP context (not created yet):
|
||||
|
||||
```
|
||||
1. Check: app/templates/shop/errors/429.html ✗ Missing
|
||||
2. Check: app/templates/shop/errors/generic.html ✗ Missing
|
||||
1. Check: app/templates/storefront/errors/429.html ✗ Missing
|
||||
2. Check: app/templates/storefront/errors/generic.html ✗ Missing
|
||||
3. Check: app/templates/shared/429-fallback.html ✗ Missing
|
||||
4. Check: app/templates/shared/generic-fallback.html ✓ EXISTS → USE THIS
|
||||
```
|
||||
@@ -189,7 +189,7 @@ For a 429 error in SHOP context (not created yet):
|
||||
│ error_code│ │ │ │ Location: │
|
||||
│ message │ │ Context- │ │ /admin/login│
|
||||
│ status │ │ aware │ │ /store/login│
|
||||
│ details │ │ template │ │ /shop/login │
|
||||
│ details │ │ template │ │ /storefront/login │
|
||||
│ } │ │ │ │ │
|
||||
└────────────┘ └────────────┘ └──────────────┘
|
||||
```
|
||||
@@ -213,12 +213,12 @@ Context: ADMIN
|
||||
Result: HTML admin/errors/404.html
|
||||
```
|
||||
|
||||
### Scenario 3: Shop Page 500 Error
|
||||
### Scenario 3: Storefront Page 500 Error
|
||||
```
|
||||
Request: GET /products/123 (on store1.platform.com)
|
||||
Accept: text/html
|
||||
Context: SHOP (store detected)
|
||||
Result: HTML shop/errors/500.html (with store theme)
|
||||
Result: HTML storefront/errors/500.html (with store theme)
|
||||
```
|
||||
|
||||
### Scenario 4: Unauthorized Access to Admin
|
||||
@@ -245,7 +245,7 @@ Result: 302 Redirect to /admin/login
|
||||
┌──────────────┐ ┌──────────────┐
|
||||
│ Admin User │ │ Other Users │
|
||||
│ (Context: │ │ (Store, │
|
||||
│ ADMIN) │ │ Shop) │
|
||||
│ ADMIN) │ │ Storefront)│
|
||||
└──────┬───────┘ └──────┬───────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
@@ -280,8 +280,8 @@ app/
|
||||
│ ├── store/
|
||||
│ │ └── errors/ # TODO: Store error pages
|
||||
│ │
|
||||
│ ├── shop/
|
||||
│ │ └── errors/ # TODO: Shop error pages (themed)
|
||||
│ ├── storefront/
|
||||
│ │ └── errors/ # TODO: Storefront error pages (themed)
|
||||
│ │
|
||||
│ └── shared/
|
||||
│ └── *-fallback.html # Shared fallback error pages
|
||||
@@ -303,13 +303,13 @@ middleware/
|
||||
✅ **Professional**: Polished error pages matching area design
|
||||
✅ **Flexible**: Fallback mechanism ensures errors always render
|
||||
✅ **Secure**: Debug info only shown to admins
|
||||
✅ **Themed**: Shop errors can use store branding (Phase 3)
|
||||
✅ **Themed**: Storefront errors can use store branding (Phase 3)
|
||||
|
||||
---
|
||||
|
||||
This flow ensures that:
|
||||
1. API calls ALWAYS get JSON responses
|
||||
2. HTML page requests get appropriate error pages
|
||||
3. Each context (admin/store/shop) has its own error design
|
||||
3. Each context (admin/store/storefront) has its own error design
|
||||
4. Fallback mechanism prevents broken error pages
|
||||
5. 401 errors redirect to appropriate login pages
|
||||
|
||||
@@ -10,7 +10,7 @@ This guide shows you how to implement the Content Management System for static p
|
||||
✅ **Service Layer**: `app/services/content_page_service.py`
|
||||
✅ **Admin API**: `app/api/v1/admin/content_pages.py`
|
||||
✅ **Store API**: `app/api/v1/store/content_pages.py`
|
||||
✅ **Shop API**: `app/api/v1/shop/content_pages.py`
|
||||
✅ **Storefront API**: `app/api/v1/storefront/content_pages.py`
|
||||
✅ **Documentation**: Full CMS documentation in `docs/features/content-management-system.md`
|
||||
|
||||
## Next Steps to Activate
|
||||
@@ -65,20 +65,20 @@ api_router.include_router(
|
||||
)
|
||||
```
|
||||
|
||||
**Shop Router** (`app/api/v1/shop/__init__.py` or create if doesn't exist):
|
||||
**Storefront Router** (`app/api/v1/storefront/__init__.py` or create if doesn't exist):
|
||||
```python
|
||||
from app.api.v1.shop import content_pages
|
||||
from app.api.v1.storefront import content_pages
|
||||
|
||||
api_router.include_router(
|
||||
content_pages.router,
|
||||
prefix="/content-pages",
|
||||
tags=["shop-content-pages"]
|
||||
tags=["storefront-content-pages"]
|
||||
)
|
||||
```
|
||||
|
||||
### 4. Update Shop Routes to Use CMS
|
||||
### 4. Update Storefront Routes to Use CMS
|
||||
|
||||
Edit `app/routes/shop_pages.py` to add a generic content page handler:
|
||||
Edit `app/routes/storefront_pages.py` to add a generic content page handler:
|
||||
|
||||
```python
|
||||
from app.services.content_page_service import content_page_service
|
||||
@@ -107,18 +107,18 @@ async def generic_content_page(
|
||||
raise HTTPException(status_code=404, detail=f"Page not found: {slug}")
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"shop/content-page.html",
|
||||
get_shop_context(request, page=page)
|
||||
"storefront/content-page.html",
|
||||
get_storefront_context(request, page=page)
|
||||
)
|
||||
```
|
||||
|
||||
### 5. Create Generic Content Page Template
|
||||
|
||||
Create `app/templates/shop/content-page.html`:
|
||||
Create `app/templates/storefront/content-page.html`:
|
||||
|
||||
```jinja2
|
||||
{# app/templates/shop/content-page.html #}
|
||||
{% extends "shop/base.html" %}
|
||||
{# app/templates/storefront/content-page.html #}
|
||||
{% extends "storefront/base.html" %}
|
||||
|
||||
{% block title %}{{ page.title }}{% endblock %}
|
||||
|
||||
@@ -159,14 +159,14 @@ Create `app/templates/shop/content-page.html`:
|
||||
|
||||
### 6. Update Footer to Load Navigation Dynamically
|
||||
|
||||
Edit `app/templates/shop/base.html` to load navigation from database.
|
||||
Edit `app/templates/storefront/base.html` to load navigation from database.
|
||||
|
||||
First, update the context helper to include footer pages:
|
||||
|
||||
```python
|
||||
# app/routes/shop_pages.py
|
||||
# app/routes/storefront_pages.py
|
||||
|
||||
def get_shop_context(request: Request, **extra_context) -> dict:
|
||||
def get_storefront_context(request: Request, **extra_context) -> dict:
|
||||
# ... existing code ...
|
||||
|
||||
# Load footer navigation pages
|
||||
@@ -198,7 +198,7 @@ def get_shop_context(request: Request, **extra_context) -> dict:
|
||||
Then update the footer template:
|
||||
|
||||
```jinja2
|
||||
{# app/templates/shop/base.html - Footer section #}
|
||||
{# app/templates/storefront/base.html - Footer section #}
|
||||
|
||||
<div>
|
||||
<h4 class="font-semibold mb-4">Quick Links</h4>
|
||||
@@ -367,7 +367,7 @@ curl -X POST http://localhost:8000/api/v1/admin/content-pages/platform \
|
||||
"show_in_footer": true
|
||||
}'
|
||||
|
||||
# View in shop
|
||||
# View in storefront
|
||||
curl http://localhost:8000/store/orion/about
|
||||
```
|
||||
|
||||
@@ -385,7 +385,7 @@ curl -X POST http://localhost:8000/api/v1/store/orion/content-pages/ \
|
||||
"is_published": true
|
||||
}'
|
||||
|
||||
# View in shop (should show store content)
|
||||
# View in storefront (should show store content)
|
||||
curl http://localhost:8000/store/orion/about
|
||||
```
|
||||
|
||||
@@ -396,7 +396,7 @@ curl http://localhost:8000/store/orion/about
|
||||
curl -X DELETE http://localhost:8000/api/v1/store/orion/content-pages/{id} \
|
||||
-H "Authorization: Bearer <store_token>"
|
||||
|
||||
# View in shop (should fall back to platform default)
|
||||
# View in storefront (should fall back to platform default)
|
||||
curl http://localhost:8000/store/orion/about
|
||||
```
|
||||
|
||||
|
||||
@@ -204,12 +204,12 @@ DELETE /api/v1/store/{code}/content-pages/15
|
||||
|
||||
After deletion, platform default will be shown again.
|
||||
|
||||
### Shop Frontend (Public)
|
||||
### Storefront (Public)
|
||||
|
||||
**1. Get Page Content**
|
||||
|
||||
```bash
|
||||
GET /api/v1/shop/content-pages/about
|
||||
GET /api/v1/storefront/content-pages/about
|
||||
```
|
||||
|
||||
Automatically uses store context from middleware:
|
||||
@@ -221,12 +221,12 @@ Automatically uses store context from middleware:
|
||||
|
||||
```bash
|
||||
# Get all navigation pages
|
||||
GET /api/v1/shop/content-pages/navigation
|
||||
GET /api/v1/storefront/content-pages/navigation
|
||||
|
||||
# Filter by placement
|
||||
GET /api/v1/shop/content-pages/navigation?header_only=true
|
||||
GET /api/v1/shop/content-pages/navigation?footer_only=true
|
||||
GET /api/v1/shop/content-pages/navigation?legal_only=true
|
||||
GET /api/v1/storefront/content-pages/navigation?header_only=true
|
||||
GET /api/v1/storefront/content-pages/navigation?footer_only=true
|
||||
GET /api/v1/storefront/content-pages/navigation?legal_only=true
|
||||
```
|
||||
|
||||
Returns published pages filtered by navigation placement.
|
||||
@@ -246,10 +246,10 @@ app/
|
||||
│ │ └── content_pages.py ← Admin API endpoints
|
||||
│ ├── store/
|
||||
│ │ └── content_pages.py ← Store API endpoints
|
||||
│ └── shop/
|
||||
│ └── storefront/
|
||||
│ └── content_pages.py ← Public API endpoints
|
||||
│
|
||||
└── templates/shop/
|
||||
└── templates/storefront/
|
||||
├── about.html ← Content page template
|
||||
├── faq.html
|
||||
├── contact.html
|
||||
@@ -263,8 +263,8 @@ app/
|
||||
Create a reusable template for all content pages:
|
||||
|
||||
```jinja2
|
||||
{# app/templates/shop/content-page.html #}
|
||||
{% extends "shop/base.html" %}
|
||||
{# app/templates/storefront/content-page.html #}
|
||||
{% extends "storefront/base.html" %}
|
||||
|
||||
{% block title %}{{ page.title }}{% endblock %}
|
||||
|
||||
@@ -310,7 +310,7 @@ Create a reusable template for all content pages:
|
||||
### Route Handler
|
||||
|
||||
```python
|
||||
# app/routes/shop_pages.py
|
||||
# app/routes/storefront_pages.py
|
||||
|
||||
from app.services.content_page_service import content_page_service
|
||||
|
||||
@@ -339,8 +339,8 @@ async def content_page(
|
||||
raise HTTPException(status_code=404, detail=f"Page not found: {slug}")
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"shop/content-page.html",
|
||||
get_shop_context(request, page=page)
|
||||
"storefront/content-page.html",
|
||||
get_storefront_context(request, page=page)
|
||||
)
|
||||
```
|
||||
|
||||
@@ -349,7 +349,7 @@ async def content_page(
|
||||
Update footer to load links from database:
|
||||
|
||||
```jinja2
|
||||
{# app/templates/shop/base.html #}
|
||||
{# app/templates/storefront/base.html #}
|
||||
|
||||
<footer>
|
||||
<div class="grid grid-cols-3">
|
||||
@@ -552,11 +552,11 @@ PUT /api/v1/store/{code}/content-pages/{id} # Update store page
|
||||
DELETE /api/v1/store/{code}/content-pages/{id} # Delete store page
|
||||
```
|
||||
|
||||
### Shop (Public) Endpoints
|
||||
### Storefront (Public) Endpoints
|
||||
|
||||
```
|
||||
GET /api/v1/shop/content-pages/navigation # Get navigation links
|
||||
GET /api/v1/shop/content-pages/{slug} # Get page content
|
||||
GET /api/v1/storefront/content-pages/navigation # Get navigation links
|
||||
GET /api/v1/storefront/content-pages/{slug} # Get page content
|
||||
```
|
||||
|
||||
## Example: Complete Workflow
|
||||
|
||||
@@ -497,12 +497,12 @@ content_page_service.create_page(
|
||||
|
||||
**Get page content:**
|
||||
```bash
|
||||
GET /api/v1/shop/content-pages/about
|
||||
GET /api/v1/storefront/content-pages/about
|
||||
```
|
||||
|
||||
**Get navigation:**
|
||||
```bash
|
||||
GET /api/v1/shop/content-pages/navigation
|
||||
GET /api/v1/storefront/content-pages/navigation
|
||||
```
|
||||
|
||||
---
|
||||
@@ -681,11 +681,11 @@ PUT /api/v1/admin/content-pages/{id} # Update page
|
||||
DELETE /api/v1/admin/content-pages/{id} # Delete page
|
||||
```
|
||||
|
||||
### Shop (Public) Endpoints
|
||||
### Storefront (Public) Endpoints
|
||||
|
||||
```
|
||||
GET /api/v1/shop/content-pages/navigation # Get nav links
|
||||
GET /api/v1/shop/content-pages/{slug} # Get page content
|
||||
GET /api/v1/storefront/content-pages/navigation # Get nav links
|
||||
GET /api/v1/storefront/content-pages/{slug} # Get page content
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -4,7 +4,7 @@ Complete guide to creating custom landing pages for store storefronts.
|
||||
|
||||
## Overview
|
||||
|
||||
Each store can have a custom landing page at their root URL with different design templates. This landing page serves as the store's homepage, separate from the e-commerce shop section.
|
||||
Each store can have a custom landing page at their root URL with different design templates. This landing page serves as the store's homepage, separate from the e-commerce storefront section.
|
||||
|
||||
### URL Structure
|
||||
|
||||
@@ -14,10 +14,10 @@ Root Landing Page:
|
||||
- Subdomain: https://orion.platform.com/ → Landing Page
|
||||
- Path-based: http://localhost:8000/stores/orion/ → Landing Page
|
||||
|
||||
E-commerce Shop:
|
||||
- Custom Domain: https://customdomain.com/shop/ → Shop Homepage
|
||||
- Subdomain: https://orion.platform.com/shop/ → Shop Homepage
|
||||
- Path-based: http://localhost:8000/stores/orion/shop/ → Shop Homepage
|
||||
E-commerce Storefront:
|
||||
- Custom Domain: https://customdomain.com/storefront/ → Storefront Homepage
|
||||
- Subdomain: https://orion.platform.com/storefront/ → Storefront Homepage
|
||||
- Path-based: http://localhost:8000/storefront/orion/ → Storefront Homepage
|
||||
```
|
||||
|
||||
## Features
|
||||
@@ -25,7 +25,7 @@ E-commerce Shop:
|
||||
✅ **Multiple Templates**: Choose from 4 different landing page designs
|
||||
✅ **CMS-Powered**: Content managed through ContentPage model
|
||||
✅ **Per-Store Customization**: Each store can have unique design
|
||||
✅ **Auto-Fallback**: Redirects to shop if no landing page exists
|
||||
✅ **Auto-Fallback**: Redirects to storefront if no landing page exists
|
||||
✅ **Theme-Aware**: Uses store's theme colors and branding
|
||||
|
||||
## Available Templates
|
||||
@@ -112,7 +112,7 @@ Set `is_published=True` to make the landing page live.
|
||||
If no landing page exists:
|
||||
1. System checks for `slug="landing"`
|
||||
2. If not found, checks for `slug="home"`
|
||||
3. If neither exists, **redirects to `/shop/`**
|
||||
3. If neither exists, **redirects to `/storefront/`**
|
||||
|
||||
This ensures stores always have a working homepage even without a landing page.
|
||||
|
||||
@@ -169,7 +169,7 @@ ADD COLUMN template VARCHAR(50) NOT NULL DEFAULT 'default';
|
||||
|
||||
### Get Landing Page
|
||||
```http
|
||||
GET /api/v1/shop/content-pages/landing
|
||||
GET /api/v1/storefront/content-pages/landing
|
||||
Referer: https://orion.platform.com/
|
||||
|
||||
Response:
|
||||
@@ -195,7 +195,7 @@ Response:
|
||||
### Test Fallback
|
||||
1. Delete or unpublish landing page
|
||||
2. Access store root URL
|
||||
3. Should redirect to `/shop/`
|
||||
3. Should redirect to `/storefront/`
|
||||
|
||||
## Customization Guide
|
||||
|
||||
@@ -206,9 +206,9 @@ Response:
|
||||
app/templates/store/landing-{name}.html
|
||||
```
|
||||
|
||||
2. Extend shop base:
|
||||
2. Extend storefront base:
|
||||
```jinja2
|
||||
{% extends "shop/base.html" %}
|
||||
{% extends "storefront/base.html" %}
|
||||
```
|
||||
|
||||
3. Use template variables as needed
|
||||
@@ -238,7 +238,7 @@ Edit directly and changes apply immediately (no rebuild needed).
|
||||
|
||||
2. **Keep Content Concise**: Landing pages should be scannable
|
||||
|
||||
3. **Strong CTAs**: Always link to `/shop/` for e-commerce
|
||||
3. **Strong CTAs**: Always link to `/storefront/` for e-commerce
|
||||
|
||||
4. **Use Theme Colors**: Templates automatically use store theme
|
||||
|
||||
|
||||
@@ -189,7 +189,7 @@ app/templates/shared/
|
||||
|
||||
### Shop Frontend
|
||||
|
||||
**Template:** `app/templates/shop/base.html`
|
||||
**Template:** `app/templates/storefront/base.html`
|
||||
|
||||
```html
|
||||
{# Lines 41-42: Tailwind CSS fallback #}
|
||||
|
||||
@@ -148,39 +148,39 @@ function myPageComponent() {
|
||||
return {
|
||||
...data(),
|
||||
currentPage: 'my-page',
|
||||
|
||||
|
||||
items: [],
|
||||
loading: false,
|
||||
|
||||
|
||||
async init() {
|
||||
pageLog.info('Initializing page');
|
||||
|
||||
|
||||
// Prevent double init
|
||||
if (window._myPageInitialized) return;
|
||||
window._myPageInitialized = true;
|
||||
|
||||
|
||||
const start = performance.now();
|
||||
|
||||
|
||||
try {
|
||||
pageLog.group('Loading Data');
|
||||
await this.loadData();
|
||||
pageLog.groupEnd();
|
||||
|
||||
|
||||
const duration = performance.now() - start;
|
||||
window.LogConfig.logPerformance('Page Init', duration);
|
||||
|
||||
|
||||
pageLog.info('Page initialized successfully');
|
||||
} catch (error) {
|
||||
window.LogConfig.logError(error, 'Page Init');
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
async loadData() {
|
||||
const url = '/api/my-data';
|
||||
window.LogConfig.logApiCall('GET', url, null, 'request');
|
||||
|
||||
|
||||
const data = await apiClient.get(url);
|
||||
|
||||
|
||||
window.LogConfig.logApiCall('GET', url, data, 'response');
|
||||
this.items = data;
|
||||
}
|
||||
@@ -300,7 +300,7 @@ The system automatically detects which frontend based on URL:
|
||||
|----------|----------|--------|
|
||||
| `/admin/*` | admin | `ADMIN:` |
|
||||
| `/store/*` | store | `STORE:` |
|
||||
| `/shop/*` | shop | `SHOP:` |
|
||||
| `/storefront/*` | storefront | `SHOP:` |
|
||||
|
||||
---
|
||||
|
||||
@@ -338,21 +338,21 @@ const DEFAULT_LOG_LEVELS = {
|
||||
window.LogConfig = {
|
||||
// Log levels
|
||||
LOG_LEVELS: { ERROR: 1, WARN: 2, INFO: 3, DEBUG: 4 },
|
||||
|
||||
|
||||
// Current config
|
||||
frontend: 'admin' | 'store' | 'shop',
|
||||
environment: 'development' | 'production',
|
||||
logLevel: 1 | 2 | 3 | 4,
|
||||
|
||||
|
||||
// Default logger
|
||||
log: { error(), warn(), info(), debug(), group(), groupEnd(), table(), time(), timeEnd() },
|
||||
|
||||
|
||||
// Pre-configured loggers
|
||||
loggers: { stores, products, orders, ... },
|
||||
|
||||
|
||||
// Create custom logger
|
||||
createLogger(prefix, level?),
|
||||
|
||||
|
||||
// Utility functions
|
||||
logApiCall(method, url, data?, status),
|
||||
logError(error, context),
|
||||
|
||||
@@ -327,13 +327,13 @@ Tab navigation components for switching between content sections.
|
||||
|
||||
---
|
||||
|
||||
## E-commerce Components (Shop Frontend)
|
||||
## E-commerce Components (Storefront Frontend)
|
||||
|
||||
Reusable macros for shop/storefront functionality. Located in `app/templates/shared/macros/shop/`.
|
||||
Reusable macros for storefront functionality. Located in `app/templates/shared/macros/storefront/`.
|
||||
|
||||
### 🛍️ Product Card
|
||||
```html
|
||||
{% from 'shared/macros/shop/product-card.html' import product_card %}
|
||||
{% from 'shared/macros/storefront/product-card.html' import product_card %}
|
||||
|
||||
{# Basic product card #}
|
||||
{{ product_card(product_var='product') }}
|
||||
@@ -360,7 +360,7 @@ Reusable macros for shop/storefront functionality. Located in `app/templates/sha
|
||||
|
||||
### 🛍️ Product Grid
|
||||
```html
|
||||
{% from 'shared/macros/shop/product-grid.html' import product_grid %}
|
||||
{% from 'shared/macros/storefront/product-grid.html' import product_grid %}
|
||||
|
||||
{# Basic grid #}
|
||||
{{ product_grid(products_var='products', loading_var='loading') }}
|
||||
@@ -381,7 +381,7 @@ Reusable macros for shop/storefront functionality. Located in `app/templates/sha
|
||||
|
||||
### 🛒 Add to Cart
|
||||
```html
|
||||
{% from 'shared/macros/shop/add-to-cart.html' import add_to_cart_button, add_to_cart_form, buy_now_button %}
|
||||
{% from 'shared/macros/storefront/add-to-cart.html' import add_to_cart_button, add_to_cart_form, buy_now_button %}
|
||||
|
||||
{# Simple button #}
|
||||
{{ add_to_cart_button(action='addToCart()') }}
|
||||
@@ -397,11 +397,11 @@ Reusable macros for shop/storefront functionality. Located in `app/templates/sha
|
||||
- `add_to_cart_button()` - Simple button with loading state
|
||||
- `add_to_cart_form()` - Form with quantity selector + button
|
||||
- `buy_now_button()` - Direct checkout button
|
||||
- `shop_quantity_selector()` - Stock-aware quantity input
|
||||
- `storefront_quantity_selector()` - Stock-aware quantity input
|
||||
|
||||
### 🛒 Mini Cart (Header)
|
||||
```html
|
||||
{% from 'shared/macros/shop/mini-cart.html' import mini_cart, mini_cart_icon %}
|
||||
{% from 'shared/macros/storefront/mini-cart.html' import mini_cart, mini_cart_icon %}
|
||||
|
||||
{# Complete mini cart with dropdown #}
|
||||
{{ mini_cart(cart_var='cart', show_var='showCart') }}
|
||||
@@ -419,7 +419,7 @@ Reusable macros for shop/storefront functionality. Located in `app/templates/sha
|
||||
|
||||
### 🖼️ Product Gallery (Priority 3)
|
||||
```html
|
||||
{% from 'shared/macros/shop/product-gallery.html' import product_gallery %}
|
||||
{% from 'shared/macros/storefront/product-gallery.html' import product_gallery %}
|
||||
|
||||
{# Full gallery with thumbnails, zoom, and lightbox #}
|
||||
{{ product_gallery(images_var='product.images', enable_zoom=true, enable_lightbox=true) }}
|
||||
@@ -436,7 +436,7 @@ Reusable macros for shop/storefront functionality. Located in `app/templates/sha
|
||||
|
||||
### 🎨 Variant Selector (Priority 3)
|
||||
```html
|
||||
{% from 'shared/macros/shop/variant-selector.html' import size_selector, color_swatches %}
|
||||
{% from 'shared/macros/storefront/variant-selector.html' import size_selector, color_swatches %}
|
||||
|
||||
{# Size buttons with size guide link #}
|
||||
{{ size_selector(sizes_var='product.sizes', show_guide=true) }}
|
||||
@@ -453,7 +453,7 @@ Reusable macros for shop/storefront functionality. Located in `app/templates/sha
|
||||
|
||||
### 📄 Product Info (Priority 3)
|
||||
```html
|
||||
{% from 'shared/macros/shop/product-info.html' import product_info, product_price %}
|
||||
{% from 'shared/macros/storefront/product-info.html' import product_info, product_price %}
|
||||
|
||||
{# Complete info block #}
|
||||
{{ product_info(product_var='product', show_store=true, show_rating=true) }}
|
||||
@@ -473,7 +473,7 @@ Reusable macros for shop/storefront functionality. Located in `app/templates/sha
|
||||
|
||||
### 📑 Product Tabs (Priority 3)
|
||||
```html
|
||||
{% from 'shared/macros/shop/product-tabs.html' import product_tabs %}
|
||||
{% from 'shared/macros/storefront/product-tabs.html' import product_tabs %}
|
||||
|
||||
{# Tabbed product information #}
|
||||
{{ product_tabs(
|
||||
@@ -486,7 +486,7 @@ Reusable macros for shop/storefront functionality. Located in `app/templates/sha
|
||||
|
||||
### 📂 Category Navigation (Priority 4)
|
||||
```html
|
||||
{% from 'shared/macros/shop/category-nav.html' import category_nav, category_tree, category_menu %}
|
||||
{% from 'shared/macros/storefront/category-nav.html' import category_nav, category_tree, category_menu %}
|
||||
|
||||
{# Sidebar with nested categories #}
|
||||
{{ category_nav(categories_var='categories', current_var='currentCategory', show_count=true) }}
|
||||
@@ -502,10 +502,10 @@ Reusable macros for shop/storefront functionality. Located in `app/templates/sha
|
||||
|
||||
### 🍞 Breadcrumbs (Priority 4)
|
||||
```html
|
||||
{% from 'shared/macros/shop/breadcrumbs.html' import shop_breadcrumbs, compact_breadcrumbs %}
|
||||
{% from 'shared/macros/storefront/breadcrumbs.html' import storefront_breadcrumbs, compact_breadcrumbs %}
|
||||
|
||||
{# Static breadcrumbs #}
|
||||
{{ shop_breadcrumbs(items=[
|
||||
{{ storefront_breadcrumbs(items=[
|
||||
{'label': 'Electronics', 'url': '/electronics'},
|
||||
{'label': 'Audio', 'url': '/audio'},
|
||||
{'label': 'Headphones'}
|
||||
@@ -515,11 +515,11 @@ Reusable macros for shop/storefront functionality. Located in `app/templates/sha
|
||||
{{ compact_breadcrumbs(parent={'label': 'Audio', 'url': '/audio'}, current='Headphones') }}
|
||||
```
|
||||
|
||||
**Macros:** `shop_breadcrumbs()`, `auto_breadcrumbs()`, `compact_breadcrumbs()`
|
||||
**Macros:** `storefront_breadcrumbs()`, `auto_breadcrumbs()`, `compact_breadcrumbs()`
|
||||
|
||||
### 🔍 Search Bar (Priority 4)
|
||||
```html
|
||||
{% from 'shared/macros/shop/search-bar.html' import search_bar, search_autocomplete %}
|
||||
{% from 'shared/macros/storefront/search-bar.html' import search_bar, search_autocomplete %}
|
||||
|
||||
{# Basic search #}
|
||||
{{ search_bar(placeholder='Search products...') }}
|
||||
@@ -535,7 +535,7 @@ Reusable macros for shop/storefront functionality. Located in `app/templates/sha
|
||||
|
||||
### 🎛️ Filter Sidebar (Priority 4)
|
||||
```html
|
||||
{% from 'shared/macros/shop/filter-sidebar.html' import filter_sidebar, price_filter, sort_dropdown %}
|
||||
{% from 'shared/macros/storefront/filter-sidebar.html' import filter_sidebar, price_filter, sort_dropdown %}
|
||||
|
||||
{# Complete filter panel #}
|
||||
{{ filter_sidebar(filters_var='filters', active_filters_var='activeFilters', on_change='filterProducts()') }}
|
||||
@@ -551,7 +551,7 @@ Reusable macros for shop/storefront functionality. Located in `app/templates/sha
|
||||
|
||||
### ⭐ Star Rating (Priority 5)
|
||||
```html
|
||||
{% from 'shared/macros/shop/star-rating.html' import star_rating, rating_input, rating_summary, compact_rating %}
|
||||
{% from 'shared/macros/storefront/star-rating.html' import star_rating, rating_input, rating_summary, compact_rating %}
|
||||
|
||||
{# Static star rating with half-star support #}
|
||||
{{ star_rating(rating=4.5, show_value=true, show_count=true, count=127) }}
|
||||
@@ -573,7 +573,7 @@ Reusable macros for shop/storefront functionality. Located in `app/templates/sha
|
||||
|
||||
### 💬 Reviews (Priority 5)
|
||||
```html
|
||||
{% from 'shared/macros/shop/reviews.html' import review_card, review_list, review_form, review_summary_section %}
|
||||
{% from 'shared/macros/storefront/reviews.html' import review_card, review_list, review_form, review_summary_section %}
|
||||
|
||||
{# Single review card with helpful buttons #}
|
||||
{{ review_card(review_var='review', on_helpful='markHelpful(review.id)') }}
|
||||
@@ -592,7 +592,7 @@ Reusable macros for shop/storefront functionality. Located in `app/templates/sha
|
||||
|
||||
### 🛡️ Trust Badges (Priority 5)
|
||||
```html
|
||||
{% from 'shared/macros/shop/trust-badges.html' import trust_badges, trust_banner, payment_icons, guarantee_badge, security_seals, checkout_trust_section %}
|
||||
{% from 'shared/macros/storefront/trust-badges.html' import trust_badges, trust_banner, payment_icons, guarantee_badge, security_seals, checkout_trust_section %}
|
||||
|
||||
{# Trust badges grid #}
|
||||
{{ trust_badges(badges=['secure_payment', 'free_shipping', 'easy_returns', 'support_24_7'], layout='grid') }}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
╔══════════════════════════════════════════════════════════════════╗
|
||||
║ SHOP FRONTEND ARCHITECTURE OVERVIEW ║
|
||||
║ STOREFRONT FRONTEND ARCHITECTURE OVERVIEW ║
|
||||
║ Alpine.js + Jinja2 + Tailwind CSS + Multi-Theme ║
|
||||
╚══════════════════════════════════════════════════════════════════╝
|
||||
|
||||
📦 WHAT IS THIS?
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
Customer-facing shop frontend provides visitors with a branded
|
||||
Customer-facing storefront frontend provides visitors with a branded
|
||||
e-commerce experience unique to each store. Built with:
|
||||
✅ Jinja2 Templates (server-side rendering)
|
||||
✅ Alpine.js (client-side reactivity)
|
||||
@@ -46,7 +46,7 @@ e-commerce experience unique to each store. Built with:
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
app/
|
||||
├── templates/shop/
|
||||
├── templates/storefront/
|
||||
│ ├── base.html ← ✅ Base template (layout + theme)
|
||||
│ ├── home.html ← ✅ Homepage / featured products
|
||||
│ ├── products.html ← ✅ Product catalog with filters
|
||||
@@ -67,11 +67,11 @@ app/
|
||||
│ ├── 404.html
|
||||
│ └── 500.html
|
||||
│
|
||||
├── static/shop/
|
||||
├── static/storefront/
|
||||
│ ├── css/
|
||||
│ │ └── shop.css ← ✅ Shop-specific styles (IMPLEMENTED)
|
||||
│ │ └── storefront.css ← ✅ Storefront-specific styles (IMPLEMENTED)
|
||||
│ ├── js/
|
||||
│ │ └── shop-layout.js ← ✅ Base shop functionality (IMPLEMENTED)
|
||||
│ │ └── storefront-layout.js ← ✅ Base storefront functionality (IMPLEMENTED)
|
||||
│ └── img/
|
||||
│ └── (placeholder images)
|
||||
│
|
||||
@@ -85,7 +85,7 @@ app/
|
||||
│ └── (shared styles if needed)
|
||||
│
|
||||
└── routes/
|
||||
└── shop_pages.py ← ✅ Route handlers (IMPLEMENTED)
|
||||
└── storefront_pages.py ← ✅ Route handlers (IMPLEMENTED)
|
||||
|
||||
|
||||
🏗️ ARCHITECTURE LAYERS
|
||||
@@ -107,37 +107,37 @@ Layer 6: Database
|
||||
Layer 1: ROUTES (FastAPI)
|
||||
──────────────────────────────────────────────────────────────────
|
||||
Purpose: Store Detection + Template Rendering
|
||||
Location: app/routes/shop_pages.py
|
||||
Location: app/routes/storefront_pages.py
|
||||
|
||||
⚠️ ROUTE REGISTRATION (main.py):
|
||||
The shop router is mounted at TWO prefixes to support both access methods:
|
||||
The storefront router is mounted at TWO prefixes to support both access methods:
|
||||
|
||||
# main.py
|
||||
app.include_router(shop_pages.router, prefix="/shop", ...) # Domain/subdomain
|
||||
app.include_router(shop_pages.router, prefix="/stores/{store_code}/shop", ...) # Path-based
|
||||
app.include_router(storefront_pages.router, prefix="/storefront", ...) # Domain/subdomain
|
||||
app.include_router(storefront_pages.router, prefix="/storefront/{store_code}", ...) # Path-based
|
||||
|
||||
This means routes defined WITHOUT /shop prefix in shop_pages.py:
|
||||
@router.get("/products") → /shop/products OR /stores/{code}/shop/products
|
||||
This means routes defined WITHOUT /storefront prefix in storefront_pages.py:
|
||||
@router.get("/products") → /storefront/products OR /storefront/{code}/products
|
||||
|
||||
❌ COMMON MISTAKE: Don't add /shop prefix in route definitions!
|
||||
@router.get("/shop/products") ❌ WRONG - creates /shop/shop/products
|
||||
@router.get("/products") ✅ CORRECT - creates /shop/products
|
||||
❌ COMMON MISTAKE: Don't add /storefront prefix in route definitions!
|
||||
@router.get("/storefront/products") ❌ WRONG - creates /storefront/storefront/products
|
||||
@router.get("/products") ✅ CORRECT - creates /storefront/products
|
||||
|
||||
Example Route Handler:
|
||||
@router.get("/", response_class=HTMLResponse, include_in_schema=False)
|
||||
@router.get("/products", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def shop_products_page(request: Request):
|
||||
async def storefront_products_page(request: Request):
|
||||
"""
|
||||
Render shop homepage / product catalog.
|
||||
Render storefront homepage / product catalog.
|
||||
Store and theme are auto-injected by middleware.
|
||||
"""
|
||||
return templates.TemplateResponse(
|
||||
"shop/products.html",
|
||||
get_shop_context(request) # Helper function
|
||||
"storefront/products.html",
|
||||
get_storefront_context(request) # Helper function
|
||||
)
|
||||
|
||||
Helper Function:
|
||||
def get_shop_context(request: Request, **extra_context) -> dict:
|
||||
def get_storefront_context(request: Request, **extra_context) -> dict:
|
||||
"""Build template context with store/theme from middleware"""
|
||||
store = getattr(request.state, 'store', None)
|
||||
theme = getattr(request.state, 'theme', None)
|
||||
@@ -176,39 +176,39 @@ Responsibilities:
|
||||
|
||||
⭐ MULTI-ACCESS ROUTING (Domain, Subdomain, Path-Based)
|
||||
──────────────────────────────────────────────────────────────────
|
||||
The shop frontend supports THREE access methods:
|
||||
The storefront frontend supports THREE access methods:
|
||||
|
||||
1. **Custom Domain** (Production)
|
||||
URL: https://customdomain.com/shop/products
|
||||
URL: https://customdomain.com/storefront/products
|
||||
- Store has their own domain
|
||||
- base_url = "/"
|
||||
- Links: /shop/products, /shop/about, /shop/contact
|
||||
- Links: /storefront/products, /storefront/about, /storefront/contact
|
||||
|
||||
2. **Subdomain** (Production)
|
||||
URL: https://orion.letzshop.com/shop/products
|
||||
URL: https://orion.letzshop.com/storefront/products
|
||||
- Store uses platform subdomain
|
||||
- base_url = "/"
|
||||
- Links: /shop/products, /shop/about, /shop/contact
|
||||
- Links: /storefront/products, /storefront/about, /storefront/contact
|
||||
|
||||
3. **Path-Based** (Development/Testing)
|
||||
URL: http://localhost:8000/stores/orion/shop/products
|
||||
URL: http://localhost:8000/storefront/orion/products
|
||||
- Store accessed via path prefix
|
||||
- base_url = "/stores/orion/"
|
||||
- Links: /stores/orion/shop/products, /stores/orion/shop/about
|
||||
- base_url = "/storefront/orion/"
|
||||
- Links: /storefront/orion/products, /storefront/orion/about
|
||||
|
||||
⚠️ CRITICAL: All template links MUST use {{ base_url }}shop/ prefix
|
||||
⚠️ CRITICAL: All template links MUST use {{ base_url }}storefront/ prefix
|
||||
|
||||
Example:
|
||||
❌ BAD: <a href="/products">Products</a>
|
||||
❌ BAD: <a href="{{ base_url }}products">Products</a>
|
||||
✅ GOOD: <a href="{{ base_url }}shop/products">Products</a>
|
||||
✅ GOOD: <a href="{{ base_url }}storefront/products">Products</a>
|
||||
|
||||
Note: The router is mounted at /shop prefix in main.py, so all links need shop/ after base_url
|
||||
Note: The router is mounted at /storefront prefix in main.py, so all links need storefront/ after base_url
|
||||
|
||||
How It Works:
|
||||
1. StoreContextMiddleware detects access method
|
||||
2. Sets request.state.store_context with detection_method
|
||||
3. get_shop_context() calculates base_url from detection_method
|
||||
3. get_storefront_context() calculates base_url from detection_method
|
||||
4. Templates use {{ base_url }} for all internal links
|
||||
5. Links work correctly regardless of access method
|
||||
|
||||
@@ -243,7 +243,7 @@ Detection Methods:
|
||||
Layer 3: TEMPLATES (Jinja2)
|
||||
──────────────────────────────────────────────────────────────────
|
||||
Purpose: HTML Structure + Store Branding
|
||||
Location: app/templates/shop/
|
||||
Location: app/templates/storefront/
|
||||
|
||||
Template Hierarchy:
|
||||
base.html (layout + theme injection)
|
||||
@@ -253,7 +253,7 @@ Template Hierarchy:
|
||||
partials/product-card.html (components)
|
||||
|
||||
Example:
|
||||
{% extends "shop/base.html" %}
|
||||
{% extends "storefront/base.html" %}
|
||||
{% block title %}{{ store.name }}{% endblock %}
|
||||
{% block alpine_data %}shopHome(){% endblock %}
|
||||
{% block content %}
|
||||
@@ -276,7 +276,7 @@ Key Features:
|
||||
Layer 4: JAVASCRIPT (Alpine.js)
|
||||
──────────────────────────────────────────────────────────────────
|
||||
Purpose: Client-Side Interactivity + Cart + Search
|
||||
Location: app/static/shop/js/
|
||||
Location: app/static/storefront/js/
|
||||
|
||||
⚠️ CRITICAL: JavaScript Loading Order
|
||||
──────────────────────────────────────────────────────────────────
|
||||
@@ -284,14 +284,14 @@ Scripts MUST load in this exact order (see base.html):
|
||||
|
||||
1. log-config.js ← Logging system (loads first)
|
||||
2. icons.js ← Icon registry
|
||||
3. shop-layout.js ← Alpine component (before Alpine!)
|
||||
3. storefront-layout.js ← Alpine component (before Alpine!)
|
||||
4. utils.js ← Utility functions
|
||||
5. api-client.js ← API wrapper
|
||||
6. Alpine.js (deferred) ← Loads last
|
||||
7. Page-specific JS ← Optional page scripts
|
||||
|
||||
Why This Order Matters:
|
||||
• shop-layout.js defines storefrontLayoutData() BEFORE Alpine initializes
|
||||
• storefront-layout.js defines storefrontLayoutData() BEFORE Alpine initializes
|
||||
• Alpine.js defers to ensure DOM is ready
|
||||
• Shared utilities available to all scripts
|
||||
• Icons and logging available immediately
|
||||
@@ -299,7 +299,7 @@ Why This Order Matters:
|
||||
Example from base.html:
|
||||
<script src="{{ url_for('static', path='shared/js/log-config.js') }}"></script>
|
||||
<script src="{{ url_for('static', path='shared/js/icons.js') }}"></script>
|
||||
<script src="{{ url_for('static', path='shop/js/shop-layout.js') }}"></script>
|
||||
<script src="{{ url_for('static', path='storefront/js/storefront-layout.js') }}"></script>
|
||||
<script src="{{ url_for('static', path='shared/js/utils.js') }}"></script>
|
||||
<script src="{{ url_for('static', path='shared/js/api-client.js') }}"></script>
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
@@ -307,14 +307,14 @@ Example from base.html:
|
||||
Alpine.js Component Architecture:
|
||||
──────────────────────────────────────────────────────────────────
|
||||
|
||||
⭐ BASE COMPONENT (shop-layout.js):
|
||||
⭐ BASE COMPONENT (storefront-layout.js):
|
||||
|
||||
Provides shared functionality for all shop pages:
|
||||
Provides shared functionality for all storefront pages:
|
||||
|
||||
function storefrontLayoutData() {
|
||||
return {
|
||||
// Theme state
|
||||
dark: localStorage.getItem('shop-theme') === 'dark',
|
||||
dark: localStorage.getItem('storefront-theme') === 'dark',
|
||||
|
||||
// UI state
|
||||
mobileMenuOpen: false,
|
||||
@@ -325,12 +325,12 @@ Provides shared functionality for all shop pages:
|
||||
cart: [],
|
||||
|
||||
init() {
|
||||
shopLog.info('Shop layout initializing...');
|
||||
shopLog.info('Storefront layout initializing...');
|
||||
this.loadCart();
|
||||
window.addEventListener('cart-updated', () => {
|
||||
this.loadCart();
|
||||
});
|
||||
shopLog.info('Shop layout initialized');
|
||||
shopLog.info('Storefront layout initialized');
|
||||
},
|
||||
|
||||
addToCart(product, quantity = 1) {
|
||||
@@ -356,7 +356,7 @@ Provides shared functionality for all shop pages:
|
||||
|
||||
toggleTheme() {
|
||||
this.dark = !this.dark;
|
||||
localStorage.setItem('shop-theme',
|
||||
localStorage.setItem('storefront-theme',
|
||||
this.dark ? 'dark' : 'light');
|
||||
shopLog.debug('Theme toggled:', this.dark ? 'dark' : 'light');
|
||||
},
|
||||
@@ -393,7 +393,7 @@ Each page extends storefrontLayoutData() for page-specific functionality:
|
||||
|
||||
// Page-specific methods
|
||||
async loadProducts() {
|
||||
const response = await fetch('/api/v1/shop/products');
|
||||
const response = await fetch('/api/v1/storefront/products');
|
||||
const data = await response.json();
|
||||
this.products = data.products;
|
||||
this.loading = false;
|
||||
@@ -454,30 +454,30 @@ Responsibilities:
|
||||
Layer 5: API (REST)
|
||||
──────────────────────────────────────────────────────────────────
|
||||
Purpose: Product Data + Cart + Orders
|
||||
Location: app/api/v1/shop/*.py
|
||||
Location: app/api/v1/storefront/*.py
|
||||
|
||||
⭐ NEW API STRUCTURE (as of 2025-11-22):
|
||||
All shop endpoints use middleware-based store context.
|
||||
All storefront endpoints use middleware-based store context.
|
||||
NO store_id or store_code in URLs!
|
||||
|
||||
Example Endpoints:
|
||||
GET /api/v1/shop/products ← Product catalog
|
||||
GET /api/v1/shop/products/{id} ← Product details
|
||||
GET /api/v1/shop/products?search=... ← Search products
|
||||
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/{product_id} ← Update item
|
||||
DELETE /api/v1/shop/cart/{session_id}/items/{product_id} ← Remove item
|
||||
POST /api/v1/shop/orders ← Place order (auth required)
|
||||
GET /api/v1/shop/orders ← Order history (auth required)
|
||||
POST /api/v1/shop/auth/login ← Customer login
|
||||
POST /api/v1/shop/auth/register ← Customer registration
|
||||
GET /api/v1/shop/content-pages/navigation ← CMS navigation
|
||||
GET /api/v1/shop/content-pages/{slug} ← CMS page content
|
||||
GET /api/v1/storefront/products ← Product catalog
|
||||
GET /api/v1/storefront/products/{id} ← Product details
|
||||
GET /api/v1/storefront/products?search=... ← Search products
|
||||
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/{product_id} ← Update item
|
||||
DELETE /api/v1/storefront/cart/{session_id}/items/{product_id} ← Remove item
|
||||
POST /api/v1/storefront/orders ← Place order (auth required)
|
||||
GET /api/v1/storefront/orders ← Order history (auth required)
|
||||
POST /api/v1/storefront/auth/login ← Customer login
|
||||
POST /api/v1/storefront/auth/register ← Customer registration
|
||||
GET /api/v1/storefront/content-pages/navigation ← CMS navigation
|
||||
GET /api/v1/storefront/content-pages/{slug} ← CMS page content
|
||||
|
||||
How Store Context Works:
|
||||
1. Browser makes API call from shop page (e.g., /stores/orion/shop/products)
|
||||
2. Browser automatically sends Referer header: http://localhost:8000/stores/orion/shop/products
|
||||
1. Browser makes API call from storefront page (e.g., /storefront/orion/products)
|
||||
2. Browser automatically sends Referer header: http://localhost:8000/storefront/orion/products
|
||||
3. StoreContextMiddleware extracts store from Referer header
|
||||
4. Middleware sets request.state.store = <Store: orion>
|
||||
5. API endpoint accesses store: store = request.state.store
|
||||
@@ -489,13 +489,13 @@ How Store Context Works:
|
||||
|
||||
Page Load Flow:
|
||||
──────────────────────────────────────────────────────────────────
|
||||
1. Customer → visits acme-shop.com (or /stores/acme/shop/products)
|
||||
1. Customer → visits acme-shop.com (or /storefront/acme/products)
|
||||
2. Store Middleware → Identifies "ACME" store from domain/path
|
||||
3. Theme Middleware → Loads ACME's theme config
|
||||
4. FastAPI → Renders shop/products.html
|
||||
4. FastAPI → Renders storefront/products.html
|
||||
5. Browser → Receives HTML with theme CSS variables
|
||||
6. Alpine.js → init() executes
|
||||
7. JavaScript → GET /api/v1/shop/products (with Referer header)
|
||||
7. JavaScript → GET /api/v1/storefront/products (with Referer header)
|
||||
8. Middleware → Extracts store from Referer, injects into request.state
|
||||
9. API → Returns product list JSON for ACME store
|
||||
10. Alpine.js → Updates products array
|
||||
@@ -516,7 +516,7 @@ Checkout Flow:
|
||||
1. Customer → Goes to /cart
|
||||
2. Page → Loads cart from localStorage
|
||||
3. Customer → Fills checkout form
|
||||
4. Alpine.js → POST /api/v1/shop/orders (with Referer header)
|
||||
4. Alpine.js → POST /api/v1/storefront/orders (with Referer header)
|
||||
5. Middleware → Extracts store from Referer
|
||||
6. API → Creates order + payment intent for store
|
||||
7. Alpine.js → Redirects to payment
|
||||
@@ -621,7 +621,7 @@ Key Functions:
|
||||
|
||||
Cart Persistence:
|
||||
• Survives page refresh
|
||||
• Shared across shop pages
|
||||
• Shared across storefront pages
|
||||
• Cleared on checkout completion
|
||||
• Synced across tabs (optional)
|
||||
|
||||
@@ -637,7 +637,7 @@ Search System:
|
||||
• Keyboard shortcuts (Cmd+K)
|
||||
|
||||
2. Search API
|
||||
POST /api/v1/shop/{store_code}/search
|
||||
POST /api/v1/storefront/{store_code}/search
|
||||
{
|
||||
"query": "laptop",
|
||||
"category": "electronics",
|
||||
@@ -730,10 +730,10 @@ Account Features:
|
||||
✅ Profile management
|
||||
|
||||
Auth Flow:
|
||||
1. Login/Register → POST /api/v1/shop/auth/login (with Referer header)
|
||||
1. Login/Register → POST /api/v1/storefront/auth/login (with Referer header)
|
||||
2. Middleware → Extracts store from Referer
|
||||
3. API → Validates credentials for store's customers
|
||||
4. API → Returns JWT token + sets cookie (path=/shop)
|
||||
4. API → Returns JWT token + sets cookie (path=/storefront)
|
||||
5. JavaScript → Store token in localStorage
|
||||
6. API Client → Add token to authenticated requests
|
||||
7. Optional → Use account features (orders, profile, etc.)
|
||||
@@ -745,9 +745,9 @@ Auth Flow:
|
||||
All authentication pages use Tailwind CSS, Alpine.js, and theme integration
|
||||
for a consistent, branded experience across all stores.
|
||||
|
||||
✅ Login Page (app/templates/shop/account/login.html)
|
||||
✅ Login Page (app/templates/storefront/account/login.html)
|
||||
──────────────────────────────────────────────────────────────────
|
||||
Route: /shop/account/login
|
||||
Route: /storefront/account/login
|
||||
|
||||
Features:
|
||||
• Two-column layout (branding + form)
|
||||
@@ -773,7 +773,7 @@ Alpine.js Component:
|
||||
dark: false,
|
||||
|
||||
async handleLogin() {
|
||||
// POST /api/v1/shop/auth/login
|
||||
// POST /api/v1/storefront/auth/login
|
||||
// Store token in localStorage
|
||||
// Redirect to account or return URL
|
||||
}
|
||||
@@ -781,13 +781,13 @@ Alpine.js Component:
|
||||
}
|
||||
|
||||
API Endpoint:
|
||||
POST /api/v1/shop/auth/login
|
||||
POST /api/v1/storefront/auth/login
|
||||
Body: { email_or_username, password }
|
||||
Returns: { access_token, user }
|
||||
|
||||
✅ Register Page (app/templates/shop/account/register.html)
|
||||
✅ Register Page (app/templates/storefront/account/register.html)
|
||||
──────────────────────────────────────────────────────────────────
|
||||
Route: /shop/account/register
|
||||
Route: /storefront/account/register
|
||||
|
||||
Features:
|
||||
• Two-column layout with store branding
|
||||
@@ -824,20 +824,20 @@ Alpine.js Component:
|
||||
},
|
||||
|
||||
async handleRegister() {
|
||||
// POST /api/v1/shop/auth/register
|
||||
// POST /api/v1/storefront/auth/register
|
||||
// Redirect to login?registered=true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
API Endpoint:
|
||||
POST /api/v1/shop/auth/register
|
||||
POST /api/v1/storefront/auth/register
|
||||
Body: { first_name, last_name, email, phone?, password, marketing_consent }
|
||||
Returns: { message }
|
||||
|
||||
✅ Forgot Password Page (app/templates/shop/account/forgot-password.html)
|
||||
✅ Forgot Password Page (app/templates/storefront/account/forgot-password.html)
|
||||
──────────────────────────────────────────────────────────────────
|
||||
Route: /shop/account/forgot-password
|
||||
Route: /storefront/account/forgot-password
|
||||
|
||||
Features:
|
||||
• Two-column layout with store branding
|
||||
@@ -848,7 +848,7 @@ Features:
|
||||
• Success state with checkmark icon
|
||||
• Option to retry if email not received
|
||||
• Theme-aware styling
|
||||
• Links back to login and shop
|
||||
• Links back to login and storefront
|
||||
• Dark mode support
|
||||
|
||||
Alpine.js Component:
|
||||
@@ -859,7 +859,7 @@ Alpine.js Component:
|
||||
loading: false,
|
||||
|
||||
async handleSubmit() {
|
||||
// POST /api/v1/shop/auth/forgot-password
|
||||
// POST /api/v1/storefront/auth/forgot-password
|
||||
// Show success message
|
||||
// emailSent = true
|
||||
}
|
||||
@@ -867,7 +867,7 @@ Alpine.js Component:
|
||||
}
|
||||
|
||||
API Endpoint:
|
||||
POST /api/v1/shop/auth/forgot-password
|
||||
POST /api/v1/storefront/auth/forgot-password
|
||||
Body: { email }
|
||||
Returns: { message }
|
||||
|
||||
@@ -907,7 +907,7 @@ Key Theme Elements:
|
||||
|
||||
Benefits:
|
||||
✅ Each store's auth pages match their brand
|
||||
✅ Consistent with main shop design
|
||||
✅ Consistent with main storefront design
|
||||
✅ Dark mode adapts to store colors
|
||||
✅ Professional, polished appearance
|
||||
|
||||
@@ -959,17 +959,17 @@ No store_code needed! Store extracted from Referer header automatically.
|
||||
|
||||
Usage:
|
||||
// Product catalog
|
||||
const products = await fetch('/api/v1/shop/products');
|
||||
const products = await fetch('/api/v1/storefront/products');
|
||||
|
||||
// Add to cart
|
||||
const response = await fetch('/api/v1/shop/cart/session123/items', {
|
||||
const response = await fetch('/api/v1/storefront/cart/session123/items', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ product_id: 1, quantity: 2 })
|
||||
});
|
||||
|
||||
// Place order
|
||||
const order = await fetch('/api/v1/shop/orders', {
|
||||
const order = await fetch('/api/v1/storefront/orders', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(orderData)
|
||||
@@ -987,7 +987,7 @@ Features:
|
||||
|
||||
Location: app/static/shared/js/log-config.js
|
||||
|
||||
Shop-Specific Logging:
|
||||
Storefront-Specific Logging:
|
||||
shopLog.info('Product added to cart', product);
|
||||
shopLog.error('Checkout failed', error);
|
||||
shopLog.debug('Search query', { query, results });
|
||||
@@ -1008,7 +1008,7 @@ Usage:
|
||||
<span x-html="$icon('shopping-cart', 'w-5 h-5')"></span>
|
||||
<span x-html="$icon('search', 'w-6 h-6 text-primary')"></span>
|
||||
|
||||
Shop Icons:
|
||||
Storefront Icons:
|
||||
• shopping-cart, shopping-bag
|
||||
• heart (wishlist)
|
||||
• search, filter
|
||||
@@ -1066,7 +1066,7 @@ Components:
|
||||
• Category cards
|
||||
• About store section
|
||||
Data Sources:
|
||||
• GET /api/v1/shop/products?is_featured=true
|
||||
• GET /api/v1/storefront/products?is_featured=true
|
||||
|
||||
/products
|
||||
──────────────────────────────────────────────────────────────────
|
||||
@@ -1077,8 +1077,8 @@ Components:
|
||||
• Sort dropdown
|
||||
• Pagination
|
||||
Data Sources:
|
||||
• GET /api/v1/shop/products?skip=0&limit=20
|
||||
• GET /api/v1/shop/products?search=query
|
||||
• GET /api/v1/storefront/products?skip=0&limit=20
|
||||
• GET /api/v1/storefront/products?search=query
|
||||
• Filters applied client-side or server-side
|
||||
|
||||
/products/{product_id}
|
||||
@@ -1091,8 +1091,8 @@ Components:
|
||||
• Related products
|
||||
• Reviews (optional)
|
||||
Data Sources:
|
||||
• GET /api/v1/shop/products/{id}
|
||||
• GET /api/v1/shop/products?limit=4 (related products)
|
||||
• GET /api/v1/storefront/products/{id}
|
||||
• GET /api/v1/storefront/products?limit=4 (related products)
|
||||
|
||||
/cart
|
||||
──────────────────────────────────────────────────────────────────
|
||||
@@ -1116,7 +1116,7 @@ Components:
|
||||
• Order summary
|
||||
• Submit button
|
||||
Data Sources:
|
||||
• POST /api/v1/shop/orders
|
||||
• POST /api/v1/storefront/orders
|
||||
• Stripe/PayPal integration
|
||||
|
||||
/search
|
||||
@@ -1128,7 +1128,7 @@ Components:
|
||||
• Filter options
|
||||
• Sort options
|
||||
Data Sources:
|
||||
• GET /api/v1/shop/products?search=query
|
||||
• GET /api/v1/storefront/products?search=query
|
||||
|
||||
/category/{category_slug}
|
||||
──────────────────────────────────────────────────────────────────
|
||||
@@ -1139,7 +1139,7 @@ Components:
|
||||
• Subcategories
|
||||
• Filters
|
||||
Data Sources:
|
||||
• GET /api/v1/shop/products?category={slug}
|
||||
• GET /api/v1/storefront/products?category={slug}
|
||||
|
||||
/about
|
||||
──────────────────────────────────────────────────────────────────
|
||||
@@ -1162,7 +1162,7 @@ Components:
|
||||
• Business hours
|
||||
• Social links
|
||||
Data Sources:
|
||||
• CMS content page (GET /api/v1/shop/content-pages/contact)
|
||||
• CMS content page (GET /api/v1/storefront/content-pages/contact)
|
||||
• Form submission to store email
|
||||
|
||||
|
||||
@@ -1179,7 +1179,7 @@ For New Developers:
|
||||
|
||||
2. Study Existing Page (2 hours)
|
||||
→ Open home.html
|
||||
→ Open shop-layout.js
|
||||
→ Open storefront-layout.js
|
||||
→ Trace product loading flow
|
||||
→ Examine cart management
|
||||
|
||||
@@ -1228,20 +1228,20 @@ Before Deploying:
|
||||
|
||||
Multi-Access Aware Error Pages:
|
||||
|
||||
All shop error pages (404, 500, etc.) are store-context aware and display
|
||||
All storefront error pages (404, 500, etc.) are store-context aware and display
|
||||
correct links based on the access method (domain, subdomain, or path-based).
|
||||
|
||||
Error Page Templates:
|
||||
• app/templates/shop/errors/404.html - Not Found
|
||||
• app/templates/shop/errors/400.html - Bad Request
|
||||
• app/templates/shop/errors/401.html - Unauthorized
|
||||
• app/templates/shop/errors/403.html - Forbidden
|
||||
• app/templates/shop/errors/422.html - Validation Error
|
||||
• app/templates/shop/errors/429.html - Rate Limited
|
||||
• app/templates/shop/errors/500.html - Server Error
|
||||
• app/templates/shop/errors/502.html - Bad Gateway
|
||||
• app/templates/shop/errors/base.html - Base error template
|
||||
• app/templates/shop/errors/generic.html - Generic error
|
||||
• app/templates/storefront/errors/404.html - Not Found
|
||||
• app/templates/storefront/errors/400.html - Bad Request
|
||||
• app/templates/storefront/errors/401.html - Unauthorized
|
||||
• app/templates/storefront/errors/403.html - Forbidden
|
||||
• app/templates/storefront/errors/422.html - Validation Error
|
||||
• app/templates/storefront/errors/429.html - Rate Limited
|
||||
• app/templates/storefront/errors/500.html - Server Error
|
||||
• app/templates/storefront/errors/502.html - Bad Gateway
|
||||
• app/templates/storefront/errors/base.html - Base error template
|
||||
• app/templates/storefront/errors/generic.html - Generic error
|
||||
|
||||
Error Renderer (app/exceptions/error_renderer.py):
|
||||
|
||||
@@ -1252,7 +1252,7 @@ Calculates base_url dynamically based on store access method:
|
||||
access_method = getattr(request.state, "access_method", None)
|
||||
store_context = getattr(request.state, "store_context", None)
|
||||
|
||||
# Calculate base_url for shop links
|
||||
# Calculate base_url for storefront links
|
||||
base_url = "/"
|
||||
if access_method == "path" and store:
|
||||
full_prefix = store_context.get('full_prefix', '/store/')
|
||||
@@ -1280,13 +1280,13 @@ All error page links use {{ base_url }} prefix for correct routing:
|
||||
How It Works:
|
||||
|
||||
1. Error occurs (404, 500, etc.)
|
||||
2. Exception handler detects shop context
|
||||
2. Exception handler detects storefront context
|
||||
3. error_renderer.py calculates base_url from store_context
|
||||
4. Error template renders with correct base_url
|
||||
5. Links work for all access methods:
|
||||
- Domain: customshop.com → base_url = "/"
|
||||
- Subdomain: orion.platform.com → base_url = "/"
|
||||
- Path: localhost/stores/orion/ → base_url = "/stores/orion/"
|
||||
- Path: localhost/storefront/orion/ → base_url = "/storefront/orion/"
|
||||
|
||||
Benefits:
|
||||
✅ Error pages work correctly regardless of access method
|
||||
@@ -1333,13 +1333,13 @@ Documentation:
|
||||
• FastAPI: https://fastapi.tiangolo.com/
|
||||
|
||||
Internal Docs:
|
||||
• Page Template Guide: FRONTEND_SHOP_ALPINE_PAGE_TEMPLATE.md
|
||||
• Multi-Theme Guide: MULTI_THEME_SHOP_GUIDE.md
|
||||
• Page Template Guide: FRONTEND_STOREFRONT_ALPINE_PAGE_TEMPLATE.md
|
||||
• Multi-Theme Guide: MULTI_THEME_STOREFRONT_GUIDE.md
|
||||
• API Documentation: API_REFERENCE.md
|
||||
• Database Schema: DATABASE_SCHEMA.md
|
||||
|
||||
|
||||
══════════════════════════════════════════════════════════════════
|
||||
SHOP FRONTEND ARCHITECTURE
|
||||
STOREFRONT FRONTEND ARCHITECTURE
|
||||
Theme-Driven, Customer-Focused, Brand-Consistent
|
||||
══════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Shop Authentication Pages
|
||||
# Storefront Authentication Pages
|
||||
|
||||
## Overview
|
||||
|
||||
This document details the implementation of customer authentication pages in the shop frontend. All pages use Tailwind CSS, Alpine.js, and integrate with the multi-theme system for a branded, consistent experience across all stores.
|
||||
This document details the implementation of customer authentication pages in the storefront frontend. All pages use Tailwind CSS, Alpine.js, and integrate with the multi-theme system for a branded, consistent experience across all stores.
|
||||
|
||||
## Implementation Date
|
||||
2025-11-24
|
||||
@@ -12,8 +12,8 @@ This document details the implementation of customer authentication pages in the
|
||||
## 📄 Available Pages
|
||||
|
||||
### 1. Login Page
|
||||
**Location:** `app/templates/shop/account/login.html`
|
||||
**Route:** `/shop/account/login`
|
||||
**Location:** `app/templates/storefront/account/login.html`
|
||||
**Route:** `/storefront/account/login`
|
||||
|
||||
#### Features
|
||||
- Two-column layout with store branding on the left
|
||||
@@ -41,7 +41,7 @@ function customerLogin() {
|
||||
|
||||
async handleLogin() {
|
||||
// Validates input
|
||||
// Calls POST /api/v1/shop/auth/login
|
||||
// Calls POST /api/v1/storefront/auth/login
|
||||
// Stores token in localStorage
|
||||
// Redirects to account page or return URL
|
||||
}
|
||||
@@ -50,15 +50,15 @@ function customerLogin() {
|
||||
```
|
||||
|
||||
#### API Integration
|
||||
- **Endpoint:** `POST /api/v1/shop/auth/login`
|
||||
- **Endpoint:** `POST /api/v1/storefront/auth/login`
|
||||
- **Request:** `{ email_or_username: string, password: string }`
|
||||
- **Response:** `{ access_token: string, user: object }`
|
||||
|
||||
---
|
||||
|
||||
### 2. Register Page
|
||||
**Location:** `app/templates/shop/account/register.html`
|
||||
**Route:** `/shop/account/register`
|
||||
**Location:** `app/templates/storefront/account/register.html`
|
||||
**Route:** `/storefront/account/register`
|
||||
|
||||
#### Features
|
||||
- Two-column layout with store branding
|
||||
@@ -110,7 +110,7 @@ function customerRegistration() {
|
||||
|
||||
async handleRegister() {
|
||||
// Validates form
|
||||
// Calls POST /api/v1/shop/auth/register
|
||||
// Calls POST /api/v1/storefront/auth/register
|
||||
// Shows success message
|
||||
// Redirects to login with ?registered=true
|
||||
}
|
||||
@@ -119,15 +119,15 @@ function customerRegistration() {
|
||||
```
|
||||
|
||||
#### API Integration
|
||||
- **Endpoint:** `POST /api/v1/shop/auth/register`
|
||||
- **Endpoint:** `POST /api/v1/storefront/auth/register`
|
||||
- **Request:** `{ first_name: string, last_name: string, email: string, phone?: string, password: string, marketing_consent: boolean }`
|
||||
- **Response:** `{ message: string }`
|
||||
|
||||
---
|
||||
|
||||
### 3. Forgot Password Page
|
||||
**Location:** `app/templates/shop/account/forgot-password.html`
|
||||
**Route:** `/shop/account/forgot-password`
|
||||
**Location:** `app/templates/storefront/account/forgot-password.html`
|
||||
**Route:** `/storefront/account/forgot-password`
|
||||
|
||||
#### Features
|
||||
- Two-column layout with store branding
|
||||
@@ -142,7 +142,7 @@ function customerRegistration() {
|
||||
- Instructions to check inbox
|
||||
- Option to retry if email not received
|
||||
- Theme-aware styling
|
||||
- Links back to login and shop homepage
|
||||
- Links back to login and storefront homepage
|
||||
- Dark mode support
|
||||
- Mobile responsive
|
||||
|
||||
@@ -159,7 +159,7 @@ function forgotPassword() {
|
||||
|
||||
async handleSubmit() {
|
||||
// Validates email
|
||||
// Calls POST /api/v1/shop/auth/forgot-password
|
||||
// Calls POST /api/v1/storefront/auth/forgot-password
|
||||
// Sets emailSent = true on success
|
||||
// Shows confirmation message
|
||||
}
|
||||
@@ -168,7 +168,7 @@ function forgotPassword() {
|
||||
```
|
||||
|
||||
#### API Integration
|
||||
- **Endpoint:** `POST /api/v1/shop/auth/forgot-password`
|
||||
- **Endpoint:** `POST /api/v1/storefront/auth/forgot-password`
|
||||
- **Request:** `{ email: string }`
|
||||
- **Response:** `{ message: string }`
|
||||
|
||||
@@ -214,7 +214,7 @@ All authentication pages inject the store's theme CSS variables for consistent b
|
||||
|
||||
### Benefits
|
||||
- ✅ Each store's auth pages automatically match their brand
|
||||
- ✅ Consistent with main shop design
|
||||
- ✅ Consistent with main storefront design
|
||||
- ✅ Dark mode adapts to store colors
|
||||
- ✅ Professional, polished appearance
|
||||
- ✅ No custom CSS needed per store
|
||||
@@ -303,7 +303,7 @@ All authentication pages inject the store's theme CSS variables for consistent b
|
||||
## 🔗 Navigation Flow
|
||||
|
||||
```
|
||||
Shop Homepage
|
||||
Storefront Homepage
|
||||
↓
|
||||
Login Page ←→ Register Page
|
||||
↓ ↓
|
||||
@@ -314,16 +314,16 @@ Check Email Account/Cart
|
||||
|
||||
### Link Structure
|
||||
- **Login Page:**
|
||||
- "Forgot password?" → `/shop/account/forgot-password`
|
||||
- "Create an account" → `/shop/account/register`
|
||||
- "← Continue shopping" → `/shop/`
|
||||
- "Forgot password?" → `/storefront/account/forgot-password`
|
||||
- "Create an account" → `/storefront/account/register`
|
||||
- "← Continue shopping" → `/storefront/`
|
||||
|
||||
- **Register Page:**
|
||||
- "Already have an account? Sign in instead" → `/shop/account/login`
|
||||
- "Already have an account? Sign in instead" → `/storefront/account/login`
|
||||
|
||||
- **Forgot Password Page:**
|
||||
- "Remember your password? Sign in" → `/shop/account/login`
|
||||
- "← Continue shopping" → `/shop/`
|
||||
- "Remember your password? Sign in" → `/storefront/account/login`
|
||||
- "← Continue shopping" → `/storefront/`
|
||||
|
||||
All links use `{{ base_url }}` for multi-access routing support.
|
||||
|
||||
@@ -386,12 +386,12 @@ No code changes needed - all controlled via theme configuration.
|
||||
|
||||
```
|
||||
app/
|
||||
├── templates/shop/account/
|
||||
├── templates/storefront/account/
|
||||
│ ├── login.html ← Customer login page
|
||||
│ ├── register.html ← Customer registration page
|
||||
│ └── forgot-password.html ← Password reset page
|
||||
│
|
||||
└── api/v1/shop/
|
||||
└── api/v1/storefront/
|
||||
└── auth.py ← Authentication endpoints
|
||||
|
||||
static/shared/css/
|
||||
@@ -463,7 +463,7 @@ Possible additions:
|
||||
|
||||
## 📚 Related Documentation
|
||||
|
||||
- [Shop Frontend Architecture](./architecture.md)
|
||||
- [Storefront Frontend Architecture](./architecture.md)
|
||||
- [Page Template Guide](./page-templates.md)
|
||||
- [Theme System Overview](../../architecture/theme-system/overview.md)
|
||||
- [Theme Presets](../../architecture/theme-system/presets.md)
|
||||
|
||||
@@ -28,7 +28,7 @@ This document proposes a comprehensive set of reusable Jinja macro components fo
|
||||
### Priority 1: Core Shopping Components
|
||||
|
||||
#### 1.1 Product Card
|
||||
**File:** `app/templates/shared/macros/shop/product-card.html`
|
||||
**File:** `app/templates/shared/macros/storefront/product-card.html`
|
||||
|
||||
A versatile product card for grids, carousels, and lists.
|
||||
|
||||
@@ -63,7 +63,7 @@ A versatile product card for grids, carousels, and lists.
|
||||
---
|
||||
|
||||
#### 1.2 Product Grid
|
||||
**File:** `app/templates/shared/macros/shop/product-grid.html`
|
||||
**File:** `app/templates/shared/macros/storefront/product-grid.html`
|
||||
|
||||
Responsive grid layout for product listings.
|
||||
|
||||
@@ -85,7 +85,7 @@ Responsive grid layout for product listings.
|
||||
---
|
||||
|
||||
#### 1.3 Add to Cart Button
|
||||
**File:** `app/templates/shared/macros/shop/add-to-cart.html`
|
||||
**File:** `app/templates/shared/macros/storefront/add-to-cart.html`
|
||||
|
||||
Standardized add-to-cart functionality.
|
||||
|
||||
@@ -115,7 +115,7 @@ Standardized add-to-cart functionality.
|
||||
Shop-specific wrapper with stock validation:
|
||||
|
||||
```jinja
|
||||
{{ shop_quantity_selector(
|
||||
{{ storefront_quantity_selector(
|
||||
model='quantity',
|
||||
max='product.stock',
|
||||
disabled_var='addingToCart',
|
||||
@@ -128,7 +128,7 @@ Shop-specific wrapper with stock validation:
|
||||
### Priority 2: Cart Components
|
||||
|
||||
#### 2.1 Mini Cart (Header Dropdown)
|
||||
**File:** `app/templates/shared/macros/shop/mini-cart.html`
|
||||
**File:** `app/templates/shared/macros/storefront/mini-cart.html`
|
||||
|
||||
Dropdown cart preview in the header.
|
||||
|
||||
@@ -151,7 +151,7 @@ Dropdown cart preview in the header.
|
||||
---
|
||||
|
||||
#### 2.2 Cart Item Row
|
||||
**File:** `app/templates/shared/macros/shop/cart-item.html`
|
||||
**File:** `app/templates/shared/macros/storefront/cart-item.html`
|
||||
|
||||
Individual cart line item.
|
||||
|
||||
@@ -177,7 +177,7 @@ Individual cart line item.
|
||||
---
|
||||
|
||||
#### 2.3 Cart Summary
|
||||
**File:** `app/templates/shared/macros/shop/cart-summary.html`
|
||||
**File:** `app/templates/shared/macros/storefront/cart-summary.html`
|
||||
|
||||
Order summary sidebar/section.
|
||||
|
||||
@@ -205,7 +205,7 @@ Order summary sidebar/section.
|
||||
### Priority 3: Product Detail Components
|
||||
|
||||
#### 3.1 Product Gallery
|
||||
**File:** `app/templates/shared/macros/shop/product-gallery.html`
|
||||
**File:** `app/templates/shared/macros/storefront/product-gallery.html`
|
||||
|
||||
Image gallery with thumbnails and zoom.
|
||||
|
||||
@@ -229,7 +229,7 @@ Image gallery with thumbnails and zoom.
|
||||
---
|
||||
|
||||
#### 3.2 Variant Selector
|
||||
**File:** `app/templates/shared/macros/shop/variant-selector.html`
|
||||
**File:** `app/templates/shared/macros/storefront/variant-selector.html`
|
||||
|
||||
Product variant selection (size, color, etc.).
|
||||
|
||||
@@ -255,7 +255,7 @@ Product variant selection (size, color, etc.).
|
||||
---
|
||||
|
||||
#### 3.3 Product Info Block
|
||||
**File:** `app/templates/shared/macros/shop/product-info.html`
|
||||
**File:** `app/templates/shared/macros/storefront/product-info.html`
|
||||
|
||||
Product details section.
|
||||
|
||||
@@ -280,7 +280,7 @@ Product details section.
|
||||
---
|
||||
|
||||
#### 3.4 Product Tabs
|
||||
**File:** `app/templates/shared/macros/shop/product-tabs.html`
|
||||
**File:** `app/templates/shared/macros/storefront/product-tabs.html`
|
||||
|
||||
Tabbed product information.
|
||||
|
||||
@@ -303,7 +303,7 @@ Tabbed product information.
|
||||
### Priority 4: Navigation & Discovery
|
||||
|
||||
#### 4.1 Category Navigation
|
||||
**File:** `app/templates/shared/macros/shop/category-nav.html`
|
||||
**File:** `app/templates/shared/macros/storefront/category-nav.html`
|
||||
|
||||
Category browsing sidebar/menu.
|
||||
|
||||
@@ -326,12 +326,12 @@ Category browsing sidebar/menu.
|
||||
---
|
||||
|
||||
#### 4.2 Breadcrumbs
|
||||
**File:** `app/templates/shared/macros/shop/breadcrumbs.html`
|
||||
**File:** `app/templates/shared/macros/storefront/breadcrumbs.html`
|
||||
|
||||
Navigation breadcrumb trail.
|
||||
|
||||
```jinja
|
||||
{{ shop_breadcrumbs(
|
||||
{{ storefront_breadcrumbs(
|
||||
items=[
|
||||
{'label': 'Home', 'url': '/'},
|
||||
{'label': 'Electronics', 'url': '/category/electronics'},
|
||||
@@ -343,7 +343,7 @@ Navigation breadcrumb trail.
|
||||
---
|
||||
|
||||
#### 4.3 Search Bar
|
||||
**File:** `app/templates/shared/macros/shop/search-bar.html`
|
||||
**File:** `app/templates/shared/macros/storefront/search-bar.html`
|
||||
|
||||
Product search with autocomplete.
|
||||
|
||||
@@ -365,7 +365,7 @@ Product search with autocomplete.
|
||||
---
|
||||
|
||||
#### 4.4 Filter Sidebar
|
||||
**File:** `app/templates/shared/macros/shop/filter-sidebar.html`
|
||||
**File:** `app/templates/shared/macros/storefront/filter-sidebar.html`
|
||||
|
||||
Product filtering panel.
|
||||
|
||||
@@ -390,7 +390,7 @@ Product filtering panel.
|
||||
### Priority 5: Social Proof & Trust
|
||||
|
||||
#### 5.1 Star Rating
|
||||
**File:** `app/templates/shared/macros/shop/star-rating.html`
|
||||
**File:** `app/templates/shared/macros/storefront/star-rating.html`
|
||||
|
||||
Reusable star rating display.
|
||||
|
||||
@@ -407,7 +407,7 @@ Reusable star rating display.
|
||||
---
|
||||
|
||||
#### 5.2 Review Card
|
||||
**File:** `app/templates/shared/macros/shop/review-card.html`
|
||||
**File:** `app/templates/shared/macros/storefront/review-card.html`
|
||||
|
||||
Individual product review.
|
||||
|
||||
@@ -431,7 +431,7 @@ Individual product review.
|
||||
---
|
||||
|
||||
#### 5.3 Trust Badges
|
||||
**File:** `app/templates/shared/macros/shop/trust-badges.html`
|
||||
**File:** `app/templates/shared/macros/storefront/trust-badges.html`
|
||||
|
||||
Trust and security indicators.
|
||||
|
||||
@@ -454,7 +454,7 @@ Trust and security indicators.
|
||||
### Priority 6: Promotional Components
|
||||
|
||||
#### 6.1 Sale Banner
|
||||
**File:** `app/templates/shared/macros/shop/sale-banner.html`
|
||||
**File:** `app/templates/shared/macros/storefront/sale-banner.html`
|
||||
|
||||
Promotional banner with countdown.
|
||||
|
||||
@@ -472,7 +472,7 @@ Promotional banner with countdown.
|
||||
---
|
||||
|
||||
#### 6.2 Product Badge
|
||||
**File:** `app/templates/shared/macros/shop/product-badge.html`
|
||||
**File:** `app/templates/shared/macros/storefront/product-badge.html`
|
||||
|
||||
Product overlay badges.
|
||||
|
||||
@@ -486,7 +486,7 @@ Product overlay badges.
|
||||
---
|
||||
|
||||
#### 6.3 Recently Viewed
|
||||
**File:** `app/templates/shared/macros/shop/recently-viewed.html`
|
||||
**File:** `app/templates/shared/macros/storefront/recently-viewed.html`
|
||||
|
||||
Recently viewed products carousel.
|
||||
|
||||
@@ -578,7 +578,7 @@ All shop components will use these CSS variables set by the store theme:
|
||||
## File Structure
|
||||
|
||||
```
|
||||
app/templates/shared/macros/shop/
|
||||
app/templates/shared/macros/storefront/
|
||||
├── product-card.html
|
||||
├── product-grid.html
|
||||
├── add-to-cart.html
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
# Shop Navigation Flow
|
||||
# Storefront Navigation Flow
|
||||
|
||||
Complete guide to navigation structure and URL hierarchy with landing pages.
|
||||
|
||||
## URL Hierarchy
|
||||
|
||||
```
|
||||
/ (Store Root) → Landing Page (if exists) OR redirect to /shop/
|
||||
├── /shop/ → E-commerce Homepage (Product Catalog)
|
||||
│ ├── /shop/products → Product Catalog (same as /shop/)
|
||||
│ ├── /shop/products/{id} → Product Detail Page
|
||||
│ ├── /shop/cart → Shopping Cart
|
||||
│ ├── /shop/checkout → Checkout Process
|
||||
│ ├── /shop/account/login → Customer Login
|
||||
│ ├── /shop/account/register → Customer Registration
|
||||
│ ├── /shop/account/dashboard → Customer Dashboard (auth required)
|
||||
│ ├── /shop/about → CMS Content Page
|
||||
│ ├── /shop/contact → CMS Content Page
|
||||
│ └── /shop/{slug} → Other CMS Pages
|
||||
/ (Store Root) → Landing Page (if exists) OR redirect to /storefront/
|
||||
├── /storefront/ → E-commerce Homepage (Product Catalog)
|
||||
│ ├── /storefront/products → Product Catalog (same as /storefront/)
|
||||
│ ├── /storefront/products/{id} → Product Detail Page
|
||||
│ ├── /storefront/cart → Shopping Cart
|
||||
│ ├── /storefront/checkout → Checkout Process
|
||||
│ ├── /storefront/account/login → Customer Login
|
||||
│ ├── /storefront/account/register → Customer Registration
|
||||
│ ├── /storefront/account/dashboard → Customer Dashboard (auth required)
|
||||
│ ├── /storefront/about → CMS Content Page
|
||||
│ ├── /storefront/contact → CMS Content Page
|
||||
│ └── /storefront/{slug} → Other CMS Pages
|
||||
```
|
||||
|
||||
## Navigation Patterns
|
||||
@@ -26,13 +26,13 @@ Complete guide to navigation structure and URL hierarchy with landing pages.
|
||||
**URL Structure:**
|
||||
```
|
||||
customdomain.com/ → Landing Page (marketing/brand)
|
||||
customdomain.com/shop/ → E-commerce Shop
|
||||
customdomain.com/shop/products → Product Catalog
|
||||
customdomain.com/storefront/ → E-commerce Storefront
|
||||
customdomain.com/storefront/products → Product Catalog
|
||||
```
|
||||
|
||||
**Navigation Flow:**
|
||||
1. User visits store domain → **Landing Page**
|
||||
2. Clicks "Shop Now" → **/shop/** (product catalog)
|
||||
2. Clicks "Shop Now" → **/storefront/** (product catalog)
|
||||
3. Clicks "Home" in breadcrumb → **/** (back to landing page)
|
||||
4. Clicks logo → **/** (back to landing page)
|
||||
|
||||
@@ -47,46 +47,46 @@ Home > Products
|
||||
|
||||
**URL Structure:**
|
||||
```
|
||||
customdomain.com/ → Redirects to /shop/
|
||||
customdomain.com/shop/ → E-commerce Shop
|
||||
customdomain.com/shop/products → Product Catalog
|
||||
customdomain.com/ → Redirects to /storefront/
|
||||
customdomain.com/storefront/ → E-commerce Storefront
|
||||
customdomain.com/storefront/products → Product Catalog
|
||||
```
|
||||
|
||||
**Navigation Flow:**
|
||||
1. User visits store domain → **Redirects to /shop/**
|
||||
2. User browses shop
|
||||
3. Clicks "Home" in breadcrumb → **/** (redirects to /shop/)
|
||||
4. Clicks logo → **/** (redirects to /shop/)
|
||||
1. User visits store domain → **Redirects to /storefront/**
|
||||
2. User browses storefront
|
||||
3. Clicks "Home" in breadcrumb → **/** (redirects to /storefront/)
|
||||
4. Clicks logo → **/** (redirects to /storefront/)
|
||||
|
||||
**Breadcrumb Example (on Products page):**
|
||||
```
|
||||
Home > Products
|
||||
↓
|
||||
/ → /shop/ (redirects)
|
||||
/ → /storefront/ (redirects)
|
||||
```
|
||||
|
||||
## Link References
|
||||
|
||||
### Base URL Calculation
|
||||
|
||||
The `base_url` variable is calculated in `shop_pages.py:get_shop_context()`:
|
||||
The `base_url` variable is calculated in `storefront_pages.py:get_storefront_context()`:
|
||||
|
||||
```python
|
||||
# For domain/subdomain access
|
||||
base_url = "/"
|
||||
# Result: /shop/products, /shop/cart, etc.
|
||||
# Result: /storefront/products, /storefront/cart, etc.
|
||||
|
||||
# For path-based access
|
||||
base_url = "/stores/orion/"
|
||||
# Result: /stores/orion/shop/products, /stores/orion/shop/cart, etc.
|
||||
base_url = "/storefront/orion/"
|
||||
# Result: /storefront/orion/products, /storefront/orion/cart, etc.
|
||||
```
|
||||
|
||||
### Template Links
|
||||
|
||||
**Logo / Home Link (Header):**
|
||||
```jinja2
|
||||
{# Points to store root (landing page or shop) #}
|
||||
<a href="{{ base_url }}shop/">{{ store.name }}</a>
|
||||
{# Points to store root (landing page or storefront) #}
|
||||
<a href="{{ base_url }}storefront/">{{ store.name }}</a>
|
||||
```
|
||||
|
||||
**Breadcrumb Home Link:**
|
||||
@@ -95,20 +95,20 @@ base_url = "/stores/orion/"
|
||||
<a href="{{ base_url }}">Home</a>
|
||||
```
|
||||
|
||||
**Shop Links:**
|
||||
**Storefront Links:**
|
||||
```jinja2
|
||||
{# All shop pages include /shop/ prefix #}
|
||||
<a href="{{ base_url }}shop/products">Products</a>
|
||||
<a href="{{ base_url }}shop/cart">Cart</a>
|
||||
<a href="{{ base_url }}shop/checkout">Checkout</a>
|
||||
{# All storefront pages include /storefront/ prefix #}
|
||||
<a href="{{ base_url }}storefront/products">Products</a>
|
||||
<a href="{{ base_url }}storefront/cart">Cart</a>
|
||||
<a href="{{ base_url }}storefront/checkout">Checkout</a>
|
||||
```
|
||||
|
||||
**CMS Page Links:**
|
||||
```jinja2
|
||||
{# CMS pages are under /shop/ #}
|
||||
<a href="{{ base_url }}shop/about">About</a>
|
||||
<a href="{{ base_url }}shop/contact">Contact</a>
|
||||
<a href="{{ base_url }}shop/{{ page.slug }}">{{ page.title }}</a>
|
||||
{# CMS pages are under /storefront/ #}
|
||||
<a href="{{ base_url }}storefront/about">About</a>
|
||||
<a href="{{ base_url }}storefront/contact">Contact</a>
|
||||
<a href="{{ base_url }}storefront/{{ page.slug }}">{{ page.title }}</a>
|
||||
```
|
||||
|
||||
## Complete URL Examples
|
||||
@@ -116,46 +116,46 @@ base_url = "/stores/orion/"
|
||||
### Path-Based Access (Development)
|
||||
|
||||
```
|
||||
http://localhost:8000/stores/orion/
|
||||
├── / (root) → Landing Page OR redirect to shop
|
||||
├── /shop/ → Shop Homepage
|
||||
├── /shop/products → Product Catalog
|
||||
├── /shop/products/4 → Product Detail
|
||||
├── /shop/cart → Shopping Cart
|
||||
├── /shop/checkout → Checkout
|
||||
├── /shop/account/login → Customer Login
|
||||
├── /shop/about → About Page (CMS)
|
||||
└── /shop/contact → Contact Page (CMS)
|
||||
http://localhost:8000/storefront/orion/
|
||||
├── / (root) → Landing Page OR redirect to storefront
|
||||
├── /storefront/ → Storefront Homepage
|
||||
├── /storefront/products → Product Catalog
|
||||
├── /storefront/products/4 → Product Detail
|
||||
├── /storefront/cart → Shopping Cart
|
||||
├── /storefront/checkout → Checkout
|
||||
├── /storefront/account/login → Customer Login
|
||||
├── /storefront/about → About Page (CMS)
|
||||
└── /storefront/contact → Contact Page (CMS)
|
||||
```
|
||||
|
||||
### Subdomain Access (Production)
|
||||
|
||||
```
|
||||
https://orion.platform.com/
|
||||
├── / (root) → Landing Page OR redirect to shop
|
||||
├── /shop/ → Shop Homepage
|
||||
├── /shop/products → Product Catalog
|
||||
├── /shop/products/4 → Product Detail
|
||||
├── /shop/cart → Shopping Cart
|
||||
├── /shop/checkout → Checkout
|
||||
├── /shop/account/login → Customer Login
|
||||
├── /shop/about → About Page (CMS)
|
||||
└── /shop/contact → Contact Page (CMS)
|
||||
├── / (root) → Landing Page OR redirect to storefront
|
||||
├── /storefront/ → Storefront Homepage
|
||||
├── /storefront/products → Product Catalog
|
||||
├── /storefront/products/4 → Product Detail
|
||||
├── /storefront/cart → Shopping Cart
|
||||
├── /storefront/checkout → Checkout
|
||||
├── /storefront/account/login → Customer Login
|
||||
├── /storefront/about → About Page (CMS)
|
||||
└── /storefront/contact → Contact Page (CMS)
|
||||
```
|
||||
|
||||
### Custom Domain Access (Production)
|
||||
|
||||
```
|
||||
https://customdomain.com/
|
||||
├── / (root) → Landing Page OR redirect to shop
|
||||
├── /shop/ → Shop Homepage
|
||||
├── /shop/products → Product Catalog
|
||||
├── /shop/products/4 → Product Detail
|
||||
├── /shop/cart → Shopping Cart
|
||||
├── /shop/checkout → Checkout
|
||||
├── /shop/account/login → Customer Login
|
||||
├── /shop/about → About Page (CMS)
|
||||
└── /shop/contact → Contact Page (CMS)
|
||||
├── / (root) → Landing Page OR redirect to storefront
|
||||
├── /storefront/ → Storefront Homepage
|
||||
├── /storefront/products → Product Catalog
|
||||
├── /storefront/products/4 → Product Detail
|
||||
├── /storefront/cart → Shopping Cart
|
||||
├── /storefront/checkout → Checkout
|
||||
├── /storefront/account/login → Customer Login
|
||||
├── /storefront/about → About Page (CMS)
|
||||
└── /storefront/contact → Contact Page (CMS)
|
||||
```
|
||||
|
||||
## User Journeys
|
||||
@@ -164,26 +164,26 @@ https://customdomain.com/
|
||||
|
||||
1. Visit `customdomain.com/` → **Landing Page**
|
||||
- Sees brand story, features, CTA
|
||||
2. Clicks "Shop Now" → `/shop/` → **Product Catalog**
|
||||
2. Clicks "Shop Now" → `/storefront/` → **Product Catalog**
|
||||
- Browses products
|
||||
3. Clicks product → `/shop/products/4` → **Product Detail**
|
||||
3. Clicks product → `/storefront/products/4` → **Product Detail**
|
||||
- Views product
|
||||
4. Clicks "Home" in breadcrumb → `/` → **Back to Landing Page**
|
||||
5. Clicks logo → `/` → **Back to Landing Page**
|
||||
|
||||
### Journey 2: Returning Customer (Direct to Shop)
|
||||
### Journey 2: Returning Customer (Direct to Storefront)
|
||||
|
||||
1. Visit `customdomain.com/shop/` → **Product Catalog**
|
||||
- Already knows the brand, goes straight to shop
|
||||
2. Adds to cart → `/shop/cart` → **Shopping Cart**
|
||||
3. Checkout → `/shop/checkout` → **Checkout**
|
||||
1. Visit `customdomain.com/storefront/` → **Product Catalog**
|
||||
- Already knows the brand, goes straight to storefront
|
||||
2. Adds to cart → `/storefront/cart` → **Shopping Cart**
|
||||
3. Checkout → `/storefront/checkout` → **Checkout**
|
||||
4. Clicks "Home" → `/` → **Landing Page** (brand homepage)
|
||||
|
||||
### Journey 3: Customer Account Management
|
||||
|
||||
1. Visit `customdomain.com/shop/account/login` → **Login**
|
||||
2. After login → `/shop/account/dashboard` → **Dashboard**
|
||||
3. View orders → `/shop/account/orders` → **Order History**
|
||||
1. Visit `customdomain.com/storefront/account/login` → **Login**
|
||||
2. After login → `/storefront/account/dashboard` → **Dashboard**
|
||||
3. View orders → `/storefront/account/orders` → **Order History**
|
||||
4. Clicks logo → `/` → **Back to Landing Page**
|
||||
|
||||
## Navigation Components
|
||||
@@ -192,32 +192,32 @@ https://customdomain.com/
|
||||
|
||||
```jinja2
|
||||
{# Logo - always points to store root #}
|
||||
<a href="{{ base_url }}shop/">
|
||||
<a href="{{ base_url }}storefront/">
|
||||
<img src="{{ theme.branding.logo }}" alt="{{ store.name }}">
|
||||
</a>
|
||||
|
||||
{# Main Navigation #}
|
||||
<nav>
|
||||
<a href="{{ base_url }}shop/">Home</a>
|
||||
<a href="{{ base_url }}shop/products">Products</a>
|
||||
<a href="{{ base_url }}shop/about">About</a>
|
||||
<a href="{{ base_url }}shop/contact">Contact</a>
|
||||
<a href="{{ base_url }}storefront/">Home</a>
|
||||
<a href="{{ base_url }}storefront/products">Products</a>
|
||||
<a href="{{ base_url }}storefront/about">About</a>
|
||||
<a href="{{ base_url }}storefront/contact">Contact</a>
|
||||
</nav>
|
||||
|
||||
{# Actions #}
|
||||
<a href="{{ base_url }}shop/cart">Cart</a>
|
||||
<a href="{{ base_url }}shop/account">Account</a>
|
||||
<a href="{{ base_url }}storefront/cart">Cart</a>
|
||||
<a href="{{ base_url }}storefront/account">Account</a>
|
||||
```
|
||||
|
||||
### Footer Navigation (base.html)
|
||||
|
||||
```jinja2
|
||||
{# Quick Links #}
|
||||
<a href="{{ base_url }}shop/products">Products</a>
|
||||
<a href="{{ base_url }}storefront/products">Products</a>
|
||||
|
||||
{# CMS Pages (dynamic) #}
|
||||
{% for page in footer_pages %}
|
||||
<a href="{{ base_url }}shop/{{ page.slug }}">{{ page.title }}</a>
|
||||
<a href="{{ base_url }}storefront/{{ page.slug }}">{{ page.title }}</a>
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
@@ -233,39 +233,39 @@ https://customdomain.com/
|
||||
### ✅ DO:
|
||||
|
||||
1. **Use Landing Pages**: Create engaging landing pages at store root
|
||||
2. **Clear Navigation**: Make it easy to get from landing to shop and back
|
||||
2. **Clear Navigation**: Make it easy to get from landing to storefront and back
|
||||
3. **Consistent "Home"**: Logo and "Home" breadcrumb both point to `/` (landing)
|
||||
4. **Shop Links**: All shop-related links include `/shop/` prefix
|
||||
5. **CMS Under Shop**: Keep CMS pages under `/shop/` for consistency
|
||||
4. **Storefront Links**: All storefront-related links include `/storefront/` prefix
|
||||
5. **CMS Under Storefront**: Keep CMS pages under `/storefront/` for consistency
|
||||
|
||||
### ❌ DON'T:
|
||||
|
||||
1. **Hardcode URLs**: Always use `{{ base_url }}` for store-aware links
|
||||
2. **Skip /shop/**: Don't link directly to `/products`, use `/shop/products`
|
||||
3. **Mix Landing & Shop**: Keep landing page separate from shop catalog
|
||||
2. **Skip /storefront/**: Don't link directly to `/products`, use `/storefront/products`
|
||||
3. **Mix Landing & Storefront**: Keep landing page separate from storefront catalog
|
||||
4. **Forget Breadcrumbs**: Always provide "Home" link to go back
|
||||
|
||||
## Migration Notes
|
||||
|
||||
### Before Landing Pages
|
||||
|
||||
All links pointed to `/shop/`:
|
||||
All links pointed to `/storefront/`:
|
||||
```jinja2
|
||||
<a href="{{ base_url }}shop/">Home</a> {# Logo #}
|
||||
<a href="{{ base_url }}shop/">Home</a> {# Breadcrumb #}
|
||||
<a href="{{ base_url }}storefront/">Home</a> {# Logo #}
|
||||
<a href="{{ base_url }}storefront/">Home</a> {# Breadcrumb #}
|
||||
```
|
||||
|
||||
### After Landing Pages
|
||||
|
||||
Separation of concerns:
|
||||
```jinja2
|
||||
<a href="{{ base_url }}shop/">Home</a> {# Logo - still goes to shop #}
|
||||
<a href="{{ base_url }}storefront/">Home</a> {# Logo - still goes to storefront #}
|
||||
<a href="{{ base_url }}">Home</a> {# Breadcrumb - goes to landing #}
|
||||
```
|
||||
|
||||
This allows:
|
||||
- Landing page at `/` for marketing/branding
|
||||
- Shop catalog at `/shop/` for e-commerce
|
||||
- Storefront catalog at `/storefront/` for e-commerce
|
||||
- Clean navigation between the two
|
||||
|
||||
## Technical Implementation
|
||||
@@ -273,27 +273,27 @@ This allows:
|
||||
### Route Handlers (main.py)
|
||||
|
||||
```python
|
||||
# Store root - serves landing page or redirects to shop
|
||||
# Store root - serves landing page or redirects to storefront
|
||||
@app.get("/")
|
||||
@app.get("/stores/{store_code}/")
|
||||
@app.get("/storefront/{store_code}/")
|
||||
async def root(request: Request):
|
||||
if has_landing_page():
|
||||
return render_landing_page()
|
||||
else:
|
||||
return redirect_to_shop()
|
||||
return redirect_to_storefront()
|
||||
|
||||
# Shop routes
|
||||
@app.include_router(shop_pages.router, prefix="/shop")
|
||||
@app.include_router(shop_pages.router, prefix="/stores/{store_code}/shop")
|
||||
# Storefront routes
|
||||
@app.include_router(storefront_pages.router, prefix="/storefront")
|
||||
@app.include_router(storefront_pages.router, prefix="/storefront/{store_code}")
|
||||
```
|
||||
|
||||
### Context Calculation (shop_pages.py)
|
||||
### Context Calculation (storefront_pages.py)
|
||||
|
||||
```python
|
||||
def get_shop_context(request: Request):
|
||||
def get_storefront_context(request: Request):
|
||||
base_url = "/"
|
||||
if access_method == "path":
|
||||
base_url = f"/stores/{store.subdomain}/"
|
||||
base_url = f"/storefront/{store.subdomain}/"
|
||||
|
||||
return {
|
||||
"base_url": base_url,
|
||||
@@ -308,11 +308,11 @@ def get_shop_context(request: Request):
|
||||
The navigation system creates a **two-tier structure**:
|
||||
|
||||
1. **Landing Page** (`/`) - Marketing, branding, store story
|
||||
2. **Shop** (`/shop/`) - E-commerce, products, cart, checkout
|
||||
2. **Storefront** (`/storefront/`) - E-commerce, products, cart, checkout
|
||||
|
||||
This gives stores flexibility to:
|
||||
- Have a marketing homepage separate from their store
|
||||
- Choose different landing page designs (minimal, modern, full)
|
||||
- Or skip the landing page and go straight to the shop
|
||||
- Or skip the landing page and go straight to the storefront
|
||||
|
||||
All while maintaining clean, consistent navigation throughout the experience.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Shop Frontend - Alpine.js/Jinja2 Page Template Guide
|
||||
# Storefront Frontend - Alpine.js/Jinja2 Page Template Guide
|
||||
|
||||
## 📋 Overview
|
||||
|
||||
This guide provides complete templates for creating new customer-facing shop pages using the established Alpine.js + Jinja2 + Multi-Theme architecture. Follow these patterns to ensure consistency across all store shops while maintaining unique branding.
|
||||
This guide provides complete templates for creating new customer-facing storefront pages using the established Alpine.js + Jinja2 + Multi-Theme architecture. Follow these patterns to ensure consistency across all store storefronts while maintaining unique branding.
|
||||
|
||||
---
|
||||
|
||||
@@ -10,9 +10,9 @@ This guide provides complete templates for creating new customer-facing shop pag
|
||||
|
||||
Three fully-implemented authentication pages are available for reference:
|
||||
|
||||
- **Login** (`app/templates/shop/account/login.html`) - Customer sign-in with email/password
|
||||
- **Register** (`app/templates/shop/account/register.html`) - New customer account creation
|
||||
- **Forgot Password** (`app/templates/shop/account/forgot-password.html`) - Password reset flow
|
||||
- **Login** (`app/templates/storefront/account/login.html`) - Customer sign-in with email/password
|
||||
- **Register** (`app/templates/storefront/account/register.html`) - New customer account creation
|
||||
- **Forgot Password** (`app/templates/storefront/account/forgot-password.html`) - Password reset flow
|
||||
|
||||
All authentication pages feature:
|
||||
- ✅ Tailwind CSS styling
|
||||
@@ -24,7 +24,7 @@ All authentication pages feature:
|
||||
- ✅ Loading states
|
||||
- ✅ Error handling
|
||||
|
||||
See the [Shop Architecture Documentation](./architecture.md) (Authentication Pages section) for complete details.
|
||||
See the [Storefront Architecture Documentation](./architecture.md) (Authentication Pages section) for complete details.
|
||||
|
||||
---
|
||||
|
||||
@@ -33,16 +33,16 @@ See the [Shop Architecture Documentation](./architecture.md) (Authentication Pag
|
||||
### File Structure for New Page
|
||||
```
|
||||
app/
|
||||
├── templates/shop/
|
||||
├── templates/storefront/
|
||||
│ └── [page-name].html # Jinja2 template
|
||||
├── static/shop/js/
|
||||
├── static/storefront/js/
|
||||
│ └── [page-name].js # Alpine.js component
|
||||
└── api/v1/shop/
|
||||
└── api/v1/storefront/
|
||||
└── pages.py # Route registration
|
||||
```
|
||||
|
||||
### Checklist for New Page
|
||||
- [ ] Create Jinja2 template extending shop/base.html
|
||||
- [ ] Create Jinja2 template extending storefront/base.html
|
||||
- [ ] Create Alpine.js JavaScript component
|
||||
- [ ] Register route in pages.py
|
||||
- [ ] Test with multiple store themes
|
||||
@@ -58,11 +58,11 @@ app/
|
||||
|
||||
### 1. Jinja2 Template
|
||||
|
||||
**File:** `app/templates/shop/[page-name].html`
|
||||
**File:** `app/templates/storefront/[page-name].html`
|
||||
|
||||
```jinja2
|
||||
{# app/templates/shop/[page-name].html #}
|
||||
{% extends "shop/base.html" %}
|
||||
{# app/templates/storefront/[page-name].html #}
|
||||
{% extends "storefront/base.html" %}
|
||||
|
||||
{# Page title for browser tab - includes store name #}
|
||||
{% block title %}[Page Name] - {{ store.name }}{% endblock %}
|
||||
@@ -174,7 +174,7 @@ app/
|
||||
|
||||
<!-- Item Image -->
|
||||
<div class="aspect-w-1 aspect-h-1 w-full overflow-hidden rounded-t-lg bg-gray-100 dark:bg-gray-700">
|
||||
<img :src="item.image || '/static/shop/img/placeholder-product.png'"
|
||||
<img :src="item.image || '/static/storefront/img/placeholder-product.png'"
|
||||
:alt="item.name"
|
||||
class="w-full h-full object-cover object-center hover:scale-105 transition-transform"
|
||||
loading="lazy">
|
||||
@@ -247,7 +247,7 @@ app/
|
||||
|
||||
{# Page-specific JavaScript #}
|
||||
{% block extra_scripts %}
|
||||
<script src="{{ url_for('static', path='shop/js/[page-name].js') }}"></script>
|
||||
<script src="{{ url_for('static', path='storefront/js/[page-name].js') }}"></script>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
@@ -255,10 +255,10 @@ app/
|
||||
|
||||
### 2. Alpine.js Component
|
||||
|
||||
**File:** `app/static/shop/js/[page-name].js`
|
||||
**File:** `app/static/storefront/js/[page-name].js`
|
||||
|
||||
```javascript
|
||||
// static/shop/js/[page-name].js
|
||||
// static/storefront/js/[page-name].js
|
||||
/**
|
||||
* [Page Name] Component
|
||||
* Handles [describe functionality]
|
||||
@@ -333,7 +333,7 @@ function shop[PageName]() {
|
||||
});
|
||||
|
||||
const response = await fetch(
|
||||
`/api/v1/shop/${this.storeCode}/items?${params}`
|
||||
`/api/v1/storefront/${this.storeCode}/items?${params}`
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -444,14 +444,14 @@ function shop[PageName]() {
|
||||
addToCart(item, quantity = 1) {
|
||||
pageLog.info('Adding to cart:', item.name);
|
||||
|
||||
// Get cart from shop layout
|
||||
// Get cart from storefront layout
|
||||
const shopLayout = Alpine.store('shop') || window.storefrontLayoutData();
|
||||
|
||||
if (shopLayout && typeof shopLayout.addToCart === 'function') {
|
||||
shopLayout.addToCart(item, quantity);
|
||||
this.showToast(`${item.name} added to cart`, 'success');
|
||||
} else {
|
||||
pageLog.error('Shop layout not available');
|
||||
pageLog.error('Storefront layout not available');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -513,7 +513,7 @@ pageLog.info('[PageName] module loaded');
|
||||
|
||||
### 3. Route Registration
|
||||
|
||||
**File:** `app/api/v1/shop/pages.py`
|
||||
**File:** `app/api/v1/storefront/pages.py`
|
||||
|
||||
```python
|
||||
from fastapi import APIRouter, Request, Depends
|
||||
@@ -536,7 +536,7 @@ async def [page_name]_page(
|
||||
theme = request.state.theme
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"shop/[page-name].html",
|
||||
"storefront/[page-name].html",
|
||||
{
|
||||
"request": request,
|
||||
"store": store,
|
||||
@@ -562,7 +562,7 @@ async loadProducts() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/v1/shop/${this.storeCode}/products?category=${this.category}`
|
||||
`/api/v1/storefront/${this.storeCode}/products?category=${this.category}`
|
||||
);
|
||||
const data = await response.json();
|
||||
this.products = data.products || [];
|
||||
@@ -578,7 +578,7 @@ async loadProducts() {
|
||||
```html
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<template x-for="product in products" :key="product.id">
|
||||
{% include 'shop/partials/product-card.html' %}
|
||||
{% include 'storefront/partials/product-card.html' %}
|
||||
</template>
|
||||
</div>
|
||||
```
|
||||
@@ -598,7 +598,7 @@ async init() {
|
||||
|
||||
async loadProduct(id) {
|
||||
const product = await fetch(
|
||||
`/api/v1/shop/${this.storeCode}/products/${id}`
|
||||
`/api/v1/storefront/${this.storeCode}/products/${id}`
|
||||
).then(r => r.json());
|
||||
|
||||
this.product = product;
|
||||
@@ -713,7 +713,7 @@ async performSearch() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/v1/shop/${this.storeCode}/search`,
|
||||
`/api/v1/storefront/${this.storeCode}/search`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -750,10 +750,10 @@ Always use CSS variables for store colors:
|
||||
|
||||
### 2. Cart Integration
|
||||
|
||||
Always use the shop layout's cart methods:
|
||||
Always use the storefront layout's cart methods:
|
||||
|
||||
```javascript
|
||||
// ✅ GOOD: Uses shop layout
|
||||
// ✅ GOOD: Uses storefront layout
|
||||
const shopLayout = window.storefrontLayoutData();
|
||||
shopLayout.addToCart(product, quantity);
|
||||
|
||||
@@ -881,7 +881,7 @@ Add proper ARIA labels and keyboard navigation:
|
||||
|
||||
### Reusable Partials
|
||||
|
||||
Create reusable components in `templates/shop/partials/`:
|
||||
Create reusable components in `templates/storefront/partials/`:
|
||||
|
||||
**product-card.html:**
|
||||
```html
|
||||
@@ -945,12 +945,12 @@ Create reusable components in `templates/shop/partials/`:
|
||||
|
||||
```bash
|
||||
# Create new page files
|
||||
touch app/templates/shop/new-page.html
|
||||
touch app/static/shop/js/new-page.js
|
||||
touch app/templates/storefront/new-page.html
|
||||
touch app/static/storefront/js/new-page.js
|
||||
|
||||
# Copy templates
|
||||
cp template.html app/templates/shop/new-page.html
|
||||
cp template.js app/static/shop/js/new-page.js
|
||||
cp template.html app/templates/storefront/new-page.html
|
||||
cp template.js app/static/storefront/js/new-page.js
|
||||
|
||||
# Update placeholders:
|
||||
# - Replace [page-name] with actual name
|
||||
@@ -969,7 +969,7 @@ cp template.js app/static/shop/js/new-page.js
|
||||
- **Logo**: Available in both light and dark versions
|
||||
- **Custom CSS**: Store-specific styles automatically injected
|
||||
|
||||
### Shop Layout Functions
|
||||
### Storefront Layout Functions
|
||||
- `addToCart(product, quantity)`: Add item to cart
|
||||
- `showToast(message, type)`: Show notification
|
||||
- `formatPrice(amount)`: Format as currency
|
||||
@@ -991,4 +991,4 @@ await apiClient.post('/endpoint', { data });
|
||||
|
||||
---
|
||||
|
||||
This template provides a complete, theme-aware pattern for building shop pages with consistent structure, store branding, cart integration, and excellent user experience across all devices.
|
||||
This template provides a complete, theme-aware pattern for building storefront pages with consistent structure, store branding, cart integration, and excellent user experience across all devices.
|
||||
|
||||
@@ -21,7 +21,7 @@ Tailwind Standalone CLI (single binary, no npm)
|
||||
│
|
||||
├── static/admin/css/tailwind.css → tailwind.output.css (Admin)
|
||||
├── static/store/css/tailwind.css → tailwind.output.css (Store)
|
||||
├── static/shop/css/tailwind.css → tailwind.output.css (Shop)
|
||||
├── static/storefront/css/tailwind.css → tailwind.output.css (Shop)
|
||||
└── static/public/css/tailwind.css → tailwind.output.css (Platform)
|
||||
```
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ POST /api/v1/store/{store_code}/content-pages
|
||||
|
||||
**Public - Retrieve Page:**
|
||||
```bash
|
||||
GET /api/v1/shop/content-pages/{slug}
|
||||
GET /api/v1/storefront/content-pages/{slug}
|
||||
```
|
||||
|
||||
## How the Two-Tier System Works
|
||||
|
||||
@@ -292,7 +292,7 @@ python scripts/create_landing_page.py
|
||||
make dev
|
||||
|
||||
# 6. Test the shop
|
||||
# Visit: http://localhost:8000/stores/orion/shop/
|
||||
# Visit: http://localhost:8000/storefront/orion
|
||||
```
|
||||
|
||||
### Common Issues
|
||||
@@ -307,7 +307,7 @@ python scripts/create_inventory.py
|
||||
```bash
|
||||
# Solution: Create landing page for the store
|
||||
python scripts/create_landing_page.py
|
||||
# OR the store will auto-redirect to /shop/
|
||||
# OR the store will auto-redirect to /storefront/
|
||||
```
|
||||
|
||||
**Problem: No products showing in shop**
|
||||
|
||||
@@ -145,9 +145,9 @@ The attachment size limit is configurable via platform settings:
|
||||
- **Default:** 10
|
||||
- **Category:** messaging
|
||||
|
||||
## Shop (Customer) Interface
|
||||
## Storefront (Customer) Interface
|
||||
|
||||
### API Endpoints (`/api/v1/shop/messages`)
|
||||
### API Endpoints (`/api/v1/storefront/messages`)
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
@@ -160,8 +160,8 @@ The attachment size limit is configurable via platform settings:
|
||||
|
||||
### Frontend
|
||||
|
||||
- **Template:** `app/templates/shop/account/messages.html`
|
||||
- **Page Route:** `/shop/account/messages` and `/shop/account/messages/{conversation_id}`
|
||||
- **Template:** `app/templates/storefront/account/messages.html`
|
||||
- **Page Route:** `/storefront/account/messages` and `/storefront/account/messages/{conversation_id}`
|
||||
|
||||
Features:
|
||||
- Conversation list with unread badges
|
||||
@@ -236,7 +236,7 @@ Messages is available under "Platform Administration" section.
|
||||
### Store Sidebar
|
||||
Messages is available under "Sales" section.
|
||||
|
||||
### Shop Account Dashboard
|
||||
### Storefront Account Dashboard
|
||||
Messages card is available on the customer account dashboard with unread count badge.
|
||||
|
||||
### Header Badge
|
||||
|
||||
@@ -63,9 +63,9 @@ class PasswordResetToken(Base):
|
||||
|
||||
### API Endpoints
|
||||
|
||||
**File:** `app/api/v1/shop/auth.py`
|
||||
**File:** `app/api/v1/storefront/auth.py`
|
||||
|
||||
#### POST /api/v1/shop/auth/forgot-password
|
||||
#### POST /api/v1/storefront/auth/forgot-password
|
||||
|
||||
Request a password reset link.
|
||||
|
||||
@@ -97,7 +97,7 @@ def forgot_password(request: Request, email: str, db: Session = Depends(get_db))
|
||||
if customer:
|
||||
# Generate token and send email
|
||||
plaintext_token = PasswordResetToken.create_for_customer(db, customer.id)
|
||||
reset_link = f"{scheme}://{host}/shop/account/reset-password?token={plaintext_token}"
|
||||
reset_link = f"{scheme}://{host}/storefront/account/reset-password?token={plaintext_token}"
|
||||
|
||||
email_service.send_template(
|
||||
template_code="password_reset",
|
||||
@@ -119,7 +119,7 @@ def forgot_password(request: Request, email: str, db: Session = Depends(get_db))
|
||||
|
||||
---
|
||||
|
||||
#### POST /api/v1/shop/auth/reset-password
|
||||
#### POST /api/v1/storefront/auth/reset-password
|
||||
|
||||
Reset password using token from email.
|
||||
|
||||
@@ -279,8 +279,8 @@ class PasswordTooShortException(ValidationException):
|
||||
|
||||
#### Forgot Password Page
|
||||
|
||||
**Template:** `app/templates/shop/account/forgot-password.html`
|
||||
**Route:** `/shop/account/forgot-password`
|
||||
**Template:** `app/templates/storefront/account/forgot-password.html`
|
||||
**Route:** `/storefront/account/forgot-password`
|
||||
|
||||
Features:
|
||||
- Email input form
|
||||
@@ -290,8 +290,8 @@ Features:
|
||||
|
||||
#### Reset Password Page
|
||||
|
||||
**Template:** `app/templates/shop/account/reset-password.html`
|
||||
**Route:** `/shop/account/reset-password?token=...`
|
||||
**Template:** `app/templates/storefront/account/reset-password.html`
|
||||
**Route:** `/storefront/account/reset-password?token=...`
|
||||
|
||||
Features:
|
||||
- New password input
|
||||
@@ -379,15 +379,15 @@ If you didn't request this, you can safely ignore this email.
|
||||
├── alembic/versions/
|
||||
│ └── t8b9c0d1e2f3_add_password_reset_tokens.py
|
||||
├── app/
|
||||
│ ├── api/v1/shop/
|
||||
│ ├── api/v1/storefront/
|
||||
│ │ └── auth.py
|
||||
│ ├── exceptions/
|
||||
│ │ └── customer.py
|
||||
│ ├── routes/
|
||||
│ │ └── shop_pages.py
|
||||
│ │ └── storefront_pages.py
|
||||
│ ├── services/
|
||||
│ │ └── customer_service.py
|
||||
│ └── templates/shop/account/
|
||||
│ └── templates/storefront/account/
|
||||
│ ├── forgot-password.html
|
||||
│ └── reset-password.html
|
||||
├── models/
|
||||
|
||||
@@ -68,8 +68,8 @@ admin.platform.com → Admin Interface
|
||||
|
||||
**3. 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
|
||||
```
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Shop Frontend Features - Testing Checklist
|
||||
# Storefront Frontend Features - Testing Checklist
|
||||
|
||||
**Last Updated:** 2026-01-08
|
||||
**Total Pages:** 15+
|
||||
@@ -11,7 +11,7 @@
|
||||
### Homepage Display
|
||||
| Test Case | Route | Method | Status |
|
||||
|-----------|-------|--------|--------|
|
||||
| Load homepage | `/shop/` | GET | Working |
|
||||
| Load homepage | `/storefront/` | GET | Working |
|
||||
|
||||
**UI Elements to Test:**
|
||||
- [ ] Hero section with store branding
|
||||
@@ -30,10 +30,10 @@
|
||||
### Products List
|
||||
| Test Case | Route | Method | Status |
|
||||
|-----------|-------|--------|--------|
|
||||
| Browse all products | `/shop/products` | GET | Working |
|
||||
| API: List products | `/api/v1/shop/products` | GET | Working |
|
||||
| Filter featured products | `/api/v1/shop/products?is_featured=true` | GET | Working |
|
||||
| Pagination | `/api/v1/shop/products?skip=&limit=` | GET | Working |
|
||||
| Browse all products | `/storefront/products` | GET | Working |
|
||||
| API: List products | `/api/v1/storefront/products` | GET | Working |
|
||||
| Filter featured products | `/api/v1/storefront/products?is_featured=true` | GET | Working |
|
||||
| Pagination | `/api/v1/storefront/products?skip=&limit=` | GET | Working |
|
||||
|
||||
**UI Elements to Test:**
|
||||
- [ ] Product grid display (2-4 columns)
|
||||
@@ -58,8 +58,8 @@
|
||||
### Product Detail Page
|
||||
| Test Case | Route | Method | Status |
|
||||
|-----------|-------|--------|--------|
|
||||
| View product details | `/shop/products/{product_id}` | GET | Working |
|
||||
| API: Get product | `/api/v1/shop/products/{product_id}` | GET | Working |
|
||||
| View product details | `/storefront/products/{product_id}` | GET | Working |
|
||||
| API: Get product | `/api/v1/storefront/products/{product_id}` | GET | Working |
|
||||
|
||||
**UI Elements to Test:**
|
||||
- [ ] Main product image
|
||||
@@ -85,9 +85,9 @@
|
||||
### Product Search
|
||||
| Test Case | Route | Method | Status |
|
||||
|-----------|-------|--------|--------|
|
||||
| Search page | `/shop/search` | GET | Working |
|
||||
| Search with query | `/shop/search?q=` | GET | Working |
|
||||
| API: Search products | `/api/v1/shop/products/search?q=` | GET | Working |
|
||||
| Search page | `/storefront/search` | GET | Working |
|
||||
| Search with query | `/storefront/search?q=` | GET | Working |
|
||||
| API: Search products | `/api/v1/storefront/products/search?q=` | GET | Working |
|
||||
|
||||
**UI Elements to Test:**
|
||||
- [ ] Search input field
|
||||
@@ -118,12 +118,12 @@
|
||||
### Cart Management
|
||||
| Test Case | Route | Method | Status |
|
||||
|-----------|-------|--------|--------|
|
||||
| View cart page | `/shop/cart` | GET | Working |
|
||||
| API: Get cart | `/api/v1/shop/cart/{session_id}` | GET | Working |
|
||||
| API: Add to cart | `/api/v1/shop/cart/{session_id}/items` | POST | Working |
|
||||
| API: Update quantity | `/api/v1/shop/cart/{session_id}/items/{product_id}` | PUT | Working |
|
||||
| API: Remove item | `/api/v1/shop/cart/{session_id}/items/{product_id}` | DELETE | Working |
|
||||
| API: Clear cart | `/api/v1/shop/cart/{session_id}` | DELETE | Working |
|
||||
| View cart page | `/storefront/cart` | GET | Working |
|
||||
| API: Get cart | `/api/v1/storefront/cart/{session_id}` | GET | Working |
|
||||
| API: Add to cart | `/api/v1/storefront/cart/{session_id}/items` | POST | Working |
|
||||
| API: Update quantity | `/api/v1/storefront/cart/{session_id}/items/{product_id}` | PUT | Working |
|
||||
| API: Remove item | `/api/v1/storefront/cart/{session_id}/items/{product_id}` | DELETE | Working |
|
||||
| API: Clear cart | `/api/v1/storefront/cart/{session_id}` | DELETE | Working |
|
||||
|
||||
**UI Elements to Test:**
|
||||
- [ ] Cart item list
|
||||
@@ -162,8 +162,8 @@
|
||||
### Checkout Process
|
||||
| Test Case | Route | Method | Status |
|
||||
|-----------|-------|--------|--------|
|
||||
| Checkout page | `/shop/checkout` | GET | Working |
|
||||
| API: Place order | `/api/v1/shop/orders` | POST | Working |
|
||||
| Checkout page | `/storefront/checkout` | GET | Working |
|
||||
| API: Place order | `/api/v1/storefront/orders` | POST | Working |
|
||||
|
||||
### Step 1: Contact & Shipping
|
||||
**Form Fields:**
|
||||
@@ -225,8 +225,8 @@
|
||||
### Login
|
||||
| Test Case | Route | Method | Status |
|
||||
|-----------|-------|--------|--------|
|
||||
| Login page | `/shop/account/login` | GET | Working |
|
||||
| API: Login | `/api/v1/shop/auth/login` | POST | Working |
|
||||
| Login page | `/storefront/account/login` | GET | Working |
|
||||
| API: Login | `/api/v1/storefront/auth/login` | POST | Working |
|
||||
|
||||
**Form Fields:**
|
||||
- [ ] Email/username (required)
|
||||
@@ -251,8 +251,8 @@
|
||||
### Registration
|
||||
| Test Case | Route | Method | Status |
|
||||
|-----------|-------|--------|--------|
|
||||
| Register page | `/shop/account/register` | GET | Working |
|
||||
| API: Register | `/api/v1/shop/auth/register` | POST | Working |
|
||||
| Register page | `/storefront/account/register` | GET | Working |
|
||||
| API: Register | `/api/v1/storefront/auth/register` | POST | Working |
|
||||
|
||||
**Form Fields:**
|
||||
- [ ] First name (required)
|
||||
@@ -283,8 +283,8 @@
|
||||
### Forgot Password
|
||||
| Test Case | Route | Method | Status |
|
||||
|-----------|-------|--------|--------|
|
||||
| Forgot password page | `/shop/account/forgot-password` | GET | Working |
|
||||
| API: Request reset | `/api/v1/shop/auth/forgot-password` | POST | Working |
|
||||
| Forgot password page | `/storefront/account/forgot-password` | GET | Working |
|
||||
| API: Request reset | `/api/v1/storefront/auth/forgot-password` | POST | Working |
|
||||
|
||||
**Form Fields:**
|
||||
- [ ] Email (required)
|
||||
@@ -303,8 +303,8 @@
|
||||
### Reset Password
|
||||
| Test Case | Route | Method | Status |
|
||||
|-----------|-------|--------|--------|
|
||||
| Reset password page | `/shop/account/reset-password` | GET | Working |
|
||||
| API: Reset password | `/api/v1/shop/auth/reset-password` | POST | Working |
|
||||
| Reset password page | `/storefront/account/reset-password` | GET | Working |
|
||||
| API: Reset password | `/api/v1/storefront/auth/reset-password` | POST | Working |
|
||||
|
||||
**Form Fields:**
|
||||
- [ ] Reset token (auto-filled from URL)
|
||||
@@ -327,7 +327,7 @@
|
||||
### Logout
|
||||
| Test Case | Route | Method | Status |
|
||||
|-----------|-------|--------|--------|
|
||||
| API: Logout | `/api/v1/shop/auth/logout` | POST | Working |
|
||||
| API: Logout | `/api/v1/storefront/auth/logout` | POST | Working |
|
||||
|
||||
---
|
||||
|
||||
@@ -336,8 +336,8 @@
|
||||
### Account Dashboard
|
||||
| Test Case | Route | Method | Status |
|
||||
|-----------|-------|--------|--------|
|
||||
| Dashboard page | `/shop/account/dashboard` | GET | Working |
|
||||
| API: Get profile | `/api/v1/shop/profile` | GET | Working |
|
||||
| Dashboard page | `/storefront/account/dashboard` | GET | Working |
|
||||
| API: Get profile | `/api/v1/storefront/profile` | GET | Working |
|
||||
|
||||
**UI Elements to Test:**
|
||||
- [ ] Welcome message with name
|
||||
@@ -354,9 +354,9 @@
|
||||
### Profile Management
|
||||
| Test Case | Route | Method | Status |
|
||||
|-----------|-------|--------|--------|
|
||||
| Profile page | `/shop/account/profile` | GET | Working |
|
||||
| API: Get profile | `/api/v1/shop/profile` | GET | Working |
|
||||
| API: Update profile | `/api/v1/shop/profile` | PUT | Working |
|
||||
| Profile page | `/storefront/account/profile` | GET | Working |
|
||||
| API: Get profile | `/api/v1/storefront/profile` | GET | Working |
|
||||
| API: Update profile | `/api/v1/storefront/profile` | PUT | Working |
|
||||
|
||||
**Profile Form Fields:**
|
||||
- [ ] First name
|
||||
@@ -382,7 +382,7 @@
|
||||
### Change Password
|
||||
| Test Case | Route | Method | Status |
|
||||
|-----------|-------|--------|--------|
|
||||
| API: Change password | `/api/v1/shop/profile/password` | PUT | Working |
|
||||
| API: Change password | `/api/v1/storefront/profile/password` | PUT | Working |
|
||||
|
||||
**Password Change Form:**
|
||||
- [ ] Current password (required)
|
||||
@@ -406,8 +406,8 @@
|
||||
### Address List
|
||||
| Test Case | Route | Method | Status |
|
||||
|-----------|-------|--------|--------|
|
||||
| Addresses page | `/shop/account/addresses` | GET | Working |
|
||||
| API: List addresses | `/api/v1/shop/addresses` | GET | Working |
|
||||
| Addresses page | `/storefront/account/addresses` | GET | Working |
|
||||
| API: List addresses | `/api/v1/storefront/addresses` | GET | Working |
|
||||
|
||||
**UI Elements to Test:**
|
||||
- [ ] Address cards grid
|
||||
@@ -421,11 +421,11 @@
|
||||
### Address CRUD
|
||||
| Test Case | Route | Method | Status |
|
||||
|-----------|-------|--------|--------|
|
||||
| API: Get single address | `/api/v1/shop/addresses/{address_id}` | GET | Working |
|
||||
| API: Create address | `/api/v1/shop/addresses` | POST | Working |
|
||||
| API: Update address | `/api/v1/shop/addresses/{address_id}` | PUT | Working |
|
||||
| API: Set as default | `/api/v1/shop/addresses/{address_id}/default` | PUT | Working |
|
||||
| API: Delete address | `/api/v1/shop/addresses/{address_id}` | DELETE | Working |
|
||||
| API: Get single address | `/api/v1/storefront/addresses/{address_id}` | GET | Working |
|
||||
| API: Create address | `/api/v1/storefront/addresses` | POST | Working |
|
||||
| API: Update address | `/api/v1/storefront/addresses/{address_id}` | PUT | Working |
|
||||
| API: Set as default | `/api/v1/storefront/addresses/{address_id}/default` | PUT | Working |
|
||||
| API: Delete address | `/api/v1/storefront/addresses/{address_id}` | DELETE | Working |
|
||||
|
||||
**Address Form Fields:**
|
||||
- [ ] Address type (shipping/billing)
|
||||
@@ -466,8 +466,8 @@
|
||||
### Orders List
|
||||
| Test Case | Route | Method | Status |
|
||||
|-----------|-------|--------|--------|
|
||||
| Orders page | `/shop/account/orders` | GET | Working |
|
||||
| API: List orders | `/api/v1/shop/orders` | GET | Working |
|
||||
| Orders page | `/storefront/account/orders` | GET | Working |
|
||||
| API: List orders | `/api/v1/storefront/orders` | GET | Working |
|
||||
|
||||
**UI Elements to Test:**
|
||||
- [ ] Order cards list
|
||||
@@ -491,8 +491,8 @@
|
||||
### Order Detail
|
||||
| Test Case | Route | Method | Status |
|
||||
|-----------|-------|--------|--------|
|
||||
| Order detail page | `/shop/account/orders/{order_id}` | GET | Working |
|
||||
| API: Get order | `/api/v1/shop/orders/{order_id}` | GET | Working |
|
||||
| Order detail page | `/storefront/account/orders/{order_id}` | GET | Working |
|
||||
| API: Get order | `/api/v1/storefront/orders/{order_id}` | GET | Working |
|
||||
|
||||
**UI Elements to Test:**
|
||||
- [ ] Order header (number, date, status)
|
||||
@@ -508,7 +508,7 @@
|
||||
### Invoice Download
|
||||
| Test Case | Route | Method | Status |
|
||||
|-----------|-------|--------|--------|
|
||||
| API: Download invoice | `/api/v1/shop/orders/{order_id}/invoice` | GET | Working |
|
||||
| API: Download invoice | `/api/v1/storefront/orders/{order_id}/invoice` | GET | Working |
|
||||
|
||||
**Note:** Only available for orders with status: processing, partially_shipped, shipped, delivered, completed
|
||||
|
||||
@@ -519,9 +519,9 @@
|
||||
### Messages List
|
||||
| Test Case | Route | Method | Status |
|
||||
|-----------|-------|--------|--------|
|
||||
| Messages page | `/shop/account/messages` | GET | Working |
|
||||
| API: List conversations | `/api/v1/shop/messages` | GET | Working |
|
||||
| API: Get unread count | `/api/v1/shop/messages/unread-count` | GET | Working |
|
||||
| Messages page | `/storefront/account/messages` | GET | Working |
|
||||
| API: List conversations | `/api/v1/storefront/messages` | GET | Working |
|
||||
| API: Get unread count | `/api/v1/storefront/messages/unread-count` | GET | Working |
|
||||
|
||||
**UI Elements to Test:**
|
||||
- [ ] Conversation list
|
||||
@@ -536,9 +536,9 @@
|
||||
### Conversation Detail
|
||||
| Test Case | Route | Method | Status |
|
||||
|-----------|-------|--------|--------|
|
||||
| API: Get conversation | `/api/v1/shop/messages/{conversation_id}` | GET | Working |
|
||||
| API: Send message | `/api/v1/shop/messages/{conversation_id}/messages` | POST | Working |
|
||||
| API: Mark as read | `/api/v1/shop/messages/{conversation_id}/read` | PUT | Working |
|
||||
| API: Get conversation | `/api/v1/storefront/messages/{conversation_id}` | GET | Working |
|
||||
| API: Send message | `/api/v1/storefront/messages/{conversation_id}/messages` | POST | Working |
|
||||
| API: Mark as read | `/api/v1/storefront/messages/{conversation_id}/read` | PUT | Working |
|
||||
|
||||
**UI Elements to Test:**
|
||||
- [ ] Message thread display
|
||||
@@ -562,8 +562,8 @@
|
||||
### Attachment Download
|
||||
| Test Case | Route | Method | Status |
|
||||
|-----------|-------|--------|--------|
|
||||
| API: Download attachment | `/api/v1/shop/messages/{conversation_id}/attachments/{attachment_id}` | GET | Working |
|
||||
| API: Get thumbnail | `/api/v1/shop/messages/{conversation_id}/attachments/{attachment_id}/thumbnail` | GET | Working |
|
||||
| API: Download attachment | `/api/v1/storefront/messages/{conversation_id}/attachments/{attachment_id}` | GET | Working |
|
||||
| API: Get thumbnail | `/api/v1/storefront/messages/{conversation_id}/attachments/{attachment_id}/thumbnail` | GET | Working |
|
||||
|
||||
---
|
||||
|
||||
@@ -572,17 +572,17 @@
|
||||
### CMS Pages
|
||||
| Test Case | Route | Method | Status |
|
||||
|-----------|-------|--------|--------|
|
||||
| View content page | `/shop/page/{slug}` | GET | Working |
|
||||
| API: Get navigation | `/api/v1/shop/content-pages/navigation` | GET | Working |
|
||||
| API: Get page content | `/api/v1/shop/content-pages/{slug}` | GET | Working |
|
||||
| View content page | `/storefront/page/{slug}` | GET | Working |
|
||||
| API: Get navigation | `/api/v1/storefront/content-pages/navigation` | GET | Working |
|
||||
| API: Get page content | `/api/v1/storefront/content-pages/{slug}` | GET | Working |
|
||||
|
||||
**Common Pages to Test:**
|
||||
- [ ] About Us (`/shop/about`)
|
||||
- [ ] Contact (`/shop/contact`)
|
||||
- [ ] Terms & Conditions (`/shop/terms`)
|
||||
- [ ] Privacy Policy (`/shop/privacy`)
|
||||
- [ ] Return Policy (`/shop/returns`)
|
||||
- [ ] FAQ (`/shop/faq`)
|
||||
- [ ] About Us (`/storefront/about`)
|
||||
- [ ] Contact (`/storefront/contact`)
|
||||
- [ ] Terms & Conditions (`/storefront/terms`)
|
||||
- [ ] Privacy Policy (`/storefront/privacy`)
|
||||
- [ ] Return Policy (`/storefront/returns`)
|
||||
- [ ] FAQ (`/storefront/faq`)
|
||||
|
||||
**UI Elements to Test:**
|
||||
- [ ] Page title
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Shop Frontend Troubleshooting
|
||||
# Storefront Troubleshooting
|
||||
|
||||
Common issues and solutions for the store shop frontend.
|
||||
Common issues and solutions for the store storefront.
|
||||
|
||||
## Cart and Product Issues
|
||||
|
||||
@@ -78,7 +78,7 @@ async init() {
|
||||
**Verify Fix:**
|
||||
Open browser console and check for:
|
||||
```
|
||||
🔍 [SHOP] Session ID: session_1763765104510_zc866tt5d
|
||||
🔍 [STOREFRONT] Session ID: session_1763765104510_zc866tt5d
|
||||
```
|
||||
|
||||
If you see `undefined`, the parent init isn't being called properly.
|
||||
@@ -95,11 +95,11 @@ If you see `undefined`, the parent init isn't being called properly.
|
||||
- Page is just plain HTML
|
||||
|
||||
**Root Cause:**
|
||||
Template doesn't extend `shop/base.html` and has hardcoded non-existent CSS references.
|
||||
Template doesn't extend `storefront/base.html` and has hardcoded non-existent CSS references.
|
||||
|
||||
**How it Works:**
|
||||
- All shop pages should extend `shop/base.html`
|
||||
- Base template includes Tailwind CSS and shop styles
|
||||
- All storefront pages should extend `storefront/base.html`
|
||||
- Base template includes Tailwind CSS and storefront styles
|
||||
- Standalone HTML pages with `<link>` tags won't work
|
||||
|
||||
**Solution:**
|
||||
@@ -118,7 +118,7 @@ Refactor template to extend base:
|
||||
</html>
|
||||
|
||||
{# AFTER: Extends base #}
|
||||
{% extends "shop/base.html" %}
|
||||
{% extends "storefront/base.html" %}
|
||||
|
||||
{% block title %}My Page{% endblock %}
|
||||
|
||||
@@ -130,16 +130,16 @@ Refactor template to extend base:
|
||||
```
|
||||
|
||||
**Templates Already Fixed:**
|
||||
- ✅ `shop/product.html` - Refactored to extend base
|
||||
- ✅ `shop/cart.html` - Refactored to extend base
|
||||
- ✅ `shop/products.html` - Already extends base
|
||||
- ✅ `shop/home.html` - Already extends base
|
||||
- ✅ `storefront/product.html` - Refactored to extend base
|
||||
- ✅ `storefront/cart.html` - Refactored to extend base
|
||||
- ✅ `storefront/products.html` - Already extends base
|
||||
- ✅ `storefront/home.html` - Already extends base
|
||||
|
||||
### Images Not Loading / Placeholder Not Showing
|
||||
|
||||
**Symptoms:**
|
||||
- Product images show broken image icon
|
||||
- Console shows 404: `/static/shop/img/placeholder.jpg`
|
||||
- Console shows 404: `/static/storefront/img/placeholder.jpg`
|
||||
- OR images point to fake URLs like `https://orion.example.com/images/product-1.jpg`
|
||||
|
||||
**Root Cause:**
|
||||
@@ -148,18 +148,18 @@ Refactor template to extend base:
|
||||
|
||||
**How it Works:**
|
||||
- Products have `marketplace_product.image_link` URLs
|
||||
- Template uses fallback: `:src="image || '/static/shop/img/placeholder.svg'"`
|
||||
- Template uses fallback: `:src="image || '/static/storefront/img/placeholder.svg'"`
|
||||
- `@error` handler switches to placeholder when image fails to load
|
||||
- Browsers won't render SVG content from `.jpg` files
|
||||
|
||||
**Solution Already Implemented:**
|
||||
- Created proper `/static/shop/img/placeholder.svg`
|
||||
- Created proper `/static/storefront/img/placeholder.svg`
|
||||
- Added `@error` handlers to all image tags:
|
||||
|
||||
```html
|
||||
<img
|
||||
:src="product.image_link || '/static/shop/img/placeholder.svg'"
|
||||
@error="$el.src = '/static/shop/img/placeholder.svg'"
|
||||
:src="product.image_link || '/static/storefront/img/placeholder.svg'"
|
||||
@error="$el.src = '/static/storefront/img/placeholder.svg'"
|
||||
alt="Product image"
|
||||
>
|
||||
```
|
||||
@@ -185,34 +185,34 @@ conn.commit()
|
||||
|
||||
**Symptoms:**
|
||||
- Clicking product shows 404 error
|
||||
- URL is: `/stores/orion/shop/shop/products/4` (double `/shop/`)
|
||||
- URL is: `/storefront/orion/storefront/products/4` (double `/storefront/`)
|
||||
- Server log shows route not found
|
||||
|
||||
**Root Cause:**
|
||||
Duplicate `/shop` prefix in route definitions when router already has prefix.
|
||||
Duplicate `/storefront` prefix in route definitions when router already has prefix.
|
||||
|
||||
**How it Works:**
|
||||
```python
|
||||
# Router is mounted with prefix
|
||||
app.include_router(shop_pages.router, prefix="/shop")
|
||||
app.include_router(storefront_pages.router, prefix="/storefront")
|
||||
|
||||
# Route decorator should NOT repeat the prefix
|
||||
@router.get("/products/{id}") # ✅ Correct
|
||||
@router.get("/shop/products/{id}") # ❌ Wrong - creates /shop/shop/products/{id}
|
||||
@router.get("/storefront/products/{id}") # ❌ Wrong - creates /storefront/storefront/products/{id}
|
||||
```
|
||||
|
||||
**Solution Already Implemented:**
|
||||
All routes in `shop_pages.py` have been fixed to remove duplicate `/shop/` prefix.
|
||||
All routes in `storefront_pages.py` have been fixed to remove duplicate `/storefront/` prefix.
|
||||
|
||||
### Missing `/shop/` in Links
|
||||
### Missing `/storefront/` in Links
|
||||
|
||||
**Symptoms:**
|
||||
- Links go to `/stores/orion/products` instead of `/stores/orion/shop/products`
|
||||
- Links go to `/storefront/orion/products` instead of correct storefront URLs
|
||||
- 404 errors on navigation
|
||||
- Footer/header links broken
|
||||
|
||||
**Root Cause:**
|
||||
Template links missing `/shop/` prefix after `{{ base_url }}`.
|
||||
Template links missing `/storefront/` prefix after `{{ base_url }}`.
|
||||
|
||||
**Solution:**
|
||||
```jinja2
|
||||
@@ -220,15 +220,15 @@ Template links missing `/shop/` prefix after `{{ base_url }}`.
|
||||
<a href="{{ base_url }}products">Products</a>
|
||||
|
||||
{# CORRECT #}
|
||||
<a href="{{ base_url }}shop/products">Products</a>
|
||||
<a href="{{ base_url }}storefront/products">Products</a>
|
||||
```
|
||||
|
||||
**All Templates Fixed:**
|
||||
- ✅ `shop/base.html` - Header, footer, navigation
|
||||
- ✅ `shop/products.html` - Product links
|
||||
- ✅ `shop/product.html` - Breadcrumbs, related products
|
||||
- ✅ `shop/cart.html` - Continue shopping, checkout
|
||||
- ✅ `shop/errors/404.html` - All fallback links
|
||||
- ✅ `storefront/base.html` - Header, footer, navigation
|
||||
- ✅ `storefront/products.html` - Product links
|
||||
- ✅ `storefront/product.html` - Breadcrumbs, related products
|
||||
- ✅ `storefront/cart.html` - Continue shopping, checkout
|
||||
- ✅ `storefront/errors/404.html` - All fallback links
|
||||
|
||||
## Landing Page Issues
|
||||
|
||||
@@ -247,7 +247,7 @@ No landing page created for store.
|
||||
if has_landing_page():
|
||||
return render_landing_page()
|
||||
else:
|
||||
return redirect("/shop/") # Fallback
|
||||
return redirect("/storefront/") # Fallback
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
@@ -258,12 +258,12 @@ python scripts/create_landing_page.py
|
||||
```
|
||||
|
||||
**Or Accept Redirect:**
|
||||
The system auto-redirects to `/shop/` if no landing page exists. This is normal behavior.
|
||||
The system auto-redirects to `/storefront/` if no landing page exists. This is normal behavior.
|
||||
|
||||
### Breadcrumb "Home" Points to Wrong Place
|
||||
|
||||
**Symptoms:**
|
||||
- Clicking "Home" in breadcrumb goes to shop instead of landing page
|
||||
- Clicking "Home" in breadcrumb goes to storefront instead of landing page
|
||||
- Want "Home" to point to store root (/)
|
||||
|
||||
**Solution Already Implemented:**
|
||||
@@ -271,12 +271,12 @@ The system auto-redirects to `/shop/` if no landing page exists. This is normal
|
||||
{# Breadcrumb Home link points to store root #}
|
||||
<a href="{{ base_url }}">Home</a>
|
||||
|
||||
{# Shop homepage link #}
|
||||
<a href="{{ base_url }}shop/">Shop</a>
|
||||
{# Storefront homepage link #}
|
||||
<a href="{{ base_url }}storefront/">Storefront</a>
|
||||
```
|
||||
|
||||
Navigation pattern:
|
||||
- **Logo click** → `/shop/` (stays in shop for convenience)
|
||||
- **Logo click** → `/storefront/` (stays in storefront for convenience)
|
||||
- **Breadcrumb "Home"** → `/` (returns to landing page/root)
|
||||
- **Header "Home" link** → `/` (returns to landing page/root)
|
||||
|
||||
@@ -317,7 +317,7 @@ return {
|
||||
### Product ID is Undefined
|
||||
|
||||
**Symptoms:**
|
||||
- API call: `/api/v1/shop/products/undefined`
|
||||
- API call: `/api/v1/storefront/products/undefined`
|
||||
- Console: `productId: undefined`
|
||||
|
||||
**Root Cause:**
|
||||
@@ -346,12 +346,12 @@ Alpine.data('productDetail', () => ({
|
||||
### Enable Verbose Logging
|
||||
|
||||
```javascript
|
||||
// In shop-layout.js, the shopLog is already configured
|
||||
// In storefront-layout.js, the storefrontLog is already configured
|
||||
// Check browser console for:
|
||||
🛒 [SHOP] Shop layout initializing...
|
||||
🔍 [SHOP] Session ID: session_xxx
|
||||
[SHOP] Adding to cart: {...}
|
||||
[SHOP] Cart loaded: 3 items
|
||||
🛒 [STOREFRONT] Storefront layout initializing...
|
||||
🔍 [STOREFRONT] Session ID: session_xxx
|
||||
[STOREFRONT] Adding to cart: {...}
|
||||
[STOREFRONT] Cart loaded: 3 items
|
||||
```
|
||||
|
||||
### Check Session ID
|
||||
@@ -372,10 +372,10 @@ Alpine.$data(document.querySelector('[x-data]')).product
|
||||
|
||||
### Check API Responses
|
||||
|
||||
In browser Network tab, filter by "shop" and check:
|
||||
- GET `/api/v1/shop/products` - Should return products array
|
||||
- GET `/api/v1/shop/cart/{session_id}` - Should return cart items
|
||||
- POST `/api/v1/shop/cart/{session_id}/items` - Should return success
|
||||
In browser Network tab, filter by "storefront" and check:
|
||||
- GET `/api/v1/storefront/products` - Should return products array
|
||||
- GET `/api/v1/storefront/cart/{session_id}` - Should return cart items
|
||||
- POST `/api/v1/storefront/cart/{session_id}/items` - Should return success
|
||||
|
||||
## Getting Help
|
||||
|
||||
@@ -383,7 +383,7 @@ In browser Network tab, filter by "shop" and check:
|
||||
2. Check browser console for errors
|
||||
3. Check server logs for API errors
|
||||
4. Verify database has inventory entries
|
||||
5. Ensure all templates extend `shop/base.html`
|
||||
5. Ensure all templates extend `storefront/base.html`
|
||||
6. Check that session ID is initialized
|
||||
|
||||
If still stuck, provide:
|
||||
|
||||
Reference in New Issue
Block a user