feat: implement customer authentication with JWT tokens

Implement secure customer authentication system with dedicated JWT tokens,
separate from admin/vendor authentication.

Backend Changes:
- Add customer JWT token support in deps.py
  - New get_current_customer_from_cookie_or_header dependency
  - Validates customer-specific tokens with type checking
  - Returns Customer object instead of User for shop routes
- Extend AuthService with customer token support
  - Add verify_password() method
  - Add create_access_token_with_data() for custom token payloads
- Update CustomerService authentication
  - Generate customer-specific JWT tokens with type="customer"
  - Use vendor-scoped customer lookup
- Enhance exception handler
  - Sanitize validation errors to prevent password leaks in logs
  - Fix shop login redirect to support multi-access routing
- Improve vendor context detection from Referer header
  - Consistent "path" detection method for cookie path logic

Schema Changes:
- Rename UserLogin.username to email_or_username for flexibility
- Update field validators accordingly

API Changes:
- Update admin/vendor auth endpoints to use email_or_username
- Customer auth already uses email field correctly

Route Changes:
- Update shop account routes to use Customer dependency
- Add /account redirect (without trailing slash)
- Change parameter names from current_user to current_customer

Frontend Changes:
- Update login forms to use email_or_username in API calls
- Change button text from "Log in" to "Sign in" for consistency
- Improve loading spinner layout with flexbox

Security Improvements:
- Customer tokens scoped to vendor_id
- Token type validation prevents cross-context token usage
- Password inputs redacted from validation error logs

🤖 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:08:49 +01:00
parent 1f2ccb4668
commit 6735d99df2
13 changed files with 219 additions and 81 deletions

View File

@@ -149,15 +149,15 @@ class CustomerService:
customer = db.query(Customer).filter(
and_(
Customer.vendor_id == vendor_id,
Customer.email == credentials.username.lower()
Customer.email == credentials.email_or_username.lower()
)
).first()
if not customer:
raise InvalidCustomerCredentialsException()
# Verify password
if not self.auth_service.verify_password(
# Verify password using auth_manager directly
if not self.auth_service.auth_manager.verify_password(
credentials.password,
customer.hashed_password
):
@@ -168,14 +168,30 @@ class CustomerService:
raise CustomerNotActiveException(customer.email)
# Generate JWT token with customer context
token_data = self.auth_service.create_access_token(
data={
"sub": str(customer.id),
"email": customer.email,
"vendor_id": vendor_id,
"type": "customer"
}
)
# Use auth_manager directly since Customer is not a User model
from datetime import datetime, timedelta, timezone
from jose import jwt
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",
"exp": expire,
"iat": datetime.now(timezone.utc),
}
token = jwt.encode(payload, auth_manager.secret_key, algorithm=auth_manager.algorithm)
token_data = {
"access_token": token,
"token_type": "bearer",
"expires_in": auth_manager.token_expire_minutes * 60,
}
logger.info(
f"Customer login successful: {customer.email} "