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

@@ -467,30 +467,51 @@ current_user: User = Depends(get_current_vendor_api)
**Purpose:** Authenticate customer users for HTML pages
**Accepts:** Cookie (`customer_token`) OR Authorization header
**Returns:** `Customer` object
**Returns:** `Customer` object (from `models.database.customer.Customer`)
**Raises:**
- `InvalidTokenException` - No token or invalid token
- `InsufficientPermissionsException` - User is not customer (admin/vendor blocked)
- `InvalidTokenException` - No token, invalid token, or not a customer token
- `UnauthorizedVendorAccessException` - Token vendor_id doesn't match URL vendor
**Note:** The `InsufficientPermissionsException` raised here is from `app.exceptions.auth`, which provides general authentication permission checking. This is distinct from `InsufficientTeamPermissionsException` used for team-specific permissions.
**Security Features:**
- **Token type validation:** Only accepts tokens with `type: "customer"` - admin and vendor tokens are rejected
- **Vendor validation:** Validates that `token.vendor_id` matches `request.state.vendor.id` (URL-based vendor)
- Prevents cross-vendor token reuse (customer from Vendor A cannot use token on Vendor B's shop)
**Usage:**
```python
current_customer: Customer = Depends(get_current_customer_from_cookie_or_header)
from models.database.customer import Customer
customer: Customer = Depends(get_current_customer_from_cookie_or_header)
# Access customer.id, customer.email, customer.vendor_id, etc.
```
#### `get_current_customer_api()`
**Purpose:** Authenticate customer users for API endpoints
**Accepts:** Authorization header ONLY
**Returns:** `Customer` object
**Returns:** `Customer` object (from `models.database.customer.Customer`)
**Raises:**
- `InvalidTokenException` - No token or invalid token
- `InsufficientPermissionsException` - User is not customer (admin/vendor blocked)
- `InvalidTokenException` - No token, invalid token, or not a customer token
- `UnauthorizedVendorAccessException` - Token vendor_id doesn't match URL vendor
**Security Features:**
- **Token type validation:** Only accepts tokens with `type: "customer"` - admin and vendor tokens are rejected
- **Vendor validation:** Validates that `token.vendor_id` matches `request.state.vendor.id` (URL-based vendor)
- Prevents cross-vendor token reuse
**Usage:**
```python
current_customer: Customer = Depends(get_current_customer_api)
from models.database.customer import Customer
@router.post("/orders")
def place_order(
request: Request,
customer: Customer = Depends(get_current_customer_api),
db: Session = Depends(get_db),
):
# customer is a Customer object, not User
vendor = request.state.vendor # Already validated to match token
order = order_service.create_order(db, vendor.id, customer.id, ...)
```
#### `get_current_user()`
@@ -581,20 +602,26 @@ async def vendor_login_page(
**Purpose:** Check if customer user is authenticated (without enforcing)
**Accepts:** Cookie (`customer_token`) OR Authorization header
**Returns:**
- `User` object with `role="customer"` if authenticated
- `None` if no token, invalid token, or user is not customer
- `Customer` object if authenticated with valid customer token
- `None` if no token, invalid token, vendor mismatch, or not a customer token
**Raises:** Never raises exceptions
**Security Features:**
- Only accepts tokens with `type: "customer"` - admin and vendor tokens return `None`
- Validates vendor_id in token matches URL vendor - mismatch returns `None`
**Usage:**
```python
from models.database.customer import Customer
# Shop login page redirect
@router.get("/shop/account/login")
async def customer_login_page(
request: Request,
current_user: Optional[User] = Depends(get_current_customer_optional)
customer: Customer | None = Depends(get_current_customer_optional)
):
if current_user:
# User already logged in, redirect to account page
if customer:
# Customer already logged in, redirect to account page
return RedirectResponse(url="/shop/account", status_code=302)
# Not logged in, show login form