feat: add customer authentication pages and documentation

Add complete customer authentication UI with login, registration,
forgot password, and dashboard pages.

Templates Added:
- app/templates/shop/account/login.html
  - Two-column layout with vendor branding
  - Email/password login with validation
  - Password visibility toggle
  - "Remember me" functionality
  - Error/success alerts
  - Loading states with spinner
- app/templates/shop/account/register.html
  - Customer registration form
  - Client-side validation (password strength, email format)
  - Marketing consent checkbox
  - Confirm password matching
- app/templates/shop/account/forgot-password.html
  - Password reset request page
  - Email validation
  - Success confirmation
- app/templates/shop/account/dashboard.html
  - Customer account dashboard
  - Overview of orders, profile, addresses

Styles Added:
- static/shared/css/auth.css
  - Authentication page styling
  - Two-column layout system
  - Form components and validation states
  - Theme-aware with CSS variables
  - Dark mode support
  - Mobile responsive
- static/shared/css/base.css updates
  - Enhanced utility classes
  - Additional form styles
  - Improved button states

Documentation Added:
- docs/frontend/shop/authentication-pages.md
  - Comprehensive guide to auth page implementation
  - Component architecture
  - API integration patterns
  - Theme customization
- docs/development/CUSTOMER_AUTHENTICATION_IMPLEMENTATION.md
  - Implementation details and technical decisions
  - Security considerations
  - Testing procedures
- docs/development/CUSTOMER_AUTH_SUMMARY.md
  - Quick reference guide
  - Endpoints and flows
- Updated docs/frontend/shop/architecture.md
  - Added authentication section
  - Documented all auth pages
- Updated docs/frontend/shop/page-templates.md
  - Added auth template documentation
- Updated mkdocs.yml
  - Added new documentation pages to navigation

Features:
- Full theme integration with vendor branding
- Alpine.js reactive components
- Tailwind CSS utility-first styling
- Client and server-side validation
- JWT token management
- Multi-access routing support (domain/subdomain/path)
- Error handling with user-friendly messages
- Loading states and animations
- Mobile responsive design
- Dark mode support

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-25 21:09:27 +01:00
parent 6735d99df2
commit 86d67b5cfb
12 changed files with 3118 additions and 473 deletions

View File

@@ -0,0 +1,650 @@
# Customer Authentication Implementation
**Date**: 2025-11-24
**Status**: Completed
## 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/vendor 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:
1. No styled authentication pages for customers
2. Customer authentication was incorrectly trying to use the User model (admins/vendors)
3. Cookie paths were hardcoded and didn't work with multi-access routing (domain, subdomain, path-based)
4. Vendor detection method was inconsistent between direct path access and API calls via referer
## Solution Architecture
### 1. Customer vs User Separation
**Key Insight**: Customers are NOT users. They are a separate entity in the system.
- **Users** (`models/database/user.py`): Admin and vendor accounts
- Have `role` field (admin/vendor)
- Have `username` field
- Managed via `app/services/auth_service.py`
- **Customers** (`models/database/customer.py`): Shop customers
- Vendor-scoped (each vendor has independent customers)
- No `role` or `username` fields
- Have `customer_number`, `total_orders`, vendor relationship
- Managed via `app/services/customer_service.py`
### 2. JWT Token Structure
Customer tokens have a distinct structure:
```python
{
"sub": str(customer.id), # Customer ID
"email": customer.email,
"vendor_id": vendor_id, # Important: Vendor isolation
"type": "customer", # CRITICAL: Distinguishes from User tokens
"exp": expire_timestamp,
"iat": issued_at_timestamp,
}
```
User tokens have `type` implicitly set to user role (admin/vendor) and different payload structure.
### 3. Cookie Path Management
Cookies must be set with paths that match how the vendor is accessed:
| Access Method | Example URL | Cookie Path |
|--------------|-------------|-------------|
| Domain | `wizamart.com/shop/account/login` | `/shop` |
| Subdomain | `wizamart.localhost/shop/account/login` | `/shop` |
| Path-based | `localhost/vendors/wizamart/shop/account/login` | `/vendors/wizamart/shop` |
This ensures cookies are only sent to the correct vendor's routes.
## Implementation Details
### Files Created
1. **`app/templates/shop/account/login.html`**
- Customer login page
- Extends `shop/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`**
- 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`**
- Password reset request page
- Two-state UI (form → success)
- Email validation
4. **`app/templates/shop/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)
### Important Schema Change
#### `models/schema/auth.py` - Unified Login Schema
Changed the `UserLogin` schema to use `email_or_username` instead of `username` to support both username and email login across all contexts (admin, vendor, and customer).
**Before**:
```python
class UserLogin(BaseModel):
username: str
password: str
```
**After**:
```python
class UserLogin(BaseModel):
email_or_username: str = Field(..., description="Username or email address")
password: str
vendor_code: Optional[str] = Field(None, description="Optional vendor code for context")
```
**Impact**: This change affects all login endpoints:
- Admin login: `/api/v1/admin/auth/login`
- Vendor login: `/api/v1/vendor/auth/login`
- Customer login: `/api/v1/shop/auth/login`
**Updated Files**:
- `app/services/auth_service.py` - Changed `user_credentials.username` to `user_credentials.email_or_username`
- `app/api/v1/admin/auth.py` - Updated logging to use `email_or_username`
- `static/admin/js/login.js` - Send `email_or_username` in payload
- `static/vendor/js/login.js` - Send `email_or_username` in payload
### Files Modified
#### 1. `app/api/v1/shop/auth.py`
**Changes**:
- Added `CustomerLoginResponse` model (uses `CustomerResponse` instead of `UserResponse`)
- Updated `customer_login` endpoint to:
- Calculate cookie path dynamically based on vendor access method
- Set cookie with correct path for multi-access support
- Return `CustomerLoginResponse` with proper customer data
- Updated `customer_logout` endpoint to calculate cookie path dynamically
**Key Code**:
```python
# Calculate cookie path based on vendor access method
vendor_context = getattr(request.state, 'vendor_context', None)
access_method = vendor_context.get('detection_method', 'unknown') if vendor_context else 'unknown'
cookie_path = "/shop" # Default for domain/subdomain access
if access_method == "path":
# For path-based access like /vendors/wizamart/shop
full_prefix = vendor_context.get('full_prefix', '/vendor/') if vendor_context else '/vendor/'
cookie_path = f"{full_prefix}{vendor.subdomain}/shop"
response.set_cookie(
key="customer_token",
value=token,
httponly=True,
secure=should_use_secure_cookies(),
samesite="lax",
max_age=expires_in,
path=cookie_path, # Dynamic path
)
```
#### 2. `app/services/customer_service.py`
**Changes**:
- Updated `login_customer` to create JWT tokens directly using `auth_manager`
- No longer tries to use `auth_service.create_access_token()` (that's for Users only)
- Directly uses `jose.jwt.encode()` with custom customer payload
**Key Code**:
```python
from jose import jwt
from datetime import datetime, timedelta, timezone
auth_manager = self.auth_service.auth_manager
expires_delta = timedelta(minutes=auth_manager.token_expire_minutes)
expire = datetime.now(timezone.utc) + expires_delta
payload = {
"sub": str(customer.id),
"email": customer.email,
"vendor_id": vendor_id,
"type": "customer", # Critical distinction
"exp": expire,
"iat": datetime.now(timezone.utc),
}
token = jwt.encode(payload, auth_manager.secret_key, algorithm=auth_manager.algorithm)
```
#### 3. `app/api/deps.py`
**Major Rewrite**: `get_current_customer_from_cookie_or_header`
**Before**: Tried to validate customer tokens as User tokens, expected `role` field
**After**:
- Decodes JWT manually
- Validates `type == "customer"` in payload
- Loads Customer from database (not User)
- Returns Customer object
**Key Code**:
```python
def get_current_customer_from_cookie_or_header(
request: Request,
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
customer_token: Optional[str] = Cookie(None),
db: Session = Depends(get_db),
):
from models.database.customer import Customer
from jose import jwt, JWTError
token, source = _get_token_from_request(...)
if not token:
raise InvalidTokenException("Customer authentication required")
# Decode and validate customer JWT token
payload = jwt.decode(token, auth_manager.secret_key, algorithms=[auth_manager.algorithm])
# Verify this is a customer token
if payload.get("type") != "customer":
raise InvalidTokenException("Customer authentication required")
customer_id = payload.get("sub")
customer = db.query(Customer).filter(Customer.id == int(customer_id)).first()
if not customer or not customer.is_active:
raise InvalidTokenException("Customer not found or inactive")
return customer # Returns Customer, not User
```
#### 4. `app/routes/shop_pages.py`
**Changes**:
- Changed import from `User` to `Customer`
- Updated all protected route handlers:
- Changed parameter type from `current_user: User` to `current_customer: Customer`
- Updated function calls from `user=current_user` to `user=current_customer`
**Affected Routes**:
- `/account/dashboard`
- `/account/orders`
- `/account/orders/{order_id}`
- `/account/profile`
- `/account/addresses`
- `/account/wishlist`
- `/account/reviews`
#### 5. `middleware/vendor_context.py`
**Critical Fix**: Harmonized vendor detection methods
**Problem**:
- Direct page access: `detection_method = "path"`
- API call via referer: `detection_method = "referer_path"`
- This inconsistency broke cookie path calculation
**Solution**:
When detecting vendor from referer path, use the same `detection_method = "path"` and include the same fields (`full_prefix`, `path_prefix`) as direct path detection.
**Key Code**:
```python
# Method 1: Path-based detection from referer path
if referer_path.startswith("/vendors/") or referer_path.startswith("/vendor/"):
prefix = "/vendors/" if referer_path.startswith("/vendors/") else "/vendor/"
path_parts = referer_path[len(prefix):].split("/")
if len(path_parts) >= 1 and path_parts[0]:
vendor_code = path_parts[0]
prefix_len = len(prefix)
# Use "path" as detection_method to be consistent with direct path detection
return {
"subdomain": vendor_code,
"detection_method": "path", # Consistent!
"path_prefix": referer_path[:prefix_len + len(vendor_code)],
"full_prefix": prefix,
"host": referer_host,
"referer": referer,
}
```
#### 6. `app/exceptions/handler.py`
**Changes**:
- Password sanitization in validation error logging
- Already had proper redirect logic for customer login (no changes needed)
#### 7. `models/schema/auth.py`
**Changes**:
- Updated `UserLogin` schema to accept `email_or_username` instead of `username`
- This allows customers to login with email (they don't have usernames)
### Multi-Access Routing Support
The implementation properly supports all three vendor access methods:
#### Domain-based Access
```
URL: https://wizamart.com/shop/account/login
Cookie Path: /shop
Cookie Sent To: https://wizamart.com/shop/*
```
#### Subdomain-based Access
```
URL: https://wizamart.myplatform.com/shop/account/login
Cookie Path: /shop
Cookie Sent To: https://wizamart.myplatform.com/shop/*
```
#### Path-based Access
```
URL: https://myplatform.com/vendors/wizamart/shop/account/login
Cookie Path: /vendors/wizamart/shop
Cookie Sent To: https://myplatform.com/vendors/wizamart/shop/*
```
## Authentication Flow
### Login Flow
1. **User loads login page**`GET /vendors/wizamart/shop/account/login`
- Middleware detects vendor from path
- Sets `detection_method = "path"` in vendor_context
- Renders login template
2. **User submits credentials**`POST /api/v1/shop/auth/login`
- Middleware detects vendor from Referer header
- Sets `detection_method = "path"` (harmonized!)
- Validates credentials via `customer_service.login_customer()`
- Creates JWT token with `type: "customer"`
- Calculates cookie path based on access method
- Sets `customer_token` cookie with correct path
- Returns token + customer data
3. **Browser redirects to dashboard**`GET /vendors/wizamart/shop/account/dashboard`
- Browser sends `customer_token` cookie (path matches!)
- Dependency `get_current_customer_from_cookie_or_header` extracts token
- Decodes JWT, validates `type == "customer"`
- Loads Customer from database
- Renders dashboard with customer data
### Logout Flow
1. **User clicks logout button** → Shows Tailwind modal confirmation
- Custom modal (not browser confirm dialog)
- Alpine.js state management
- Smooth animations with transitions
- Dark mode support
2. **User confirms logout**`POST /api/v1/shop/auth/logout`
- Calculates cookie path (same logic as login)
- Deletes cookie with matching path
- Returns success message
3. **Frontend redirects to login page**
- Shows success toast notification
- Clears localStorage token
- Redirects after 500ms delay
## Security Features
### Cookie Security
```python
response.set_cookie(
key="customer_token",
value=token,
httponly=True, # JavaScript cannot access (XSS protection)
secure=True, # HTTPS only (production/staging)
samesite="lax", # CSRF protection
max_age=1800, # 30 minutes (matches JWT expiry)
path=cookie_path, # Restricted to vendor's shop routes
)
```
### Token Validation
- JWT expiration checked
- Customer active status verified
- Token type validated (`type == "customer"`)
- Vendor isolation enforced (customer must belong to vendor)
### Password Security
- Bcrypt hashing via `auth_manager.hash_password()`
- Validation errors sanitized (passwords never logged)
- Minimum password length enforced
## Design Patterns Followed
### Frontend Templates
All authentication pages follow the shop template pattern:
```jinja2
{% extends "shop/base.html" %}
{% block title %}Page Title{% endblock %}
{% block alpine_data %}componentName(){% endblock %}
{% block content %}
<!-- Page content -->
{% endblock %}
{% block extra_scripts %}
<script>
function componentName() {
return {
...shopLayoutData(),
// Component-specific data/methods
}
}
</script>
{% endblock %}
```
**Benefits**:
- Consistent header/footer/navigation
- Theme CSS variables automatically injected
- Dark mode support
- Mobile responsive
- Alpine.js component pattern
### Service Layer
- Customer operations in `customer_service.py`
- Auth operations in `auth_service.py`
- Clear separation of concerns
- Database operations via SQLAlchemy ORM
### Exception Handling
- Custom exceptions for customer-specific errors
- Consistent error responses (JSON for API, HTML for pages)
- Automatic redirect to login on 401 for HTML page requests
### UI Components
#### Logout Confirmation Modal
Custom Tailwind CSS modal for logout confirmation instead of browser's native `confirm()` dialog.
**Features**:
- Beautiful animated modal with backdrop overlay
- Warning icon (red triangle with exclamation mark)
- Clear confirmation message
- Two action buttons: "Logout" (red) and "Cancel" (gray)
- Dark mode support
- Mobile responsive
- Keyboard accessible (ARIA attributes)
- Click backdrop to dismiss
**Implementation**:
```html
<!-- Modal trigger -->
<button @click="showLogoutModal = true">Logout</button>
<!-- Modal component -->
<div x-show="showLogoutModal" x-cloak class="fixed inset-0 z-50">
<!-- Backdrop with fade animation -->
<div x-show="showLogoutModal"
x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
@click="showLogoutModal = false"
class="fixed inset-0 bg-gray-500 bg-opacity-75">
</div>
<!-- Modal panel with slide+scale animation -->
<div x-show="showLogoutModal"
x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
class="bg-white dark:bg-gray-800 rounded-lg">
<!-- Modal content -->
</div>
</div>
```
**Alpine.js Component**:
```javascript
function accountDashboard() {
return {
...shopLayoutData(),
showLogoutModal: false, // Modal state
confirmLogout() {
this.showLogoutModal = false;
// Perform logout API call
// Show toast notification
// Redirect to login
}
}
}
```
**Why Custom Modal vs Browser Confirm**:
- ✅ Consistent with design system
- ✅ Customizable styling and animations
- ✅ Dark mode support
- ✅ Better mobile experience
- ✅ More professional appearance
- ✅ Accessible (ARIA labels, keyboard navigation)
- ❌ Browser confirm: Cannot be styled, looks dated, poor mobile UX
## Testing Checklist
- [x] Customer can register new account
- [x] Customer can login with email/password
- [x] Admin can login with username (using unified schema)
- [x] Vendor can login with username (using unified schema)
- [x] Cookie is set with correct path for path-based access
- [x] Cookie is sent on subsequent requests to dashboard
- [x] Customer authentication dependency validates token correctly
- [x] Dashboard loads with customer data
- [x] Customer can logout
- [x] Logout confirmation modal displays correctly
- [x] Modal has smooth animations and transitions
- [x] Modal supports dark mode
- [x] Toast notification shows on logout
- [x] Cookie is properly deleted on logout
- [x] Unauthorized access redirects to login
- [x] Theme styling is applied correctly
- [x] Dark mode works
- [x] Mobile responsive layout
- [x] Admin/vendor login button spinner aligns correctly
## Known Limitations
1. **Password Reset Not Implemented**: `forgot-password` and `reset-password` endpoints are placeholders (TODO comments in code)
2. **Email Verification Not Implemented**: Customers are immediately active after registration
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
## Future Enhancements
1. **Password Reset Flow**:
- Generate secure reset tokens
- Send password reset emails
- Token expiry and validation
- Password reset form
2. **Email Verification**:
- Send verification email on registration
- Verification token validation
- Resend verification email
3. **Account Management**:
- Edit profile (name, email, phone)
- Change password
- Manage addresses (CRUD)
- View order history with filtering/search
- Order tracking
4. **Security Enhancements**:
- Refresh tokens for longer sessions
- Rate limiting on login/registration
- Account lockout after failed attempts
- 2FA/MFA support
5. **User Experience**:
- Remember me functionality
- Social login (OAuth)
- Progressive disclosure of forms
- Better error messages
## References
- **Customer Model**: `models/database/customer.py`
- **Customer Service**: `app/services/customer_service.py`
- **Auth Endpoints**: `app/api/v1/shop/auth.py`
- **Auth Dependencies**: `app/api/deps.py`
- **Shop Routes**: `app/routes/shop_pages.py`
- **Vendor Context**: `middleware/vendor_context.py`
- **Templates**: `app/templates/shop/account/`
## Deployment Notes
### Environment Variables
No new environment variables required. Uses existing:
- `JWT_SECRET_KEY` - for token signing
- `JWT_EXPIRE_MINUTES` - token expiry (default: 30)
- `ENVIRONMENT` - for secure cookie setting
### Database
No migrations required. Uses existing `customer` table.
### Static Files
Ensure these files exist:
- `static/shared/js/log-config.js`
- `static/shared/js/icons.js`
- `static/shop/js/shop-layout.js`
- `static/shared/js/utils.js`
- `static/shared/js/api-client.js`
- `static/shop/css/shop.css`
## Troubleshooting
### Issue: "No token for path"
**Cause**: Cookie path doesn't match request path
**Solution**:
- Check vendor context middleware is running
- Verify `detection_method` is set correctly
- Confirm cookie path calculation includes vendor subdomain for path-based access
### Issue: "Invalid token type"
**Cause**: Trying to use User token for customer route or vice versa
**Solution**:
- Ensure customer login creates token with `type: "customer"`
- Verify dependency checks `type == "customer"`
### Issue: Cookie not sent by browser
**Cause**: Cookie path doesn't match or cookie expired
**Solution**:
- Check browser DevTools → Application → Cookies
- Verify cookie path matches request URL
- Check cookie expiry timestamp
## Summary
This implementation establishes a complete customer authentication system that is:
**Secure**: HTTP-only cookies, CSRF protection, password hashing
**Scalable**: Multi-tenant with vendor isolation
**Flexible**: Supports domain, subdomain, and path-based access
**Maintainable**: Clear separation of concerns, follows established patterns
**User-Friendly**: Responsive design, theme integration, proper UX flows
The key architectural decision was recognizing that customers and users are fundamentally different entities requiring separate authentication flows, token structures, and database models.

View File

@@ -0,0 +1,82 @@
# Customer Authentication - Quick Summary
**Date**: 2025-11-24
**Full Documentation**: [CUSTOMER_AUTHENTICATION_IMPLEMENTATION.md](CUSTOMER_AUTHENTICATION_IMPLEMENTATION.md)
## What Was Implemented
✅ Customer login, registration, and forgot password pages
✅ Customer dashboard with account overview
✅ Complete customer authentication system separate from admin/vendor
✅ Multi-access routing support (domain, subdomain, path-based)
✅ Secure cookie management with proper path restrictions
✅ Theme integration and responsive design
✅ Custom logout confirmation modal (Tailwind CSS + Alpine.js)
## 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`
### Modified
- `app/api/v1/shop/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
- `middleware/vendor_context.py` - Harmonized detection methods
## Critical Architecture Decision
**Customers ≠ Users**
- **Users** (admin/vendor): Have `role`, `username`, managed by `auth_service`
- **Customers**: Vendor-scoped, have `customer_number`, managed by `customer_service`
JWT tokens have `type: "customer"` to distinguish them.
## Cookie Path Logic
```python
# Domain/Subdomain access
cookie_path = "/shop"
# Path-based access (/vendors/wizamart/shop)
cookie_path = f"/vendors/{vendor_code}/shop"
```
## Authentication Flow
1. Login → Create JWT with `type: "customer"`
2. Set cookie with vendor-aware path
3. Dashboard request → Cookie sent (path matches!)
4. Dependency decodes JWT, validates type, loads Customer
5. Render dashboard with customer data
## Logout Flow
1. User clicks "Logout" button → Custom Tailwind modal appears
2. User confirms → API call to `/api/v1/shop/auth/logout`
3. Cookie deleted, localStorage cleared
4. Success toast shown, redirect to login page
**Note**: Uses custom modal instead of browser's `confirm()` for better UX and styling consistency.
## Testing URLs
```
# Path-based access
http://localhost:8000/vendors/wizamart/shop/account/login
http://localhost:8000/vendors/wizamart/shop/account/register
http://localhost:8000/vendors/wizamart/shop/account/dashboard
```
## Next Steps (TODO)
- [ ] Implement password reset functionality
- [ ] Add email verification
- [ ] Build account management pages (orders, profile, addresses)
- [ ] Add refresh tokens for longer sessions
- [ ] Implement rate limiting on auth endpoints