fix: customer authentication and shop error page styling

## Customer Authentication Fixes
- Fix get_current_customer_api to properly decode customer tokens (was using User model)
- Add _validate_customer_token() helper for shared customer token validation
- Add vendor validation: token.vendor_id must match request URL vendor
- Block admin/vendor tokens from shop endpoints (type != "customer")
- Update get_current_customer_optional to use proper customer token validation
- Customer auth functions now return Customer object (not User)

## Shop Orders API
- Update orders.py to receive Customer directly from auth dependency
- Remove broken get_customer_from_user() helper
- Use VendorNotFoundException instead of HTTPException

## Shop Error Pages
- Fix all error templates (400, 401, 403, 404, 422, 429, 500, 502, generic)
- Templates were using undefined CSS classes (.btn, .status-code, etc.)
- Now properly extend base.html and override specific blocks
- Use Tailwind utility classes for consistent styling

## Documentation
- Update docs/api/authentication.md with new Customer return types
- Document vendor validation security features
- Update docs/api/authentication-quick-reference.md examples

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-04 22:48:02 +01:00
parent 8a367077e1
commit cbfbbb4654
13 changed files with 302 additions and 394 deletions

View File

@@ -1,23 +1,26 @@
# app/api/v1/shop/orders.py
"""
Shop Orders API (Public)
Shop Orders API (Customer authenticated)
Public endpoints for managing customer orders in shop frontend.
Endpoints for managing customer orders in shop frontend.
Uses vendor from request.state (injected by VendorContextMiddleware).
Requires customer authentication for most operations.
Requires customer authentication - get_current_customer_api validates
that customer token vendor_id matches the URL vendor.
Customer Context: get_current_customer_api returns Customer directly
(not User), with vendor validation already performed.
"""
import logging
from fastapi import APIRouter, Depends, HTTPException, Path, Query, Request
from fastapi import APIRouter, Depends, Path, Query, Request
from sqlalchemy.orm import Session
from app.api.deps import get_current_customer_api
from app.core.database import get_db
from app.services.customer_service import customer_service
from app.exceptions import VendorNotFoundException
from app.services.order_service import order_service
from models.database.customer import Customer
from models.database.user import User
from models.schema.order import (
OrderCreate,
OrderDetailResponse,
@@ -29,47 +32,11 @@ router = APIRouter()
logger = logging.getLogger(__name__)
def get_customer_from_user(request: Request, user: User, db: Session) -> Customer:
"""
Helper to get Customer record from authenticated User.
Args:
request: FastAPI request (to get vendor)
user: Authenticated user
db: Database session
Returns:
Customer record
Raises:
HTTPException: If customer not found or vendor mismatch
"""
vendor = getattr(request.state, "vendor", None)
if not vendor:
raise HTTPException(
status_code=404,
detail="Vendor not found. Please access via vendor domain/subdomain/path.",
)
# Find customer record for this user and vendor
customer = customer_service.get_customer_by_user_id(
db=db, vendor_id=vendor.id, user_id=user.id
)
if not customer:
raise HTTPException(
status_code=404, detail="Customer account not found for current vendor"
)
return customer
@router.post("/orders", response_model=OrderResponse)
def place_order(
request: Request,
order_data: OrderCreate,
current_user: User = Depends(get_current_customer_api),
customer: Customer = Depends(get_current_customer_api),
db: Session = Depends(get_db),
):
"""
@@ -82,17 +49,11 @@ def place_order(
Request Body:
- Order data including shipping address, payment method, etc.
"""
# Get vendor from middleware
# Get vendor from middleware (already validated by get_current_customer_api)
vendor = getattr(request.state, "vendor", None)
if not vendor:
raise HTTPException(
status_code=404,
detail="Vendor not found. Please access via vendor domain/subdomain/path.",
)
# Get customer record
customer = get_customer_from_user(request, current_user, db)
raise VendorNotFoundException("context", identifier_type="subdomain")
logger.debug(
f"[SHOP_API] place_order for customer {customer.id}",
@@ -100,7 +61,6 @@ def place_order(
"vendor_id": vendor.id,
"vendor_code": vendor.subdomain,
"customer_id": customer.id,
"user_id": current_user.id,
},
)
@@ -132,7 +92,7 @@ def get_my_orders(
request: Request,
skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=100),
current_user: User = Depends(get_current_customer_api),
customer: Customer = Depends(get_current_customer_api),
db: Session = Depends(get_db),
):
"""
@@ -145,17 +105,11 @@ def get_my_orders(
- skip: Number of orders to skip (pagination)
- limit: Maximum number of orders to return
"""
# Get vendor from middleware
# Get vendor from middleware (already validated by get_current_customer_api)
vendor = getattr(request.state, "vendor", None)
if not vendor:
raise HTTPException(
status_code=404,
detail="Vendor not found. Please access via vendor domain/subdomain/path.",
)
# Get customer record
customer = get_customer_from_user(request, current_user, db)
raise VendorNotFoundException("context", identifier_type="subdomain")
logger.debug(
f"[SHOP_API] get_my_orders for customer {customer.id}",
@@ -185,7 +139,7 @@ def get_my_orders(
def get_order_details(
request: Request,
order_id: int = Path(..., description="Order ID", gt=0),
current_user: User = Depends(get_current_customer_api),
customer: Customer = Depends(get_current_customer_api),
db: Session = Depends(get_db),
):
"""
@@ -197,17 +151,11 @@ def get_order_details(
Path Parameters:
- order_id: ID of the order to retrieve
"""
# Get vendor from middleware
# Get vendor from middleware (already validated by get_current_customer_api)
vendor = getattr(request.state, "vendor", None)
if not vendor:
raise HTTPException(
status_code=404,
detail="Vendor not found. Please access via vendor domain/subdomain/path.",
)
# Get customer record
customer = get_customer_from_user(request, current_user, db)
raise VendorNotFoundException("context", identifier_type="subdomain")
logger.debug(
f"[SHOP_API] get_order_details: order {order_id}",