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

- Add Development URL Quick Reference section to url-routing overview
  with all login URLs, entry points, and full examples
- Replace /shop/ path segments with /storefront/ across 50 docs files
- Update file references: shop_pages.py → storefront_pages.py,
  templates/shop/ → templates/storefront/, api/v1/shop/ → api/v1/storefront/
- Preserve domain references (orion.shop) and /store/ staff dashboard paths
- Archive docs left unchanged (historical)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-25 13:23:44 +01:00
parent 3df75e2e78
commit d648c921b7
50 changed files with 1104 additions and 1049 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -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/*
```

View File

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

View File

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

View File

@@ -6,9 +6,9 @@
## Executive Summary
The platform currently has **two parallel API structures** for shop/customer-facing endpoints:
The platform currently has **two parallel API structures** for storefront/customer-facing endpoints:
1. **Original:** `/api/v1/platform/stores/{store_id}/*`
2. **New:** `/api/v1/shop/*`
2. **New:** `/api/v1/storefront/*`
This divergence creates confusion, maintenance overhead, and potential bugs. This document analyzes the situation and proposes a consolidation strategy.
@@ -47,26 +47,26 @@ POST /api/v1/platform/stores/auth/register → Customer registration
---
### 2. New Architecture (`/api/v1/shop/`)
### 2. New Architecture (`/api/v1/storefront/`)
**Location:** `app/api/v1/shop/`
**Location:** `app/api/v1/storefront/`
**Endpoints:**
```
GET /api/v1/shop/content-pages/navigation → CMS navigation pages
GET /api/v1/shop/content-pages/{slug} → CMS page content
GET /api/v1/storefront/content-pages/navigation → CMS navigation pages
GET /api/v1/storefront/content-pages/{slug} → CMS page content
```
**Characteristics:**
-**Store-agnostic URLs:** Clean paths without store_id
-**Middleware-driven:** Relies on `StoreContextMiddleware` to inject store
-**Simpler URLs:** `/api/v1/shop/products` vs `/api/v1/platform/stores/123/products`
-**Simpler URLs:** `/api/v1/storefront/products` vs `/api/v1/platform/stores/123/products`
-**Incomplete:** Only CMS endpoints implemented
-**Divergent:** Not consistent with existing public API
**Current Usage:**
- CMS content pages only
- Called from shop templates (e.g., `shop/products.html`, `shop/home.html`)
- Called from storefront templates (e.g., `storefront/products.html`, `storefront/home.html`)
---
@@ -77,7 +77,7 @@ GET /api/v1/shop/content-pages/{slug} → CMS page content
```javascript
// ❌ INCONSISTENT - Two different patterns for same context
// CMS pages use new pattern
fetch('/api/v1/shop/content-pages/about')
fetch('/api/v1/storefront/content-pages/about')
// Products use old pattern
fetch('/api/v1/platform/stores/123/products')
@@ -86,7 +86,7 @@ fetch('/api/v1/platform/stores/123/products')
### Confusion
Developers must remember:
- "Is this endpoint under `/shop` or `/public/stores`?"
- "Is this endpoint under `/storefront` or `/public/stores`?"
- "Do I need to pass store_id or is it from middleware?"
- "Which authentication endpoints do I use?"
@@ -102,7 +102,7 @@ Developers must remember:
**Current Issue:** CMS pages not loading at `/stores/orion/about`
**Root Cause:**
- CMS API exists at `/api/v1/shop/content-pages/{slug}`
- CMS API exists at `/api/v1/storefront/content-pages/{slug}`
- No corresponding HTML route handler in `store_pages.py`
- JavaScript might be calling wrong endpoint
@@ -110,13 +110,13 @@ Developers must remember:
## Options Analysis
### Option 1: Move Everything to `/api/v1/shop/*` (Middleware-Driven)
### Option 1: Move Everything to `/api/v1/storefront/*` (Middleware-Driven)
**Approach:** Consolidate all customer-facing endpoints under `/api/v1/shop/*`
**Approach:** Consolidate all customer-facing endpoints under `/api/v1/storefront/*`
**Proposed Structure:**
```
/api/v1/shop/
/api/v1/storefront/
├── auth/
│ ├── POST /login → Customer login
│ ├── POST /register → Customer registration
@@ -143,7 +143,7 @@ Developers must remember:
**Implementation:**
- Store extracted by `StoreContextMiddleware` from request
- All endpoints use `request.state.store` instead of path parameter
- URLs are cleaner: `/api/v1/shop/products` instead of `/api/v1/platform/stores/123/products`
- URLs are cleaner: `/api/v1/storefront/products` instead of `/api/v1/platform/stores/123/products`
**Pros:**
- ✅ Clean, consistent API structure
@@ -162,17 +162,17 @@ Developers must remember:
---
### Option 2: Keep `/api/v1/platform/stores/*` and Deprecate `/api/v1/shop/*`
### Option 2: Keep `/api/v1/platform/stores/*` and Deprecate `/api/v1/storefront/*`
**Approach:** Move CMS endpoints to `/api/v1/platform/stores/{store_id}/content-pages/*`
**Proposed Changes:**
```
# Move CMS endpoints
FROM: /api/v1/shop/content-pages/navigation
FROM: /api/v1/storefront/content-pages/navigation
TO: /api/v1/platform/stores/{store_id}/content-pages/navigation
FROM: /api/v1/shop/content-pages/{slug}
FROM: /api/v1/storefront/content-pages/{slug}
TO: /api/v1/platform/stores/{store_id}/content-pages/{slug}
```
@@ -228,7 +228,7 @@ async def get_products_legacy(store_id: int, db: Session = Depends(get_db)):
## Recommendation
### **OPTION 1: Consolidate to `/api/v1/shop/*` (Middleware-Driven)**
### **OPTION 1: Consolidate to `/api/v1/storefront/*` (Middleware-Driven)**
**Rationale:**
@@ -237,7 +237,7 @@ async def get_products_legacy(store_id: int, db: Session = Depends(get_db)):
2. **User Experience**: Cleaner URLs are easier for frontend developers:
```javascript
// ✅ GOOD
fetch('/api/v1/shop/products')
fetch('/api/v1/storefront/products')
// ❌ BAD
fetch('/api/v1/platform/stores/123/products')
@@ -245,7 +245,7 @@ async def get_products_legacy(store_id: int, db: Session = Depends(get_db)):
3. **Multi-Tenant Best Practice**: Store context should be implicit (from domain/path), not explicit in every API call.
4. **Consistency**: All shop endpoints follow same pattern - no mixing `/shop` and `/public/stores`.
4. **Consistency**: All storefront endpoints follow same pattern - no mixing `/storefront` and `/public/stores`.
5. **Future-Proof**: Easier to add new shop features without worrying about store_id paths.
@@ -258,7 +258,7 @@ async def get_products_legacy(store_id: int, db: Session = Depends(get_db)):
**Day 1-2: Move Products**
```bash
# Copy and adapt
app/api/v1/platform/stores/products.py → app/api/v1/shop/products.py
app/api/v1/platform/stores/products.py → app/api/v1/storefront/products.py
# Changes:
- Remove store_id path parameter
@@ -268,24 +268,24 @@ app/api/v1/platform/stores/products.py → app/api/v1/shop/products.py
**Day 3: Move Cart**
```bash
app/api/v1/platform/stores/cart.py → app/api/v1/shop/cart.py
app/api/v1/platform/stores/cart.py → app/api/v1/storefront/cart.py
```
**Day 4: Move Orders**
```bash
app/api/v1/platform/stores/orders.py → app/api/v1/shop/orders.py
app/api/v1/platform/stores/orders.py → app/api/v1/storefront/orders.py
```
**Day 5: Move Auth**
```bash
app/api/v1/platform/stores/auth.py → app/api/v1/shop/auth.py
app/api/v1/platform/stores/auth.py → app/api/v1/storefront/auth.py
```
### Phase 2: Update Frontend (Week 1)
**Templates:**
- Update all `fetch()` calls in shop templates
- Change from `/api/v1/platform/stores/${storeId}/...` to `/api/v1/shop/...`
- Change from `/api/v1/platform/stores/${storeId}/...` to `/api/v1/storefront/...`
**JavaScript:**
- Update any shop-related API client code
@@ -335,10 +335,10 @@ const storeId = 123;
fetch(`/api/v1/platform/stores/${storeId}/products`)
```
### After (Proposed - `/api/v1/shop`)
### After (Proposed - `/api/v1/storefront`)
```python
# app/api/v1/shop/products.py
# app/api/v1/storefront/products.py
@router.get("/products")
def get_product_catalog(
request: Request,
@@ -350,7 +350,7 @@ def get_product_catalog(
```javascript
// Frontend
fetch('/api/v1/shop/products') // Store context automatic
fetch('/api/v1/storefront/products') // Store context automatic
```
---
@@ -382,7 +382,7 @@ If full migration is not approved immediately, we can do a **minimal fix** for t
### Quick Fix: Just Move CMS to Public API
```python
# Move: app/api/v1/shop/content_pages.py
# Move: app/api/v1/storefront/content_pages.py
# To: app/api/v1/platform/stores/content_pages.py
# Update routes:
@@ -401,7 +401,7 @@ If full migration is not approved immediately, we can do a **minimal fix** for t
**Question for Team:**
Should we:
1. ✅ **Consolidate to `/api/v1/shop/*`** (Recommended)
1. ✅ **Consolidate to `/api/v1/storefront/*`** (Recommended)
2. ❌ **Keep `/api/v1/platform/stores/*`** and move CMS there
3. ❌ **Hybrid approach** with both patterns
4. ❌ **Quick fix only** - move CMS, address later
@@ -422,15 +422,15 @@ Should we:
- 🚧 `search.py` - Stub
- 🚧 `shop.py` - Stub
### `/api/v1/shop/*`
### `/api/v1/storefront/*`
- ✅ `content_pages.py` - CMS pages
### To Be Created (if Option 1 chosen)
- 📝 `shop/products.py`
- 📝 `shop/cart.py`
- 📝 `shop/orders.py`
- 📝 `shop/auth.py`
- 📝 `shop/stores.py` (marketplace listing)
- 📝 `storefront/products.py`
- 📝 `storefront/cart.py`
- 📝 `storefront/orders.py`
- 📝 `storefront/auth.py`
- 📝 `storefront/stores.py` (marketplace listing)
---

View File

@@ -1,55 +1,55 @@
# API Migration Status - `/api/v1/shop/*` Consolidation
# API Migration Status - `/api/v1/storefront/*` Consolidation
**Date:** 2025-11-22
**Status:** 🎉 MIGRATION COMPLETE - All Phases Done
**Decision:** Option 1 - Full Consolidation to `/api/v1/shop/*`
**Decision:** Option 1 - Full Consolidation to `/api/v1/storefront/*`
---
## Progress Overview
### ✅ Phase 1: New Shop API Endpoints (COMPLETE)
### ✅ Phase 1: New Storefront API Endpoints (COMPLETE)
All new shop endpoints have been created using middleware-based store context:
All new storefront endpoints have been created using middleware-based store context:
### ✅ Middleware Update: Referer-Based Store Extraction (COMPLETE)
Updated `StoreContextMiddleware` to support shop API routes:
- Added `is_shop_api_request()` method to detect `/api/v1/shop/*` routes
Updated `StoreContextMiddleware` to support storefront API routes:
- Added `is_storefront_api_request()` method to detect `/api/v1/storefront/*` routes
- Added `extract_store_from_referer()` method to extract store from Referer/Origin headers
- Modified `dispatch()` to handle shop API routes specially (no longer skips them)
- Shop API now receives store context from the page that made the API call
- Modified `dispatch()` to handle storefront API routes specially (no longer skips them)
- Storefront API now receives store context from the page that made the API call
**How it works:**
1. Browser JavaScript on `/stores/orion/shop/products` calls `/api/v1/shop/products`
2. Browser automatically sends `Referer: http://localhost:8000/stores/orion/shop/products`
1. Browser JavaScript on `/storefront/orion/products` calls `/api/v1/storefront/products`
2. Browser automatically sends `Referer: http://localhost:8000/storefront/orion/products`
3. Middleware extracts `orion` from Referer path
4. Queries database to get Store object
5. Sets `request.state.store` for the API endpoint
### ✅ Phase 1a: Endpoint Testing (COMPLETE)
Tested shop API endpoints with Referer header:
Tested storefront API endpoints with Referer header:
| Endpoint | Method | Test Result | Notes |
|----------|--------|-------------|-------|
| `/api/v1/shop/products` | GET | ✅ Working | Returns paginated product list |
| `/api/v1/shop/products?search=Sample` | GET | ✅ Working | Search functionality works |
| `/api/v1/shop/products?is_featured=true` | GET | ✅ Working | Featured filter works |
| `/api/v1/shop/products/{id}` | GET | ✅ Working | Product details returned |
| `/api/v1/shop/cart/{session_id}` | GET | ✅ Working | Empty cart returns correctly |
| `/api/v1/shop/content-pages/navigation` | GET | ✅ Working | Navigation links returned |
| `/api/v1/storefront/products` | GET | ✅ Working | Returns paginated product list |
| `/api/v1/storefront/products?search=Sample` | GET | ✅ Working | Search functionality works |
| `/api/v1/storefront/products?is_featured=true` | GET | ✅ Working | Featured filter works |
| `/api/v1/storefront/products/{id}` | GET | ✅ Working | Product details returned |
| `/api/v1/storefront/cart/{session_id}` | GET | ✅ Working | Empty cart returns correctly |
| `/api/v1/storefront/content-pages/navigation` | GET | ✅ Working | Navigation links returned |
**All tested endpoints successfully receive store context from Referer header.**
| Endpoint File | Status | Routes | Description |
|--------------|--------|--------|-------------|
| `shop/products.py` | ✅ Complete | 3 routes | Product catalog, details, search |
| `shop/cart.py` | ✅ Complete | 5 routes | Cart CRUD operations |
| `shop/orders.py` | ✅ Complete | 3 routes | Order placement & history |
| `shop/auth.py` | ✅ Complete | 5 routes | Login, register, password reset |
| `shop/content_pages.py` | ✅ Existing | 2 routes | CMS pages (already present) |
| `shop/__init__.py` | ✅ Updated | - | Router aggregation |
| `storefront/products.py` | ✅ Complete | 3 routes | Product catalog, details, search |
| `storefront/cart.py` | ✅ Complete | 5 routes | Cart CRUD operations |
| `storefront/orders.py` | ✅ Complete | 3 routes | Order placement & history |
| `storefront/auth.py` | ✅ Complete | 5 routes | Login, register, password reset |
| `storefront/content_pages.py` | ✅ Existing | 2 routes | CMS pages (already present) |
| `storefront/__init__.py` | ✅ Updated | - | Router aggregation |
**Total:** 18 new API endpoints created
@@ -57,46 +57,46 @@ Tested shop API endpoints with Referer header:
## New API Structure
### Shop Endpoints (`/api/v1/shop/*`)
### Storefront Endpoints (`/api/v1/storefront/*`)
All endpoints use `request.state.store` (injected by `StoreContextMiddleware`).
#### Products (Public - No Auth)
```
GET /api/v1/shop/products → Product catalog (paginated)
GET /api/v1/shop/products/{id} → Product details
GET /api/v1/shop/products/search?q=... → Search products
GET /api/v1/storefront/products → Product catalog (paginated)
GET /api/v1/storefront/products/{id} → Product details
GET /api/v1/storefront/products/search?q=... → Search products
```
#### Cart (Public - Session Based)
```
GET /api/v1/shop/cart/{session_id} → Get cart
POST /api/v1/shop/cart/{session_id}/items → Add to cart
PUT /api/v1/shop/cart/{session_id}/items/{id} → Update item
DELETE /api/v1/shop/cart/{session_id}/items/{id} → Remove item
DELETE /api/v1/shop/cart/{session_id} → Clear cart
GET /api/v1/storefront/cart/{session_id} → Get cart
POST /api/v1/storefront/cart/{session_id}/items → Add to cart
PUT /api/v1/storefront/cart/{session_id}/items/{id} → Update item
DELETE /api/v1/storefront/cart/{session_id}/items/{id} → Remove item
DELETE /api/v1/storefront/cart/{session_id} → Clear cart
```
#### Orders (Authenticated - Customer)
```
POST /api/v1/shop/orders → Place order (auth required)
GET /api/v1/shop/orders → Order history (auth required)
GET /api/v1/shop/orders/{id} → Order details (auth required)
POST /api/v1/storefront/orders → Place order (auth required)
GET /api/v1/storefront/orders → Order history (auth required)
GET /api/v1/storefront/orders/{id} → Order details (auth required)
```
#### Authentication (Public)
```
POST /api/v1/shop/auth/register → Register customer
POST /api/v1/shop/auth/login → Customer login
POST /api/v1/shop/auth/logout → Customer logout
POST /api/v1/shop/auth/forgot-password → Request reset
POST /api/v1/shop/auth/reset-password → Reset password
POST /api/v1/storefront/auth/register → Register customer
POST /api/v1/storefront/auth/login → Customer login
POST /api/v1/storefront/auth/logout → Customer logout
POST /api/v1/storefront/auth/forgot-password → Request reset
POST /api/v1/storefront/auth/reset-password → Reset password
```
#### CMS Content (Public)
```
GET /api/v1/shop/content-pages/navigation → Navigation links
GET /api/v1/shop/content-pages/{slug} → Page content
GET /api/v1/storefront/content-pages/navigation → Navigation links
GET /api/v1/storefront/content-pages/{slug} → Page content
```
---
@@ -130,7 +130,7 @@ def endpoint_handler(request: Request, ...):
- **Public endpoints** (products, cart, CMS): No authentication
- **Authenticated endpoints** (orders): Use `get_current_customer_api` dependency
- **Cookie strategy**: Customer tokens stored at `path=/shop` only
- **Cookie strategy**: Customer tokens stored at `path=/storefront` only
### Error Handling
@@ -145,7 +145,7 @@ def endpoint_handler(request: Request, ...):
### Files Created ✨
```
app/api/v1/shop/
app/api/v1/storefront/
├── products.py (NEW - 182 lines)
├── cart.py (NEW - 242 lines)
├── orders.py (NEW - 193 lines)
@@ -156,9 +156,9 @@ app/api/v1/shop/
### Files Modified 🔧
```
app/exceptions/error_renderer.py → Added base_url calculation for shop context
app/exceptions/error_renderer.py → Added base_url calculation for storefront context
app/routes/store_pages.py → Added CMS route handler
app/templates/shop/errors/*.html → Fixed links to use base_url
app/templates/storefront/errors/*.html → Fixed links to use base_url
docs/architecture/
├── api-consolidation-proposal.md → Analysis & recommendation
└── api-migration-status.md → This file
@@ -170,27 +170,27 @@ docs/architecture/
### ✅ Phase 2: Frontend Migration (COMPLETE)
Updated all shop templates to use new API endpoints:
Updated all storefront templates to use new API endpoints:
| Template | Old Endpoint | New Endpoint | Status |
|----------|-------------|--------------|---------|
| `shop/account/login.html` | `/api/v1/platform/stores/${id}/customers/login` | `/api/v1/shop/auth/login` | ✅ Complete |
| `shop/account/register.html` | `/api/v1/platform/stores/${id}/customers/register` | `/api/v1/shop/auth/register` | ✅ Complete |
| `shop/product.html` | `/api/v1/platform/stores/${id}/products/${pid}` | `/api/v1/shop/products/${pid}` | ✅ Complete |
| `shop/product.html` | `/api/v1/platform/stores/${id}/products?limit=4` | `/api/v1/shop/products?limit=4` | ✅ Complete |
| `shop/product.html` | `/api/v1/platform/stores/${id}/cart/${sid}` | `/api/v1/shop/cart/${sid}` | ✅ Complete |
| `shop/product.html` | `/api/v1/platform/stores/${id}/cart/${sid}/items` | `/api/v1/shop/cart/${sid}/items` | ✅ Complete |
| `shop/cart.html` | `/api/v1/platform/stores/${id}/cart/${sid}` | `/api/v1/shop/cart/${sid}` | ✅ Complete |
| `shop/cart.html` | `/api/v1/platform/stores/${id}/cart/${sid}/items/${pid}` (PUT) | `/api/v1/shop/cart/${sid}/items/${pid}` | ✅ Complete |
| `shop/cart.html` | `/api/v1/platform/stores/${id}/cart/${sid}/items/${pid}` (DELETE) | `/api/v1/shop/cart/${sid}/items/${pid}` | ✅ Complete |
| `shop/products.html` | Already using `/api/v1/shop/products` | (No change needed) | ✅ Already Updated |
| `shop/home.html` | Already using `/api/v1/shop/products?featured=true` | (No change needed) | ✅ Already Updated |
| `storefront/account/login.html` | `/api/v1/platform/stores/${id}/customers/login` | `/api/v1/storefront/auth/login` | ✅ Complete |
| `storefront/account/register.html` | `/api/v1/platform/stores/${id}/customers/register` | `/api/v1/storefront/auth/register` | ✅ Complete |
| `storefront/product.html` | `/api/v1/platform/stores/${id}/products/${pid}` | `/api/v1/storefront/products/${pid}` | ✅ Complete |
| `storefront/product.html` | `/api/v1/platform/stores/${id}/products?limit=4` | `/api/v1/storefront/products?limit=4` | ✅ Complete |
| `storefront/product.html` | `/api/v1/platform/stores/${id}/cart/${sid}` | `/api/v1/storefront/cart/${sid}` | ✅ Complete |
| `storefront/product.html` | `/api/v1/platform/stores/${id}/cart/${sid}/items` | `/api/v1/storefront/cart/${sid}/items` | ✅ Complete |
| `storefront/cart.html` | `/api/v1/platform/stores/${id}/cart/${sid}` | `/api/v1/storefront/cart/${sid}` | ✅ Complete |
| `storefront/cart.html` | `/api/v1/platform/stores/${id}/cart/${sid}/items/${pid}` (PUT) | `/api/v1/storefront/cart/${sid}/items/${pid}` | ✅ Complete |
| `storefront/cart.html` | `/api/v1/platform/stores/${id}/cart/${sid}/items/${pid}` (DELETE) | `/api/v1/storefront/cart/${sid}/items/${pid}` | ✅ Complete |
| `storefront/products.html` | Already using `/api/v1/storefront/products` | (No change needed) | ✅ Already Updated |
| `storefront/home.html` | Already using `/api/v1/storefront/products?featured=true` | (No change needed) | ✅ Already Updated |
**Total Changes:** 9 API endpoint migrations across 3 template files
**Verification:**
```bash
grep -r "api/v1/public/stores" app/templates/shop --include="*.html"
grep -r "api/v1/public/stores" app/templates/storefront --include="*.html"
# Returns: (no results - all migrated)
```
@@ -199,16 +199,16 @@ grep -r "api/v1/public/stores" app/templates/shop --include="*.html"
Cleaned up old `/api/v1/platform/stores/*` endpoints:
**Files Removed:**
-`auth.py` - Migrated to `/api/v1/shop/auth.py`
-`products.py` - Migrated to `/api/v1/shop/products.py`
-`cart.py` - Migrated to `/api/v1/shop/cart.py`
-`orders.py` - Migrated to `/api/v1/shop/orders.py`
-`auth.py` - Migrated to `/api/v1/storefront/auth.py`
-`products.py` - Migrated to `/api/v1/storefront/products.py`
-`cart.py` - Migrated to `/api/v1/storefront/cart.py`
-`orders.py` - Migrated to `/api/v1/storefront/orders.py`
-`payments.py` - Empty placeholder (removed)
-`search.py` - Empty placeholder (removed)
-`shop.py` - Empty placeholder (removed)
-`storefront.py` - Empty placeholder (removed)
**Files Kept:**
-`stores.py` - Store lookup endpoints (truly public, not shop-specific)
-`stores.py` - Store lookup endpoints (truly public, not storefront-specific)
- `GET /api/v1/platform/stores/by-code/{store_code}`
- `GET /api/v1/platform/stores/by-subdomain/{subdomain}`
- `GET /api/v1/platform/stores/{store_id}/info`
@@ -216,7 +216,7 @@ Cleaned up old `/api/v1/platform/stores/*` endpoints:
**Updated:**
-`/app/api/v1/platform/__init__.py` - Now only includes store lookup endpoints
**Result:** Old shop endpoints completely removed, only store lookup remains in `/api/v1/platform/stores/*`
**Result:** Old storefront endpoints completely removed, only store lookup remains in `/api/v1/platform/stores/*`
### ⚠️ Phase 4: Deprecation Warnings (SKIPPED - Not Needed)
@@ -291,12 +291,12 @@ Old endpoint cleanup completed immediately (no gradual migration needed):
### After (New Pattern)
```
# Clean - store from context
/api/v1/shop/products
/api/v1/shop/products/456
/api/v1/shop/cart/abc-session-id
/api/v1/shop/cart/abc-session-id/items
/api/v1/shop/orders
/api/v1/shop/auth/login
/api/v1/storefront/products
/api/v1/storefront/products/456
/api/v1/storefront/cart/abc-session-id
/api/v1/storefront/cart/abc-session-id/items
/api/v1/storefront/orders
/api/v1/storefront/auth/login
```
**URL Reduction:** ~40% shorter URLs on average
@@ -308,7 +308,7 @@ Old endpoint cleanup completed immediately (no gradual migration needed):
### For Frontend Developers
- ✅ Cleaner, more intuitive URLs
- ✅ No need to track store_id in state
- ✅ Consistent API pattern across all shop endpoints
- ✅ Consistent API pattern across all storefront endpoints
- ✅ Automatic store context from middleware
### For Backend Developers
@@ -331,7 +331,7 @@ If issues arise, rollback is simple since old endpoints still exist:
1. **Revert frontend changes:**
```bash
git checkout app/templates/shop/*.html
git checkout app/templates/storefront/*.html
```
2. **Old endpoints still work:**
@@ -357,7 +357,7 @@ Add logging to track which pattern is being used:
logger.info(
"API call",
extra={
"endpoint_pattern": "new" if "/shop/" in request.url.path else "old",
"endpoint_pattern": "new" if "/storefront/" in request.url.path else "old",
"path": request.url.path,
"store_id": store.id if store else None,
}
@@ -377,7 +377,7 @@ logger.info(
### ✅ Decided
1. **Use `/api/v1/shop/*` pattern?** → YES (Option 1)
1. **Use `/api/v1/storefront/*` pattern?** → YES (Option 1)
2. **Store from middleware?** → YES
3. **Keep old endpoints during migration?** → YES
4. **Deprecation period?** → 2-4 weeks
@@ -412,7 +412,7 @@ logger.info(
Migration is considered successful when:
- [x] All new endpoints created and tested
- [x] Middleware updated to support shop API routes
- [x] Middleware updated to support storefront API routes
- [x] Store context extraction from Referer working
- [x] All frontend templates updated (9 API calls across 3 files)
- [x] Old endpoint usage drops to zero (verified with grep)
@@ -432,7 +432,7 @@ Migration is considered successful when:
- [Middleware Documentation](./middleware.md) - How middleware works
**Issues?** Review:
- Server logs: Check for `[SHOP_API]` log entries
- Server logs: Check for `[STOREFRONT_API]` log entries
- Browser console: Check for failed API calls
- Network tab: Verify correct endpoints are called

View File

@@ -7,7 +7,7 @@ Complete guide to the authentication and authorization system powering the multi
The platform uses a JWT-based authentication system combined with role-based access control (RBAC) to secure all interfaces:
- **Admin** interface
- **Store** dashboard
- **Shop** storefront
- **Storefront**
- **REST API** endpoints
## Authentication System
@@ -60,17 +60,17 @@ User.role: "super_admin" | "platform_admin" | "merchant_owner" | "store_member"
### Customer Role (Separate Model)
**Access**: Public shop and own account space
**Access**: Public storefront and own account space
**Capabilities**:
- Browse store shops
- Browse store storefronts
- Place orders
- Manage their own account and order history
- View order status
- Update profile information
- Can register directly from shop frontend
- Can register directly from storefront frontend
**Account Creation**: Self-registration via shop frontend (email verification required)
**Account Creation**: Self-registration via storefront frontend (email verification required)
**Authentication**: Standard JWT authentication (separate `Customer` model, not `User`)
@@ -89,7 +89,7 @@ User.role: "super_admin" | "platform_admin" | "merchant_owner" | "store_member"
- Manage products and inventory
- Process orders
- View analytics and reports
- Configure shop settings
- Configure storefront settings
- Manage team members (invite, remove, update roles)
- Access store-specific APIs
@@ -155,8 +155,8 @@ The platform has three distinct areas with different access requirements:
| Area | URL Pattern | Access | Purpose |
|------|-------------|--------|---------|
| **Admin** | `/admin/*` or `admin.platform.com` | Super admins and platform admins (`is_admin`) | Platform administration and store management |
| **Store** | `/store/*` | Merchant owners and store members (`is_store_user`) | Store dashboard and shop management |
| **Shop** | `/shop/*`, custom domains, subdomains | Customers and public | Public-facing eCommerce storefront |
| **Store** | `/store/*` | Merchant owners and store members (`is_store_user`) | Store dashboard and storefront management |
| **Storefront** | `/storefront/*`, custom domains, subdomains | Customers and public | Public-facing eCommerce storefront |
| **API** | `/api/*` | All authenticated users (role-based) | REST API for all operations |
## Account Registration Flow
@@ -174,7 +174,7 @@ The platform has three distinct areas with different access requirements:
- Activation: Upon clicking email verification link
### Customer Accounts
- ✅ Can register directly on store shop
- ✅ Can register directly on store storefront
- Activation: Upon clicking registration email link
- Used for: Shopping and order management
@@ -222,8 +222,8 @@ Allows access to customer users and admins.
**Usage**:
```python
@app.get("/shop/orders")
async def customer_orders(
@app.get("/storefront/orders")
async def storefront_orders(
current_user: User = Depends(auth_manager.require_customer)
):
return {"orders": [...]}
@@ -390,13 +390,13 @@ graph TD
D[Merchant Owner<br/>role=merchant_owner] --> E[Store Dashboard]
D --> F[Team Management]
D --> G[Shop Settings]
D --> G[Storefront Settings]
D --> H[All Store Permissions - 75]
I[Store Member<br/>role=store_member] --> E
I --> J[Role-Based Permissions]
K[Customer<br/>separate model] --> L[Shop Access]
K[Customer<br/>separate model] --> L[Storefront Access]
K --> M[Own Orders]
K --> N[Own Profile]
```

View File

@@ -68,7 +68,7 @@ The `FrontendDetector` uses the following priority order:
2. Path-based detection:
- /admin/* or /api/v1/admin/* → ADMIN
- /store/* or /api/v1/store/* → STORE
- /storefront/*, /shop/*, /stores/* → STOREFRONT
- /storefront/*, /stores/* → STOREFRONT
- /api/v1/platform/* → PLATFORM
3. Store subdomain (orion.omsflow.lu) → STOREFRONT
4. Store context set by middleware → STOREFRONT
@@ -88,8 +88,8 @@ STORE_PATH_PREFIXES = ("/store/", "/api/v1/store")
STOREFRONT_PATH_PREFIXES = (
"/storefront",
"/api/v1/storefront",
"/shop", # Legacy support
"/api/v1/shop", # Legacy support
"/shop", # Legacy support (redirects to /storefront)
"/api/v1/shop", # Legacy support (redirects to /api/v1/storefront)
"/stores/", # Path-based store access
)

View File

@@ -186,7 +186,7 @@ function languageSelector(currentLang, enabledLanguages) {
### Rule FE-003: Language Selector Must Be In Shared JavaScript
**Language selector function MUST be defined in:**
- `static/shop/js/shop-layout.js` for storefront
- `static/storefront/js/storefront-layout.js` for storefront
- `static/store/js/init-alpine.js` for store dashboard
```javascript
@@ -444,7 +444,7 @@ static/locales/
"title": "Tableau de bord"
}
},
"shop": {
"storefront": {
"cart": {
"empty": "Votre panier est vide"
}
@@ -466,7 +466,7 @@ static/locales/
### Language Selector Implementation Checklist
- [ ] Function defined in appropriate JS file (`shop-layout.js` or `init-alpine.js`)
- [ ] Function defined in appropriate JS file (`storefront-layout.js` or `init-alpine.js`)
- [ ] Function exported to `window.languageSelector`
- [ ] Uses `tojson|safe` filter for language array
- [ ] Provides default values for both parameters
@@ -492,14 +492,14 @@ static/locales/
| File | Type | Notes |
|------|------|-------|
| `static/shop/js/shop-layout.js` | JS | `languageSelector()` for storefront |
| `static/storefront/js/storefront-layout.js` | JS | `languageSelector()` for storefront |
| `app/modules/core/static/store/js/init-alpine.js` | JS | `languageSelector()` for store dashboard |
| `app/modules/core/static/admin/js/init-alpine.js` | JS | `languageSelector()` for admin |
| `app/modules/core/static/merchant/js/init-alpine.js` | JS | `languageSelector()` for merchant |
| `app/templates/store/partials/header.html` | Template | Store dashboard language selector |
| `app/templates/admin/partials/header.html` | Template | Admin language selector |
| `app/templates/merchant/partials/header.html` | Template | Merchant language selector |
| `app/templates/shop/base.html` | Template | Storefront language selector |
| `app/templates/storefront/base.html` | Template | Storefront language selector |
| `app/modules/core/routes/api/platform.py` | API | Language endpoints (`/api/v1/platform/language/*`) |
| `middleware/language.py` | Middleware | Language detection per frontend type |
| `static/locales/*.json` | JSON | Translation files |

View File

@@ -8,7 +8,7 @@ The application uses a custom middleware stack that processes **every request**
- REST API calls (`/api/*`)
- Admin interface pages (`/admin/*`)
- Store dashboard pages (`/store/*`)
- Shop pages (`/shop/*` or custom domains)
- Storefront pages (`/storefront/*` or custom domains)
This middleware layer is **system-wide** and enables the multi-tenant architecture to function seamlessly.
@@ -89,7 +89,7 @@ INFO Response: 200 for GET /admin/dashboard (0.143s)
### 2. Store Context Middleware
**Purpose**: Detect which store's shop the request is for (multi-tenant core)
**Purpose**: Detect which store's storefront the request is for (multi-tenant core)
**What it does**:
- Detects store from:
@@ -103,7 +103,7 @@ INFO Response: 200 for GET /admin/dashboard (0.143s)
**Example**:
```
Request: https://orion.platform.com/shop/products
Request: https://orion.platform.com/storefront/products
Middleware detects: store_code = "orion"
@@ -111,14 +111,14 @@ Queries database: SELECT * FROM stores WHERE code = 'orion'
Injects: request.state.store = <Store object>
request.state.store_id = 1
request.state.clean_path = "/shop/products"
request.state.clean_path = "/storefront/products"
```
**Why it's critical**: Without this, the system wouldn't know which store's data to show
**See**: [Multi-Tenant System](multi-tenant.md) for routing modes
**Note on Path-Based Routing:** Previous implementations used a `PathRewriteMiddleware` to rewrite paths at runtime. This has been replaced with **double router mounting** in `main.py`, where shop routes are registered twice with different prefixes (`/shop` and `/stores/{store_code}/shop`). This approach is simpler and uses FastAPI's native routing capabilities.
**Note on Path-Based Routing:** Previous implementations used a `PathRewriteMiddleware` to rewrite paths at runtime. This has been replaced with **double router mounting** in `main.py`, where storefront routes are registered twice with different prefixes (`/storefront` and `/stores/{store_code}/storefront`). This approach is simpler and uses FastAPI's native routing capabilities.
### 3. Frontend Type Detection Middleware
@@ -139,7 +139,7 @@ Injects: request.state.store = <Store object>
2. Path-based detection:
- /admin/* or /api/v1/admin/* ADMIN
- /store/* or /api/v1/store/* STORE
- /storefront/*, /shop/*, /stores/* STOREFRONT
- /storefront/*, /stores/* STOREFRONT
- /api/v1/platform/* PLATFORM
3. Store subdomain (orion.omsflow.lu) STOREFRONT
4. Store context set by middleware STOREFRONT
@@ -171,7 +171,7 @@ Injects: request.state.store = <Store object>
}
```
**Why it's needed**: Each store shop can have custom branding
**Why it's needed**: Each store storefront can have custom branding
## Naming Conventions
@@ -311,7 +311,7 @@ graph TD
**Breaking this order will break the application!**
**Note:** Path-based routing (e.g., `/stores/{code}/shop/*`) is handled by double router mounting in `main.py`, not by middleware. Platform path-based routing (e.g., `/platforms/oms/`) IS handled by PlatformContextMiddleware which rewrites the path.
**Note:** Path-based routing (e.g., `/stores/{code}/storefront/*`) is handled by double router mounting in `main.py`, not by middleware. Platform path-based routing (e.g., `/platforms/oms/`) IS handled by PlatformContextMiddleware which rewrites the path.
## Request State Variables
@@ -332,7 +332,7 @@ Middleware components inject these variables into `request.state`:
```python
from fastapi import Request
@app.get("/shop/products")
@app.get("/storefront/products")
async def get_products(request: Request):
# Access store
store = request.state.store
@@ -374,26 +374,26 @@ async def get_products(request: Request):
## Request Flow Example
### Example: Shop Product Page Request
### Example: Storefront Product Page Request
**URL**: `https://orion.myplatform.com/shop/products`
**URL**: `https://orion.myplatform.com/storefront/products`
**Middleware Processing**:
```
1. LoggingMiddleware
↓ Starts timer
↓ Logs: "Request: GET /shop/products from 192.168.1.100"
↓ Logs: "Request: GET /storefront/products from 192.168.1.100"
2. StoreContextMiddleware
↓ Detects subdomain: "orion"
↓ Queries DB: store = get_store_by_code("orion")
↓ Sets: request.state.store = <Store: Orion>
↓ Sets: request.state.store_id = 1
↓ Sets: request.state.clean_path = "/shop/products"
↓ Sets: request.state.clean_path = "/storefront/products"
3. FrontendTypeMiddleware
↓ Uses FrontendDetector with path: "/shop/products"
↓ Uses FrontendDetector with path: "/storefront/products"
↓ Has store context: Yes
↓ Detects storefront frontend
↓ Sets: request.state.frontend_type = FrontendType.STOREFRONT
@@ -403,7 +403,7 @@ async def get_products(request: Request):
↓ Sets: request.state.theme = {...theme config...}
5. FastAPI Router
↓ Matches route: @app.get("/shop/products")
↓ Matches route: @app.get("/storefront/products")
↓ Calls handler function
6. Route Handler
@@ -415,7 +415,7 @@ async def get_products(request: Request):
↓ Returns HTML with store theme
9. LoggingMiddleware (response phase)
↓ Logs: "Response: 200 for GET /shop/products (0.143s)"
↓ Logs: "Response: 200 for GET /storefront/products (0.143s)"
↓ Adds header: X-Process-Time: 0.143
```
@@ -510,9 +510,9 @@ def test_store_detection_subdomain():
Test the full middleware stack:
```python
def test_shop_request_flow(client):
def test_storefront_request_flow(client):
response = client.get(
"/shop/products",
"/storefront/products",
headers={"Host": "orion.platform.com"}
)

View File

@@ -4,14 +4,14 @@ Complete guide to the multi-tenant architecture supporting custom domains, subdo
## Overview
The Orion platform supports **three deployment modes** for multi-tenancy, allowing each store to have their own isolated shop while sharing the same application instance and database.
The Orion platform supports **three deployment modes** for multi-tenancy, allowing each store to have their own isolated storefront while sharing the same application instance and database.
**Key Concept**: One application, multiple isolated store shops, each accessible via different URLs.
**Key Concept**: One application, multiple isolated store storefronts, each accessible via different URLs.
**Important Distinction:**
- **Store Dashboard** (all modes): `/store/{code}/*` (singular) - Management interface for stores
- **Shop Storefront** (path-based only): `/stores/{code}/shop/*` (plural) - Customer-facing shop
- This naming distinction helps separate administrative routes from public-facing shop routes
- **Storefront** (path-based only): `/storefront/{code}/*` - Customer-facing storefront
- This naming distinction helps separate administrative routes from public-facing storefront routes
## The Three Routing Modes
@@ -21,16 +21,16 @@ The Orion platform supports **three deployment modes** for multi-tenancy, allowi
**Example**:
```
customdomain1.com → Store 1 Shop
anothershop.com → Store 2 Shop
beststore.net → Store 3 Shop
customdomain1.com → Store 1 Storefront
anothershop.com → Store 2 Storefront
beststore.net → Store 3 Storefront
```
**How it works**:
1. Store registers a custom domain
2. Domain's DNS is configured to point to the platform
3. Platform detects store by matching domain in database
4. Store's shop is displayed with their theme/branding
4. Store's storefront is displayed with their theme/branding
**Use Case**: Professional stores who want their own branded domain
@@ -50,9 +50,9 @@ store_id | domain
**Example**:
```
store1.platform.com → Store 1 Shop
store2.platform.com → Store 2 Shop
store3.platform.com → Store 3 Shop
store1.platform.com → Store 1 Storefront
store2.platform.com → Store 2 Storefront
store3.platform.com → Store 3 Storefront
admin.platform.com → Admin Interface
```
@@ -69,9 +69,9 @@ admin.platform.com → Admin Interface
# Stores table
id | code | name
---|---------|----------
1 | store1 | Store One Shop
2 | store2 | Store Two Shop
3 | store3 | Store Three Shop
1 | store1 | Store One Storefront
2 | store2 | Store Two Storefront
3 | store3 | Store Three Storefront
```
### 3. Path-Based Mode
@@ -80,9 +80,9 @@ id | code | name
**Example**:
```
platform.com/stores/store1/shop → Store 1 Shop
platform.com/stores/store2/shop → Store 2 Shop
platform.com/stores/store3/shop → Store 3 Shop
platform.com/storefront/store1 → Store 1 Storefront
platform.com/storefront/store2 → Store 2 Storefront
platform.com/storefront/store3 → Store 3 Storefront
```
**How it works**:
@@ -94,7 +94,7 @@ platform.com/stores/store3/shop → Store 3 Shop
**Use Case**: Development and testing environments only
**Path Patterns**:
- `/stores/{code}/shop/*` - Storefront pages (correct pattern)
- `/storefront/{code}/*` - Storefront pages (correct pattern)
- `/store/{code}/*` - Store dashboard pages (different context)
## Routing Mode Comparison
@@ -107,7 +107,7 @@ platform.com/stores/store3/shop → Store 3 Shop
| **SEO Benefits** | Best (own domain) | Good | Limited |
| **Cost** | High (domain + SSL) | Low (wildcard SSL) | Lowest |
| **Isolation** | Best (separate domain) | Good | Good |
| **URL Appearance** | `shop.com` | `shop.platform.com` | `platform.com/store/shop` |
| **URL Appearance** | `shop.com` | `shop.platform.com` | `platform.com/storefront/store` |
## Implementation Details
@@ -133,7 +133,7 @@ def detect_store(request):
# 3. Try path-based
path = request.url.path
if path.startswith("/store/") or path.startswith("/stores/"):
if path.startswith("/store/") or path.startswith("/storefront/"):
store_code = extract_code_from_path(path)
store = find_by_code(store_code)
if store:
@@ -146,11 +146,11 @@ def detect_store(request):
For path-based routing, clean paths are extracted:
**Path-Based Shop Routes** (Development):
**Path-Based Storefront Routes** (Development):
```
Original: /stores/ORION/shop/products
Original: /storefront/ORION/products
Extracted: store_code = "ORION"
Clean: /shop/products
Clean: /storefront/products
```
**Store Dashboard Routes** (All environments):
@@ -160,11 +160,11 @@ Extracted: store_code = "ORION"
Clean: /dashboard
```
**Note**: The shop storefront uses `/stores/` (plural) while the store dashboard uses `/store/` (singular). This distinction helps separate customer-facing shop routes from store management routes.
**Note**: The storefront uses `/storefront/` while the store dashboard uses `/store/` (singular). This distinction helps separate customer-facing storefront routes from store management routes.
**Why Clean Path?**
- FastAPI routes don't include store prefix
- Routes defined as: `@app.get("/shop/products")`
- Routes defined as: `@app.get("/storefront/products")`
- Path must be rewritten to match routes
## Database Schema
@@ -197,7 +197,7 @@ CREATE TABLE store_domains (
```sql
-- Stores
INSERT INTO stores (code, name) VALUES
('orion', 'Orion Shop'),
('orion', 'Orion Storefront'),
('techstore', 'Tech Store'),
('fashionhub', 'Fashion Hub');
@@ -220,9 +220,9 @@ INSERT INTO store_domains (store_id, domain) VALUES
**URLs**:
```
myplatform.com/admin
myplatform.com/stores/shop1/shop
myplatform.com/stores/shop2/shop
myplatform.com/stores/shop3/shop
myplatform.com/storefront/shop1
myplatform.com/storefront/shop2
myplatform.com/storefront/shop3
```
**Infrastructure**:
@@ -272,8 +272,8 @@ shop3.myplatform.com → Store 3
shop4.myplatform.com → Store 4
# Path-based (free tier)
myplatform.com/stores/shop5/shop → Store 5
myplatform.com/stores/shop6/shop → Store 6
myplatform.com/storefront/shop5 → Store 5
myplatform.com/storefront/shop6 → Store 6
```
**Infrastructure**:
@@ -391,7 +391,7 @@ static/
**Request**:
```http
GET /shop/products HTTP/1.1
GET /storefront/products HTTP/1.1
Host: customdomain.com
```
@@ -404,8 +404,8 @@ Host: customdomain.com
- Sets: request.state.store = <Store 1>
2. ContextDetectionMiddleware
- Analyzes: path = "/shop/products"
- Sets: context_type = SHOP
- Analyzes: path = "/storefront/products"
- Sets: context_type = STOREFRONT
3. ThemeContextMiddleware
- Queries: store_themes WHERE store_id = 1
@@ -420,7 +420,7 @@ Host: customdomain.com
**Request**:
```http
GET /shop/products HTTP/1.1
GET /storefront/products HTTP/1.1
Host: orion.myplatform.com
```
@@ -439,22 +439,22 @@ Host: orion.myplatform.com
**Request**:
```http
GET /stores/ORION/shop/products HTTP/1.1
GET /storefront/ORION/products HTTP/1.1
Host: myplatform.com
```
**Processing**:
```
1. StoreContextMiddleware
- Checks: path starts with "/store/"
- Checks: path starts with "/storefront/"
- Extracts: code = "ORION"
- Queries: stores WHERE code = "ORION"
- Sets: request.state.store = <Store>
- Sets: request.state.clean_path = "/shop/products"
- Sets: request.state.clean_path = "/storefront/products"
2. FastAPI Router
- Routes registered with prefix: /stores/{store_code}/shop
- Matches: /stores/ORION/shop/products
- Routes registered with prefix: /storefront/{store_code}
- Matches: /storefront/ORION/products
- store_code path parameter = "ORION"
3-4. Same as previous examples (Context, Theme middleware)
@@ -487,17 +487,17 @@ def test_store_detection_subdomain():
### Integration Tests
```python
def test_shop_page_multi_tenant(client):
def test_storefront_page_multi_tenant(client):
# Test subdomain routing
response = client.get(
"/shop/products",
"/storefront/products",
headers={"Host": "orion.platform.com"}
)
assert "Orion" in response.text
# Test different store
response = client.get(
"/shop/products",
"/storefront/products",
headers={"Host": "techstore.platform.com"}
)
assert "Tech Store" in response.text

View File

@@ -49,8 +49,8 @@ admin.platform.com → Admin Interface
#### Path-Based Mode (Development Only)
```
platform.com/stores/store1/shop → Store 1 Shop
platform.com/stores/store2/shop → Store 2 Shop
platform.com/storefront/store1 → Store 1 Shop
platform.com/storefront/store2 → Store 2 Shop
platform.com/admin → Admin Interface
```
@@ -124,7 +124,7 @@ graph TB
F -->|API /api/*| G[API Router]
F -->|Admin /admin/*| H[Admin Page Router]
F -->|Store /store/*| I[Store Page Router]
F -->|Shop /shop/*| J[Shop Page Router]
F -->|Shop /storefront/*| J[Shop Page Router]
G --> K[JSON Response]
H --> L[Admin HTML]
@@ -163,7 +163,7 @@ graph TB
**Access**: Store users (owners and team members)
### Shop Interface (`/shop/*` or custom domains)
### Shop Interface (`/storefront/*` or custom domains)
**Purpose**: Customer-facing storefront

View File

@@ -42,7 +42,7 @@ graph TB
```http
# Shop page request (subdomain mode)
GET https://orion.platform.com/shop/products
GET https://orion.platform.com/storefront/products
Host: orion.platform.com
# API request
@@ -69,7 +69,7 @@ Host: platform.com
start_time = time.time()
# Log entry
logger.info(f"Request: GET /shop/products from 192.168.1.100")
logger.info(f"Request: GET /storefront/products from 192.168.1.100")
```
**Output**: Nothing added to `request.state` yet
@@ -87,7 +87,7 @@ logger.info(f"Request: GET /shop/products from 192.168.1.100")
```python
# Input
host = "orion.platform.com"
path = "/shop/products"
path = "/storefront/products"
# Detection logic
if host != settings.platform_domain:
@@ -102,14 +102,14 @@ if host != settings.platform_domain:
# Set request state
request.state.store = store
request.state.store_id = store.id
request.state.clean_path = "/shop/products" # Already clean
request.state.clean_path = "/storefront/products" # Already clean
```
**Request State After**:
```python
request.state.store = <Store: Orion>
request.state.store_id = 1
request.state.clean_path = "/shop/products"
request.state.clean_path = "/storefront/products"
```
### 4. Router Matching (FastAPI Native)
@@ -117,18 +117,18 @@ request.state.clean_path = "/shop/products"
**What happens**:
- FastAPI matches the request path against registered routes
- For path-based development mode, routes are registered with two prefixes:
- `/shop/*` for subdomain/custom domain
- `/stores/{store_code}/shop/*` for path-based development
- `/storefront/*` for subdomain/custom domain
- `/stores/{store_code}/storefront/*` for path-based development
**Example** (Path-Based Mode):
```python
# In main.py - Double router mounting
app.include_router(shop_pages.router, prefix="/shop")
app.include_router(shop_pages.router, prefix="/stores/{store_code}/shop")
app.include_router(storefront_pages.router, prefix="/storefront")
app.include_router(storefront_pages.router, prefix="/stores/{store_code}/storefront")
# Request: /stores/ORION/shop/products
# Matches: Second router (/stores/{store_code}/shop)
# Request: /stores/ORION/storefront/products
# Matches: Second router (/stores/{store_code}/storefront)
# Route: @router.get("/products")
# store_code available as path parameter = "ORION"
```
@@ -148,7 +148,7 @@ app.include_router(shop_pages.router, prefix="/stores/{store_code}/shop")
```python
host = request.headers.get("host", "")
path = request.state.clean_path # "/shop/products"
path = request.state.clean_path # "/storefront/products"
has_store = hasattr(request.state, 'store') and request.state.store
# FrontendDetector handles all detection logic centrally
@@ -210,11 +210,11 @@ request.state.theme = {
**Route Matching**:
```python
# Request path (after rewrite): "/shop/products"
# Request path (after rewrite): "/storefront/products"
# Matches this route
@app.get("/shop/products")
async def get_shop_products(request: Request):
@app.get("/storefront/products")
async def get_storefront_products(request: Request):
# Handler code
pass
```
@@ -254,7 +254,7 @@ async def shop_products_page(
### 9. Template Rendering (Jinja2)
**Template** (`templates/shop/products.html`):
**Template** (`templates/storefront/products.html`):
```jinja2
<!DOCTYPE html>
@@ -322,7 +322,7 @@ async def shop_products_page(
duration = time.time() - start_time # 0.143 seconds
logger.info(
f"Response: 200 for GET /shop/products (0.143s)"
f"Response: 200 for GET /storefront/products (0.143s)"
)
# Add header
@@ -397,7 +397,7 @@ sequenceDiagram
```mermaid
sequenceDiagram
participant Client
participant Client
participant Logging
participant Store
participant Path
@@ -412,7 +412,7 @@ sequenceDiagram
Logging->>Store: Pass request
Store->>DB: Query store by subdomain
DB-->>Store: Store object
Note over Store: Set store, store_id, clean_path
Note over Store: Set store, store_id, clean_path
Store->>Path: Pass request
Note over Path: Path already clean
Path->>Context: Pass request
@@ -420,7 +420,7 @@ sequenceDiagram
Note over Context: frontend_type = STOREFRONT
Context->>Theme: Pass request
Theme->>DB: Query theme
DB-->>Theme: Theme config
DB-->>Theme: Theme config
Note over Theme: Set theme in request.state
Theme->>Router: Route request
Router->>Handler: Call handler
@@ -431,7 +431,7 @@ sequenceDiagram
```
## Request State Timeline
Showing how `request.state` is built up through the middleware stack:
```
@@ -445,14 +445,14 @@ After StoreContextMiddleware:
}
After FrontendTypeMiddleware:
{
{
store: <Store: Orion>,
store_id: 1,
clean_path: "/storefront/products",
frontend_type: FrontendType.STOREFRONT
}
After ThemeContextMiddleware:
After ThemeContextMiddleware:
{
store: <Store: Orion>,
store_id: 1,
@@ -460,7 +460,7 @@ After ThemeContextMiddleware:
frontend_type: FrontendType.STOREFRONT,
theme: {
primary_color: "#3B82F6",
secondary_color: "#10B981",
secondary_color: "#10B981",
logo_url: "/static/stores/orion/logo.png",
custom_css: "..."
}

View File

@@ -56,7 +56,7 @@ CREATE TABLE store_themes (
store_id INTEGER UNIQUE NOT NULL REFERENCES stores(id) ON DELETE CASCADE,
theme_name VARCHAR(100) DEFAULT 'default',
is_active BOOLEAN DEFAULT TRUE,
-- Colors (JSON)
colors JSONB DEFAULT '{
"primary": "#6366f1",
@@ -66,30 +66,30 @@ CREATE TABLE store_themes (
"text": "#1f2937",
"border": "#e5e7eb"
}'::jsonb,
-- Typography
font_family_heading VARCHAR(100) DEFAULT 'Inter, sans-serif',
font_family_body VARCHAR(100) DEFAULT 'Inter, sans-serif',
-- Branding
logo_url VARCHAR(500),
logo_dark_url VARCHAR(500),
favicon_url VARCHAR(500),
banner_url VARCHAR(500),
-- Layout
layout_style VARCHAR(50) DEFAULT 'grid',
header_style VARCHAR(50) DEFAULT 'fixed',
product_card_style VARCHAR(50) DEFAULT 'modern',
-- Customization
custom_css TEXT,
social_links JSONB DEFAULT '{}'::jsonb,
-- Meta
meta_title_template VARCHAR(200),
meta_description TEXT,
-- Timestamps
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
@@ -122,7 +122,7 @@ from sqlalchemy.orm import relationship
class Store(Base):
# ... existing fields ...
# Add theme relationship
theme = relationship(
"StoreTheme",
@@ -130,7 +130,7 @@ class Store(Base):
uselist=False, # One-to-one relationship
cascade="all, delete-orphan"
)
@property
def active_theme(self):
"""Get store's active theme or return None"""
@@ -161,7 +161,7 @@ app.middleware("http")(theme_context_middleware)
### Step 5: Create Shop Base Template
File: `app/templates/shop/base.html`
File: `app/templates/storefront/base.html`
See complete template in `/home/claude/shop_base_template.html`
@@ -184,7 +184,7 @@ See complete template in `/home/claude/shop_base_template.html`
### Step 6: Create Shop Layout JavaScript
File: `static/shop/js/shop-layout.js`
File: `static/storefront/js/shop-layout.js`
See complete code in `/home/claude/shop_layout.js`
@@ -208,13 +208,13 @@ from middleware.theme_context import get_current_theme
async def shop_home(request: Request, db: Session = Depends(get_db)):
store = request.state.store
theme = get_current_theme(request) # or request.state.theme
# Get products for store
products = db.query(Product).filter(
Product.store_id == store.id,
Product.is_active == True
).all()
return templates.TemplateResponse("shop/home.html", {
"request": request,
"store": store,
@@ -308,7 +308,7 @@ theme = {
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.product-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 12px rgba(0, 0, 0, 0.15);
@@ -340,7 +340,7 @@ THEME_PRESETS = {
"header": "fixed"
}
},
"classic": {
"colors": {
"primary": "#1e40af",
@@ -356,7 +356,7 @@ THEME_PRESETS = {
"header": "static"
}
},
"minimal": {
"colors": {
"primary": "#000000",
@@ -372,7 +372,7 @@ THEME_PRESETS = {
"header": "transparent"
}
},
"vibrant": {
"colors": {
"primary": "#f59e0b",
@@ -394,16 +394,16 @@ def apply_preset(theme: StoreTheme, preset_name: str):
"""Apply a preset to a store theme"""
if preset_name not in THEME_PRESETS:
raise ValueError(f"Unknown preset: {preset_name}")
preset = THEME_PRESETS[preset_name]
theme.theme_name = preset_name
theme.colors = preset["colors"]
theme.font_family_heading = preset["fonts"]["heading"]
theme.font_family_body = preset["fonts"]["body"]
theme.layout_style = preset["layout"]["style"]
theme.header_style = preset["layout"]["header"]
return theme
```
@@ -420,11 +420,11 @@ def get_store_theme(store_id: int, db: Session = Depends(get_db)):
theme = db.query(StoreTheme).filter(
StoreTheme.store_id == store_id
).first()
if not theme:
# Return default theme
return get_default_theme()
return theme.to_dict()
@@ -435,38 +435,38 @@ def update_store_theme(
db: Session = Depends(get_db)
):
"""Update or create theme for store"""
theme = db.query(StoreTheme).filter(
StoreTheme.store_id == store_id
).first()
if not theme:
theme = StoreTheme(store_id=store_id)
db.add(theme)
# Update fields
if "colors" in theme_data:
theme.colors = theme_data["colors"]
if "fonts" in theme_data:
theme.font_family_heading = theme_data["fonts"].get("heading")
theme.font_family_body = theme_data["fonts"].get("body")
if "branding" in theme_data:
theme.logo_url = theme_data["branding"].get("logo")
theme.logo_dark_url = theme_data["branding"].get("logo_dark")
theme.favicon_url = theme_data["branding"].get("favicon")
if "layout" in theme_data:
theme.layout_style = theme_data["layout"].get("style")
theme.header_style = theme_data["layout"].get("header")
if "custom_css" in theme_data:
theme.custom_css = theme_data["custom_css"]
db.commit()
db.refresh(theme)
return theme.to_dict()
@@ -478,22 +478,22 @@ def apply_theme_preset(
):
"""Apply a preset theme to store"""
from app.core.theme_presets import apply_preset, THEME_PRESETS
if preset_name not in THEME_PRESETS:
raise HTTPException(400, f"Unknown preset: {preset_name}")
theme = db.query(StoreTheme).filter(
StoreTheme.store_id == store_id
).first()
if not theme:
theme = StoreTheme(store_id=store_id)
db.add(theme)
apply_preset(theme, preset_name)
db.commit()
db.refresh(theme)
return {
"message": f"Applied {preset_name} preset",
"theme": theme.to_dict()
@@ -636,7 +636,7 @@ Create a marketplace of premium themes:
```python
class PremiumTheme(Base):
__tablename__ = "premium_themes"
id = Column(Integer, primary_key=True)
name = Column(String(100))
description = Column(Text)
@@ -650,7 +650,7 @@ Respect user's system preferences:
```javascript
// Detect system dark mode preference
if (window.matchMedia &&
if (window.matchMedia &&
window.matchMedia('(prefers-color-scheme: dark)').matches) {
this.dark = true;
}
@@ -662,7 +662,7 @@ Track which themes perform best:
```python
class ThemeAnalytics(Base):
__tablename__ = "theme_analytics"
theme_id = Column(Integer, ForeignKey("store_themes.id"))
conversion_rate = Column(Numeric(5, 2))
avg_session_duration = Column(Integer)

View File

@@ -42,6 +42,61 @@ http://localhost:8000/platforms/loyalty/storefront/TECHPRO/cart
---
## Development URL Quick Reference
All development URLs use `http://localhost:8000` as the base.
### Login Pages
| Panel | URL | Description |
|-------|-----|-------------|
| Admin | `/admin/login` | Platform-wide admin panel |
| Merchant | `/merchants/login` | Merchant management panel |
| Store Dashboard | `/platforms/{platform_code}/store/{store_code}/login` | Store staff login |
| Storefront | `/platforms/{platform_code}/storefront/{store_code}/account/login` | Customer login |
### Key Entry Points
| Panel | URL | Description |
|-------|-----|-------------|
| Admin Dashboard | `/admin/` | Admin panel home |
| Merchant Dashboard | `/merchants/dashboard` | Merchant panel home |
| Store Dashboard | `/platforms/{platform_code}/store/{store_code}/dashboard` | Store management |
| Storefront Homepage | `/platforms/{platform_code}/storefront/{store_code}/` | Customer-facing store |
| Platform Homepage | `/platforms/{platform_code}/` | Platform marketing site |
### Full Example (OMS Platform, Store Code "ACME")
```
Admin login: http://localhost:8000/admin/login
Merchant login: http://localhost:8000/merchants/login
Store login: http://localhost:8000/platforms/oms/store/ACME/login
Store dashboard: http://localhost:8000/platforms/oms/store/ACME/dashboard
Storefront login: http://localhost:8000/platforms/oms/storefront/ACME/account/login
Storefront homepage: http://localhost:8000/platforms/oms/storefront/ACME/
Storefront products: http://localhost:8000/platforms/oms/storefront/ACME/products
Storefront cart: http://localhost:8000/platforms/oms/storefront/ACME/cart
Storefront checkout: http://localhost:8000/platforms/oms/storefront/ACME/checkout
Storefront account: http://localhost:8000/platforms/oms/storefront/ACME/account/dashboard
```
### API Endpoints
```
Admin API: http://localhost:8000/api/v1/admin/...
Store API: http://localhost:8000/api/v1/store/...
Storefront API: http://localhost:8000/api/v1/storefront/...
```
### Notes
- **Admin and Merchant** panels are global — no platform prefix needed.
- **Store and Storefront** panels require the `/platforms/{platform_code}/` prefix in development. This prefix is stripped by `PlatformContextMiddleware` before routing.
- In **production**, storefronts are accessed via subdomain (`acme.omsflow.lu/`) or custom domain. The root path `/` is the storefront.
- The storefront router is **double-mounted** at `/storefront/` and `/storefront/{store_code}/` to support both production and development modes transparently.
---
## Multi-Platform URL Routing
Orion supports multiple platforms (OMS, Loyalty, Site Builder), each with its own marketing site and store ecosystem.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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