Files
orion/app/routes/shop_pages.py

690 lines
22 KiB
Python

# app/routes/shop_pages.py
"""
Shop/Customer HTML page routes using Jinja2 templates.
These routes serve the public-facing shop interface for customers.
Authentication required only for account pages.
AUTHENTICATION:
- Public pages (catalog, products): No auth required
- Account pages (dashboard, orders): Requires customer authentication
- Customer authentication accepts:
* customer_token cookie (path=/shop) - for page navigation
* Authorization header - for API calls
- Customers CANNOT access admin or vendor routes
Routes:
- GET /shop/ → Shop homepage / product catalog
- GET /shop/products → Product catalog
- GET /shop/products/{id} → Product detail page
- GET /shop/categories/{slug} → Category products
- GET /shop/cart → Shopping cart
- GET /shop/checkout → Checkout process
- GET /shop/account/register → Customer registration
- GET /shop/account/login → Customer login
- GET /shop/account/dashboard → Customer dashboard (auth required)
- GET /shop/account/orders → Order history (auth required)
- GET /shop/account/orders/{id} → Order detail (auth required)
- GET /shop/account/profile → Customer profile (auth required)
- GET /shop/account/addresses → Address management (auth required)
"""
import logging
from fastapi import APIRouter, Request, Depends, Path
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session
from app.api.deps import get_current_customer_from_cookie_or_header, get_db
from models.database.user import User
router = APIRouter()
templates = Jinja2Templates(directory="app/templates")
logger = logging.getLogger(__name__)
# ============================================================================
# HELPER: Build Shop Template Context
# ============================================================================
def get_shop_context(request: Request, **extra_context) -> dict:
"""
Build template context for shop pages.
Automatically includes vendor and theme from middleware request.state.
Additional context can be passed as keyword arguments.
Args:
request: FastAPI request object with vendor/theme in state
**extra_context: Additional variables for template (user, product_id, etc.)
Returns:
Dictionary with request, vendor, theme, and extra context
Example:
# Simple usage
get_shop_context(request)
# With extra data
get_shop_context(request, user=current_user, product_id=123)
"""
# Extract from middleware state
vendor = getattr(request.state, 'vendor', None)
theme = getattr(request.state, 'theme', None)
clean_path = getattr(request.state, 'clean_path', request.url.path)
if vendor is None:
logger.warning(
"[SHOP_CONTEXT] Vendor not found in request.state",
extra={
"path": request.url.path,
"host": request.headers.get("host", ""),
"has_vendor": False,
}
)
context = {
"request": request,
"vendor": vendor,
"theme": theme,
"clean_path": clean_path,
}
# Add any extra context (user, product_id, category_slug, etc.)
if extra_context:
context.update(extra_context)
logger.debug(
f"[SHOP_CONTEXT] Context built",
extra={
"vendor_id": vendor.id if vendor else None,
"vendor_name": vendor.name if vendor else None,
"has_theme": theme is not None,
"extra_keys": list(extra_context.keys()) if extra_context else [],
}
)
return context
# ============================================================================
# PUBLIC SHOP ROUTES (No Authentication Required)
# ============================================================================
@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):
"""
Render shop homepage / product catalog.
Shows featured products and categories.
"""
logger.debug(
f"[SHOP_HANDLER] shop_products_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
"context": getattr(request.state, 'context_type', 'NOT SET'),
}
)
return templates.TemplateResponse(
"shop/products.html",
get_shop_context(request)
)
@router.get("/shop/products/{product_id}", response_class=HTMLResponse, include_in_schema=False)
async def shop_product_detail_page(
request: Request,
product_id: int = Path(..., description="Product ID")
):
"""
Render product detail page.
Shows product information, images, reviews, and buy options.
"""
logger.debug(
f"[SHOP_HANDLER] shop_products_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
"context": getattr(request.state, 'context_type', 'NOT SET'),
}
)
return templates.TemplateResponse(
"shop/product.html",
get_shop_context(request, product_id=product_id)
)
@router.get("/shop/categories/{category_slug}", response_class=HTMLResponse, include_in_schema=False)
async def shop_category_page(
request: Request,
category_slug: str = Path(..., description="Category slug")
):
"""
Render category products page.
Shows all products in a specific category.
"""
logger.debug(
f"[SHOP_HANDLER] shop_products_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
"context": getattr(request.state, 'context_type', 'NOT SET'),
}
)
return templates.TemplateResponse(
"shop/category.html",
get_shop_context(request, category_slug=category_slug)
)
@router.get("/shop/cart", response_class=HTMLResponse, include_in_schema=False)
async def shop_cart_page(request: Request):
"""
Render shopping cart page.
Shows cart items and allows quantity updates.
"""
logger.debug(
f"[SHOP_HANDLER] shop_products_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
"context": getattr(request.state, 'context_type', 'NOT SET'),
}
)
return templates.TemplateResponse(
"shop/cart.html",
get_shop_context(request)
)
@router.get("/shop/checkout", response_class=HTMLResponse, include_in_schema=False)
async def shop_checkout_page(request: Request):
"""
Render checkout page.
Handles shipping, payment, and order confirmation.
"""
logger.debug(
f"[SHOP_HANDLER] shop_products_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
"context": getattr(request.state, 'context_type', 'NOT SET'),
}
)
return templates.TemplateResponse(
"shop/checkout.html",
get_shop_context(request)
)
@router.get("/shop/search", response_class=HTMLResponse, include_in_schema=False)
async def shop_search_page(request: Request):
"""
Render search results page.
Shows products matching search query.
"""
logger.debug(
f"[SHOP_HANDLER] shop_products_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
"context": getattr(request.state, 'context_type', 'NOT SET'),
}
)
return templates.TemplateResponse(
"shop/search.html",
get_shop_context(request)
)
# ============================================================================
# CUSTOMER ACCOUNT - PUBLIC ROUTES (No Authentication)
# ============================================================================
@router.get("/shop/account/register", response_class=HTMLResponse, include_in_schema=False)
async def shop_register_page(request: Request):
"""
Render customer registration page.
No authentication required.
"""
logger.debug(
f"[SHOP_HANDLER] shop_products_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
"context": getattr(request.state, 'context_type', 'NOT SET'),
}
)
return templates.TemplateResponse(
"shop/account/register.html",
get_shop_context(request)
)
@router.get("/shop/account/login", response_class=HTMLResponse, include_in_schema=False)
async def shop_login_page(request: Request):
"""
Render customer login page.
No authentication required.
"""
logger.debug(
f"[SHOP_HANDLER] shop_products_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
"context": getattr(request.state, 'context_type', 'NOT SET'),
}
)
return templates.TemplateResponse(
"shop/account/login.html",
get_shop_context(request)
)
@router.get("/shop/account/forgot-password", response_class=HTMLResponse, include_in_schema=False)
async def shop_forgot_password_page(request: Request):
"""
Render forgot password page.
Allows customers to reset their password.
"""
logger.debug(
f"[SHOP_HANDLER] shop_products_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
"context": getattr(request.state, 'context_type', 'NOT SET'),
}
)
return templates.TemplateResponse(
"shop/account/forgot-password.html",
get_shop_context(request)
)
# ============================================================================
# CUSTOMER ACCOUNT - AUTHENTICATED ROUTES
# ============================================================================
@router.get("/shop/account/", response_class=RedirectResponse, include_in_schema=False)
async def shop_account_root():
"""
Redirect /shop/account/ to dashboard.
"""
logger.debug(
f"[SHOP_HANDLER] shop_products_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
"context": getattr(request.state, 'context_type', 'NOT SET'),
}
)
return RedirectResponse(url="/shop/account/dashboard", status_code=302)
@router.get("/shop/account/dashboard", response_class=HTMLResponse, include_in_schema=False)
async def shop_account_dashboard_page(
request: Request,
current_user: User = Depends(get_current_customer_from_cookie_or_header),
db: Session = Depends(get_db)
):
"""
Render customer account dashboard.
Shows account overview, recent orders, and quick links.
Requires customer authentication.
"""
logger.debug(
f"[SHOP_HANDLER] shop_products_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
"context": getattr(request.state, 'context_type', 'NOT SET'),
}
)
return templates.TemplateResponse(
"shop/account/dashboard.html",
get_shop_context(request, user=current_user)
)
@router.get("/shop/account/orders", response_class=HTMLResponse, include_in_schema=False)
async def shop_orders_page(
request: Request,
current_user: User = Depends(get_current_customer_from_cookie_or_header),
db: Session = Depends(get_db)
):
"""
Render customer orders history page.
Shows all past and current orders.
Requires customer authentication.
"""
logger.debug(
f"[SHOP_HANDLER] shop_products_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
"context": getattr(request.state, 'context_type', 'NOT SET'),
}
)
return templates.TemplateResponse(
"shop/account/orders.html",
get_shop_context(request, user=current_user)
)
@router.get("/shop/account/orders/{order_id}", response_class=HTMLResponse, include_in_schema=False)
async def shop_order_detail_page(
request: Request,
order_id: int = Path(..., description="Order ID"),
current_user: User = Depends(get_current_customer_from_cookie_or_header),
db: Session = Depends(get_db)
):
"""
Render customer order detail page.
Shows detailed order information and tracking.
Requires customer authentication.
"""
logger.debug(
f"[SHOP_HANDLER] shop_products_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
"context": getattr(request.state, 'context_type', 'NOT SET'),
}
)
return templates.TemplateResponse(
"shop/account/order-detail.html",
get_shop_context(request, user=current_user, order_id=order_id)
)
@router.get("/shop/account/profile", response_class=HTMLResponse, include_in_schema=False)
async def shop_profile_page(
request: Request,
current_user: User = Depends(get_current_customer_from_cookie_or_header),
db: Session = Depends(get_db)
):
"""
Render customer profile page.
Edit personal information and preferences.
Requires customer authentication.
"""
logger.debug(
f"[SHOP_HANDLER] shop_products_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
"context": getattr(request.state, 'context_type', 'NOT SET'),
}
)
return templates.TemplateResponse(
"shop/account/profile.html",
get_shop_context(request, user=current_user)
)
@router.get("/shop/account/addresses", response_class=HTMLResponse, include_in_schema=False)
async def shop_addresses_page(
request: Request,
current_user: User = Depends(get_current_customer_from_cookie_or_header),
db: Session = Depends(get_db)
):
"""
Render customer addresses management page.
Manage shipping and billing addresses.
Requires customer authentication.
"""
logger.debug(
f"[SHOP_HANDLER] shop_products_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
"context": getattr(request.state, 'context_type', 'NOT SET'),
}
)
return templates.TemplateResponse(
"shop/account/addresses.html",
get_shop_context(request, user=current_user)
)
@router.get("/shop/account/wishlist", response_class=HTMLResponse, include_in_schema=False)
async def shop_wishlist_page(
request: Request,
current_user: User = Depends(get_current_customer_from_cookie_or_header),
db: Session = Depends(get_db)
):
"""
Render customer wishlist page.
View and manage saved products.
Requires customer authentication.
"""
logger.debug(
f"[SHOP_HANDLER] shop_products_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
"context": getattr(request.state, 'context_type', 'NOT SET'),
}
)
return templates.TemplateResponse(
"shop/account/wishlist.html",
get_shop_context(request, user=current_user)
)
@router.get("/shop/account/settings", response_class=HTMLResponse, include_in_schema=False)
async def shop_settings_page(
request: Request,
current_user: User = Depends(get_current_customer_from_cookie_or_header),
db: Session = Depends(get_db)
):
"""
Render customer account settings page.
Configure notifications, privacy, and preferences.
Requires customer authentication.
"""
logger.debug(
f"[SHOP_HANDLER] shop_products_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
"context": getattr(request.state, 'context_type', 'NOT SET'),
}
)
return templates.TemplateResponse(
"shop/account/settings.html",
get_shop_context(request, user=current_user)
)
# ============================================================================
# STATIC CONTENT PAGES
# ============================================================================
@router.get("/shop/about", response_class=HTMLResponse, include_in_schema=False)
async def shop_about_page(request: Request):
"""
Render about us page.
"""
logger.debug(
f"[SHOP_HANDLER] shop_products_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
"context": getattr(request.state, 'context_type', 'NOT SET'),
}
)
return templates.TemplateResponse(
"shop/about.html",
get_shop_context(request)
)
@router.get("/shop/contact", response_class=HTMLResponse, include_in_schema=False)
async def shop_contact_page(request: Request):
"""
Render contact us page.
"""
logger.debug(
f"[SHOP_HANDLER] shop_products_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
"context": getattr(request.state, 'context_type', 'NOT SET'),
}
)
return templates.TemplateResponse(
"shop/contact.html",
get_shop_context(request)
)
@router.get("/shop/faq", response_class=HTMLResponse, include_in_schema=False)
async def shop_faq_page(request: Request):
"""
Render FAQ page.
"""
logger.debug(
f"[SHOP_HANDLER] shop_products_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
"context": getattr(request.state, 'context_type', 'NOT SET'),
}
)
return templates.TemplateResponse(
"shop/faq.html",
get_shop_context(request)
)
@router.get("/shop/privacy", response_class=HTMLResponse, include_in_schema=False)
async def shop_privacy_page(request: Request):
"""
Render privacy policy page.
"""
logger.debug(
f"[SHOP_HANDLER] shop_products_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
"context": getattr(request.state, 'context_type', 'NOT SET'),
}
)
return templates.TemplateResponse(
"shop/privacy.html",
get_shop_context(request)
)
@router.get("/shop/terms", response_class=HTMLResponse, include_in_schema=False)
async def shop_terms_page(request: Request):
"""
Render terms and conditions page.
"""
logger.debug(
f"[SHOP_HANDLER] shop_products_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
"context": getattr(request.state, 'context_type', 'NOT SET'),
}
)
return templates.TemplateResponse(
"shop/terms.html",
get_shop_context(request)
)
# ============================================================================
# DEBUG ENDPOINTS - For troubleshooting context issues
# ============================================================================
@router.get("/debug/context", response_class=HTMLResponse, include_in_schema=False)
async def debug_context(request: Request):
"""
DEBUG ENDPOINT: Display request context.
Shows what's available in request.state.
Useful for troubleshooting template variable issues.
URL: /shop/debug/context
"""
vendor = getattr(request.state, 'vendor', None)
theme = getattr(request.state, 'theme', None)
debug_info = {
"path": request.url.path,
"host": request.headers.get("host", ""),
"vendor": {
"found": vendor is not None,
"id": vendor.id if vendor else None,
"name": vendor.name if vendor else None,
"subdomain": vendor.subdomain if vendor else None,
"is_active": vendor.is_active if vendor else None,
},
"theme": {
"found": theme is not None,
"name": theme.get("theme_name") if theme else None,
},
"clean_path": getattr(request.state, 'clean_path', 'NOT SET'),
"context_type": str(getattr(request.state, 'context_type', 'NOT SET')),
}
# Return as JSON-like HTML for easy reading
import json
html_content = f"""
<!DOCTYPE html>
<html>
<head>
<title>Debug Context</title>
<style>
body {{ font-family: monospace; margin: 20px; }}
pre {{ background: #f0f0f0; padding: 20px; border-radius: 5px; }}
.good {{ color: green; }}
.bad {{ color: red; }}
</style>
</head>
<body>
<h1>Request Context Debug</h1>
<pre>{json.dumps(debug_info, indent=2)}</pre>
<h2>Status</h2>
<p class="{'good' if vendor else 'bad'}">
Vendor: {'✓ Found' if vendor else '✗ Not Found'}
</p>
<p class="{'good' if theme else 'bad'}">
Theme: {'✓ Found' if theme else '✗ Not Found'}
</p>
<p class="{'good' if str(getattr(request.state, 'context_type', 'NOT SET')) == 'shop' else 'bad'}">
Context Type: {str(getattr(request.state, 'context_type', 'NOT SET'))}
</p>
</body>
</html>
"""
return HTMLResponse(content=html_content)