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:
@@ -98,7 +98,7 @@ class AuthService:
|
||||
"""
|
||||
try:
|
||||
user = self.auth_manager.authenticate_user(
|
||||
db, user_credentials.username, user_credentials.password
|
||||
db, user_credentials.email_or_username, user_credentials.password
|
||||
)
|
||||
if not user:
|
||||
raise InvalidCredentialsException("Incorrect username or password")
|
||||
@@ -161,6 +161,52 @@ class AuthService:
|
||||
logger.error(f"Error hashing password: {str(e)}")
|
||||
raise ValidationException("Failed to hash password")
|
||||
|
||||
def verify_password(self, plain_password: str, hashed_password: str) -> bool:
|
||||
"""Verify password against hash."""
|
||||
try:
|
||||
return self.auth_manager.verify_password(plain_password, hashed_password)
|
||||
except Exception as e:
|
||||
logger.error(f"Error verifying password: {str(e)}")
|
||||
return False
|
||||
|
||||
def create_access_token_with_data(self, data: dict) -> dict:
|
||||
"""
|
||||
Create JWT token with custom data payload.
|
||||
|
||||
Useful for non-User entities like customers that need tokens.
|
||||
|
||||
Args:
|
||||
data: Dictionary containing token payload data (must include 'sub')
|
||||
|
||||
Returns:
|
||||
Dictionary with access_token, token_type, and expires_in
|
||||
"""
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from jose import jwt
|
||||
from app.core.config import settings
|
||||
|
||||
try:
|
||||
expires_delta = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
expire = datetime.now(timezone.utc) + expires_delta
|
||||
|
||||
# Build payload with provided data
|
||||
payload = {
|
||||
**data,
|
||||
"exp": expire,
|
||||
"iat": datetime.now(timezone.utc),
|
||||
}
|
||||
|
||||
token = jwt.encode(payload, settings.SECRET_KEY, algorithm="HS256")
|
||||
|
||||
return {
|
||||
"access_token": token,
|
||||
"token_type": "bearer",
|
||||
"expires_in": settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating access token with data: {str(e)}")
|
||||
raise ValidationException("Failed to create access token")
|
||||
|
||||
# Private helper methods
|
||||
def _email_exists(self, db: Session, email: str) -> bool:
|
||||
"""Check if email already exists."""
|
||||
|
||||
@@ -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} "
|
||||
|
||||
Reference in New Issue
Block a user