Files
orion/app/routes/vendor_pages.py
Samir Boulahtit 5a9f44f3d1 Complete shop API consolidation to /api/v1/shop/* with middleware-based vendor context
## API Migration (Complete)

### New Shop API Endpoints Created
- **Products API** (app/api/v1/shop/products.py)
  - GET /api/v1/shop/products - Product catalog with pagination/search/filters
  - GET /api/v1/shop/products/{id} - Product details

- **Cart API** (app/api/v1/shop/cart.py)
  - 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 quantity
  - DELETE /api/v1/shop/cart/{session_id}/items/{product_id} - Remove item
  - DELETE /api/v1/shop/cart/{session_id} - Clear cart

- **Orders API** (app/api/v1/shop/orders.py)
  - POST /api/v1/shop/orders - Place order (authenticated)
  - GET /api/v1/shop/orders - Order history (authenticated)
  - GET /api/v1/shop/orders/{id} - Order details (authenticated)

- **Auth API** (app/api/v1/shop/auth.py)
  - POST /api/v1/shop/auth/register - Customer registration
  - POST /api/v1/shop/auth/login - Customer login (sets cookie at path=/shop)
  - POST /api/v1/shop/auth/logout - Customer logout
  - POST /api/v1/shop/auth/forgot-password - Password reset request
  - POST /api/v1/shop/auth/reset-password - Password reset

**Total: 18 new shop API endpoints**

### Middleware Enhancement
Updated VendorContextMiddleware (middleware/vendor_context.py):
- Added is_shop_api_request() to detect /api/v1/shop/* routes
- Added extract_vendor_from_referer() to extract vendor from Referer header
  - Supports path-based: /vendors/wizamart/shop/* → wizamart
  - Supports subdomain: wizamart.platform.com → wizamart
  - Supports custom domain: customshop.com → customshop.com
- Modified dispatch() to handle shop API specially (no longer skips)
- Vendor context now injected into request.state.vendor for shop API calls

### Frontend Migration (Complete)
Updated all shop templates to use new API endpoints:
- app/templates/shop/account/login.html - Updated login endpoint
- app/templates/shop/account/register.html - Updated register endpoint
- app/templates/shop/product.html - Updated 4 API calls (products, cart)
- app/templates/shop/cart.html - Updated 3 API calls (get, update, delete)
- app/templates/shop/products.html - Activated product loading from API

**Total: 9 API endpoint migrations across 5 templates**

### Old Endpoint Cleanup (Complete)
Removed deprecated /api/v1/public/vendors/* shop endpoints:
- Deleted app/api/v1/public/vendors/auth.py
- Deleted app/api/v1/public/vendors/products.py
- Deleted app/api/v1/public/vendors/cart.py
- Deleted app/api/v1/public/vendors/orders.py
- Deleted app/api/v1/public/vendors/payments.py (empty)
- Deleted app/api/v1/public/vendors/search.py (empty)
- Deleted app/api/v1/public/vendors/shop.py (empty)

Updated app/api/v1/public/__init__.py to only include vendor lookup endpoints:
- GET /api/v1/public/vendors/by-code/{code}
- GET /api/v1/public/vendors/by-subdomain/{subdomain}
- GET /api/v1/public/vendors/{id}/info

**Result: Only 3 truly public endpoints remain**

### Error Page Improvements
Updated all shop error templates to use base_url:
- app/templates/shop/errors/*.html (10 files)
- Updated error_renderer.py to calculate base_url from vendor context
- Links now work correctly for path-based, subdomain, and custom domain access

### CMS Route Handler
Added catch-all CMS route to app/routes/vendor_pages.py:
- Handles /{vendor_code}/{slug} for content pages
- Uses content_page_service for two-tier lookup (vendor override → platform default)

### Template Architecture Fix
Updated app/templates/shop/base.html:
- Changed x-data to use {% block alpine_data %} for component override
- Allows pages to specify custom Alpine.js components
- Enables page-specific state while extending shared shopLayoutData()

### Documentation (Complete)
Created comprehensive documentation:
- docs/api/shop-api-reference.md - Complete API reference with examples
- docs/architecture/API_CONSOLIDATION_PROPOSAL.md - Analysis of 3 options
- docs/architecture/API_MIGRATION_STATUS.md - Migration tracking (100% complete)
- Updated docs/api/index.md - Added Shop API section
- Updated docs/frontend/shop/architecture.md - New API structure and component pattern

## Benefits Achieved

### Cleaner URLs (~40% shorter)
Before: /api/v1/public/vendors/{vendor_id}/products
After:  /api/v1/shop/products

### Better Architecture
- Middleware-driven vendor context (no manual vendor_id passing)
- Proper separation of concerns (public vs shop vs vendor APIs)
- Consistent authentication pattern
- RESTful design

### Developer Experience
- No need to track vendor_id in frontend state
- Automatic vendor context from Referer header
- Simpler API calls
- Better documentation

## Testing
- Verified middleware extracts vendor from Referer correctly
- Tested all shop API endpoints with vendor context
- Confirmed products page loads and displays products
- Verified error pages show correct links
- No old API references remain in templates

Migration Status:  100% Complete (8/8 success criteria met)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-22 23:03:05 +01:00

399 lines
13 KiB
Python

# app/routes/vendor_pages.py
"""
Vendor HTML page routes using Jinja2 templates.
These routes serve HTML pages for vendor-facing interfaces.
Follows the same minimal server-side rendering pattern as admin routes.
All routes except /login require vendor authentication.
Authentication failures redirect to /vendor/{vendor_code}/login.
Routes:
- GET /vendor/{vendor_code}/ → Redirect to login or dashboard
- GET /vendor/{vendor_code}/login → Vendor login page
- GET /vendor/{vendor_code}/dashboard → Vendor dashboard
- GET /vendor/{vendor_code}/products → Product management
- GET /vendor/{vendor_code}/orders → Order management
- GET /vendor/{vendor_code}/customers → Customer management
- GET /vendor/{vendor_code}/inventory → Inventory management
- GET /vendor/{vendor_code}/marketplace → Marketplace imports
- GET /vendor/{vendor_code}/team → Team management
- GET /vendor/{vendor_code}/settings → Vendor settings
"""
from fastapi import APIRouter, Request, Depends, Path, HTTPException
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session
from typing import Optional
import logging
from app.api.deps import (
get_current_vendor_from_cookie_or_header,
get_current_vendor_optional,
get_db
)
from app.services.content_page_service import content_page_service
from models.database.user import User
logger = logging.getLogger(__name__)
router = APIRouter()
templates = Jinja2Templates(directory="app/templates")
# ============================================================================
# PUBLIC ROUTES (No Authentication Required)
# ============================================================================
@router.get("/{vendor_code}", response_class=RedirectResponse, include_in_schema=False)
async def vendor_root_no_slash(vendor_code: str = Path(..., description="Vendor code")):
"""
Redirect /vendor/{code} (no trailing slash) to login page.
Handles requests without trailing slash.
"""
return RedirectResponse(url=f"/vendor/{vendor_code}/login", status_code=302)
@router.get("/{vendor_code}/", response_class=RedirectResponse, include_in_schema=False)
async def vendor_root(
vendor_code: str = Path(..., description="Vendor code"),
current_user: Optional[User] = Depends(get_current_vendor_optional)
):
"""
Redirect /vendor/{code}/ based on authentication status.
- Authenticated vendor users → /vendor/{code}/dashboard
- Unauthenticated users → /vendor/{code}/login
"""
if current_user:
# User is already logged in as vendor, redirect to dashboard
return RedirectResponse(url=f"/vendor/{vendor_code}/dashboard", status_code=302)
return RedirectResponse(url=f"/vendor/{vendor_code}/login", status_code=302)
@router.get("/{vendor_code}/login", response_class=HTMLResponse, include_in_schema=False)
async def vendor_login_page(
request: Request,
vendor_code: str = Path(..., description="Vendor code"),
current_user: Optional[User] = Depends(get_current_vendor_optional)
):
"""
Render vendor login page.
If user is already authenticated as vendor, redirect to dashboard.
Otherwise, show login form.
JavaScript will:
- Load vendor info via API
- Handle login form submission
- Redirect to dashboard on success
"""
if current_user:
# User is already logged in as vendor, redirect to dashboard
return RedirectResponse(url=f"/vendor/{vendor_code}/dashboard", status_code=302)
return templates.TemplateResponse(
"vendor/login.html",
{
"request": request,
"vendor_code": vendor_code,
}
)
# ============================================================================
# AUTHENTICATED ROUTES (Vendor Users Only)
# ============================================================================
@router.get("/{vendor_code}/dashboard", response_class=HTMLResponse, include_in_schema=False)
async def vendor_dashboard_page(
request: Request,
vendor_code: str = Path(..., description="Vendor code"),
current_user: User = Depends(get_current_vendor_from_cookie_or_header)
):
"""
Render vendor dashboard.
JavaScript will:
- Load vendor info via API
- Load dashboard stats via API
- Load recent orders via API
- Handle all interactivity
"""
return templates.TemplateResponse(
"vendor/dashboard.html",
{
"request": request,
"user": current_user,
"vendor_code": vendor_code,
}
)
# ============================================================================
# PRODUCT MANAGEMENT
# ============================================================================
@router.get("/{vendor_code}/products", response_class=HTMLResponse, include_in_schema=False)
async def vendor_products_page(
request: Request,
vendor_code: str = Path(..., description="Vendor code"),
current_user: User = Depends(get_current_vendor_from_cookie_or_header)
):
"""
Render products management page.
JavaScript loads product list via API.
"""
return templates.TemplateResponse(
"vendor/products.html",
{
"request": request,
"user": current_user,
"vendor_code": vendor_code,
}
)
# ============================================================================
# ORDER MANAGEMENT
# ============================================================================
@router.get("/{vendor_code}/orders", response_class=HTMLResponse, include_in_schema=False)
async def vendor_orders_page(
request: Request,
vendor_code: str = Path(..., description="Vendor code"),
current_user: User = Depends(get_current_vendor_from_cookie_or_header)
):
"""
Render orders management page.
JavaScript loads order list via API.
"""
return templates.TemplateResponse(
"vendor/orders.html",
{
"request": request,
"user": current_user,
"vendor_code": vendor_code,
}
)
# ============================================================================
# CUSTOMER MANAGEMENT
# ============================================================================
@router.get("/{vendor_code}/customers", response_class=HTMLResponse, include_in_schema=False)
async def vendor_customers_page(
request: Request,
vendor_code: str = Path(..., description="Vendor code"),
current_user: User = Depends(get_current_vendor_from_cookie_or_header)
):
"""
Render customers management page.
JavaScript loads customer list via API.
"""
return templates.TemplateResponse(
"vendor/customers.html",
{
"request": request,
"user": current_user,
"vendor_code": vendor_code,
}
)
# ============================================================================
# INVENTORY MANAGEMENT
# ============================================================================
@router.get("/{vendor_code}/inventory", response_class=HTMLResponse, include_in_schema=False)
async def vendor_inventory_page(
request: Request,
vendor_code: str = Path(..., description="Vendor code"),
current_user: User = Depends(get_current_vendor_from_cookie_or_header)
):
"""
Render inventory management page.
JavaScript loads inventory data via API.
"""
return templates.TemplateResponse(
"vendor/inventory.html",
{
"request": request,
"user": current_user,
"vendor_code": vendor_code,
}
)
# ============================================================================
# MARKETPLACE IMPORTS
# ============================================================================
@router.get("/{vendor_code}/marketplace", response_class=HTMLResponse, include_in_schema=False)
async def vendor_marketplace_page(
request: Request,
vendor_code: str = Path(..., description="Vendor code"),
current_user: User = Depends(get_current_vendor_from_cookie_or_header)
):
"""
Render marketplace import page.
JavaScript loads import jobs and products via API.
"""
return templates.TemplateResponse(
"vendor/marketplace.html",
{
"request": request,
"user": current_user,
"vendor_code": vendor_code,
}
)
# ============================================================================
# TEAM MANAGEMENT
# ============================================================================
@router.get("/{vendor_code}/team", response_class=HTMLResponse, include_in_schema=False)
async def vendor_team_page(
request: Request,
vendor_code: str = Path(..., description="Vendor code"),
current_user: User = Depends(get_current_vendor_from_cookie_or_header)
):
"""
Render team management page.
JavaScript loads team members via API.
"""
return templates.TemplateResponse(
"vendor/team.html",
{
"request": request,
"user": current_user,
"vendor_code": vendor_code,
}
)
# ============================================================================
# PROFILE & SETTINGS
# ============================================================================
@router.get("/{vendor_code}/profile", response_class=HTMLResponse, include_in_schema=False)
async def vendor_profile_page(
request: Request,
vendor_code: str = Path(..., description="Vendor code"),
current_user: User = Depends(get_current_vendor_from_cookie_or_header)
):
"""
Render vendor profile page.
User can manage their personal profile information.
"""
return templates.TemplateResponse(
"vendor/profile.html",
{
"request": request,
"user": current_user,
"vendor_code": vendor_code,
}
)
@router.get("/{vendor_code}/settings", response_class=HTMLResponse, include_in_schema=False)
async def vendor_settings_page(
request: Request,
vendor_code: str = Path(..., description="Vendor code"),
current_user: User = Depends(get_current_vendor_from_cookie_or_header)
):
"""
Render vendor settings page.
JavaScript loads settings via API.
"""
return templates.TemplateResponse(
"vendor/settings.html",
{
"request": request,
"user": current_user,
"vendor_code": vendor_code,
}
)
# ============================================================================
# DYNAMIC CONTENT PAGES (CMS)
# ============================================================================
@router.get("/{vendor_code}/{slug}", response_class=HTMLResponse, include_in_schema=False)
async def vendor_content_page(
request: Request,
vendor_code: str = Path(..., description="Vendor code"),
slug: str = Path(..., description="Content page slug"),
db: Session = Depends(get_db)
):
"""
Generic content page handler for vendor shop (CMS).
Handles dynamic content pages like:
- /vendors/wizamart/about, /vendors/wizamart/faq, /vendors/wizamart/contact, etc.
Features:
- Two-tier system: Vendor overrides take priority, fallback to platform defaults
- Only shows published pages
- Returns 404 if page not found or unpublished
NOTE: This is a catch-all route and must be registered LAST to avoid
shadowing other specific routes.
"""
logger.debug(
f"[VENDOR_HANDLER] vendor_content_page REACHED",
extra={
"path": request.url.path,
"vendor_code": vendor_code,
"slug": slug,
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
"context": getattr(request.state, 'context_type', 'NOT SET'),
}
)
vendor = getattr(request.state, 'vendor', None)
vendor_id = vendor.id if vendor else None
# Load content page from database (vendor override → platform default)
page = content_page_service.get_page_for_vendor(
db,
slug=slug,
vendor_id=vendor_id,
include_unpublished=False
)
if not page:
logger.info(
f"[VENDOR_HANDLER] Content page not found: {slug}",
extra={
"slug": slug,
"vendor_code": vendor_code,
"vendor_id": vendor_id,
}
)
raise HTTPException(status_code=404, detail="Page not found")
logger.info(
f"[VENDOR_HANDLER] Rendering CMS page: {page.title}",
extra={
"slug": slug,
"page_id": page.id,
"is_vendor_override": page.vendor_id is not None,
"vendor_id": vendor_id,
}
)
return templates.TemplateResponse(
"shop/content-page.html",
{
"request": request,
"page": page,
"vendor_code": vendor_code,
}
)