242 lines
7.0 KiB
Python
242 lines
7.0 KiB
Python
# app/api/v1/public/vendors/auth.py
|
|
"""
|
|
Customer authentication endpoints (public-facing).
|
|
|
|
Implements dual token storage with path restriction:
|
|
- Sets HTTP-only cookie with path=/shop (restricted to shop routes only)
|
|
- Returns token in response for localStorage (API calls)
|
|
|
|
This prevents:
|
|
- Customer cookies from being sent to admin or vendor routes
|
|
- Cross-context authentication confusion
|
|
"""
|
|
|
|
import logging
|
|
from fastapi import APIRouter, Depends, Response, Request
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.core.database import get_db
|
|
from app.services.customer_service import customer_service
|
|
from app.exceptions import VendorNotFoundException
|
|
from models.schema.auth import LoginResponse, UserLogin
|
|
from models.schema.customer import CustomerRegister, CustomerResponse
|
|
from models.database.vendor import Vendor
|
|
from app.api.deps import get_current_customer_api
|
|
from app.core.environment import should_use_secure_cookies
|
|
|
|
router = APIRouter(prefix="/auth")
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@router.post("/{vendor_id}/customers/register", response_model=CustomerResponse)
|
|
def register_customer(
|
|
vendor_id: int,
|
|
customer_data: CustomerRegister,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
Register a new customer for a specific vendor.
|
|
|
|
Customer accounts are vendor-scoped - each vendor has independent customers.
|
|
Same email can be used for different vendors.
|
|
"""
|
|
# Verify vendor exists and is active
|
|
vendor = db.query(Vendor).filter(
|
|
Vendor.id == vendor_id,
|
|
Vendor.is_active == True
|
|
).first()
|
|
|
|
if not vendor:
|
|
raise VendorNotFoundException(str(vendor_id), identifier_type="id")
|
|
|
|
# Create customer account
|
|
customer = customer_service.register_customer(
|
|
db=db,
|
|
vendor_id=vendor_id,
|
|
customer_data=customer_data
|
|
)
|
|
|
|
logger.info(
|
|
f"New customer registered: {customer.email} "
|
|
f"for vendor {vendor.vendor_code}"
|
|
)
|
|
|
|
return CustomerResponse.model_validate(customer)
|
|
|
|
|
|
@router.post("/{vendor_id}/customers/login", response_model=LoginResponse)
|
|
def customer_login(
|
|
vendor_id: int,
|
|
user_credentials: UserLogin,
|
|
response: Response,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
Customer login for a specific vendor.
|
|
|
|
Authenticates customer and returns JWT token.
|
|
Customer must belong to the specified vendor.
|
|
|
|
Sets token in two places:
|
|
1. HTTP-only cookie with path=/shop (for browser page navigation)
|
|
2. Response body (for localStorage and API calls)
|
|
|
|
The cookie is restricted to /shop/* routes only to prevent
|
|
it from being sent to admin or vendor routes.
|
|
"""
|
|
# Verify vendor exists and is active
|
|
vendor = db.query(Vendor).filter(
|
|
Vendor.id == vendor_id,
|
|
Vendor.is_active == True
|
|
).first()
|
|
|
|
if not vendor:
|
|
raise VendorNotFoundException(str(vendor_id), identifier_type="id")
|
|
|
|
# Authenticate customer
|
|
login_result = customer_service.login_customer(
|
|
db=db,
|
|
vendor_id=vendor_id,
|
|
credentials=user_credentials
|
|
)
|
|
|
|
logger.info(
|
|
f"Customer login successful: {login_result['customer'].email} "
|
|
f"for vendor {vendor.vendor_code}"
|
|
)
|
|
|
|
# Set HTTP-only cookie for browser navigation
|
|
# CRITICAL: path=/shop restricts cookie to shop routes only
|
|
response.set_cookie(
|
|
key="customer_token",
|
|
value=login_result["token_data"]["access_token"],
|
|
httponly=True, # JavaScript cannot access (XSS protection)
|
|
secure=should_use_secure_cookies(), # HTTPS only in production/staging
|
|
samesite="lax", # CSRF protection
|
|
max_age=login_result["token_data"]["expires_in"], # Match JWT expiry
|
|
path="/shop", # RESTRICTED TO SHOP ROUTES ONLY
|
|
)
|
|
|
|
logger.debug(
|
|
f"Set customer_token cookie with {login_result['token_data']['expires_in']}s expiry "
|
|
f"(path=/shop, httponly=True, secure={should_use_secure_cookies()})"
|
|
)
|
|
|
|
# Return full login response
|
|
return LoginResponse(
|
|
access_token=login_result["token_data"]["access_token"],
|
|
token_type=login_result["token_data"]["token_type"],
|
|
expires_in=login_result["token_data"]["expires_in"],
|
|
user=login_result["customer"], # Return customer as user
|
|
)
|
|
|
|
|
|
@router.post("/{vendor_id}/customers/logout")
|
|
def customer_logout(
|
|
vendor_id: int,
|
|
response: Response
|
|
):
|
|
"""
|
|
Customer logout.
|
|
|
|
Clears the customer_token cookie.
|
|
Client should also remove token from localStorage.
|
|
"""
|
|
logger.info(f"Customer logout for vendor {vendor_id}")
|
|
|
|
# Clear the cookie (must match path used when setting)
|
|
response.delete_cookie(
|
|
key="customer_token",
|
|
path="/shop",
|
|
)
|
|
|
|
logger.debug("Deleted customer_token cookie")
|
|
|
|
return {"message": "Logged out successfully"}
|
|
|
|
|
|
@router.post("/{vendor_id}/customers/forgot-password")
|
|
def forgot_password(
|
|
vendor_id: int,
|
|
email: str,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
Request password reset for customer.
|
|
|
|
Sends password reset email to customer if account exists.
|
|
"""
|
|
# Verify vendor exists
|
|
vendor = db.query(Vendor).filter(
|
|
Vendor.id == vendor_id,
|
|
Vendor.is_active == True
|
|
).first()
|
|
|
|
if not vendor:
|
|
raise VendorNotFoundException(str(vendor_id), identifier_type="id")
|
|
|
|
# TODO: Implement password reset logic
|
|
# - Generate reset token
|
|
# - Send email with reset link
|
|
# - Store token in database
|
|
|
|
logger.info(f"Password reset requested for {email} in vendor {vendor.vendor_code}")
|
|
|
|
return {
|
|
"message": "If an account exists, a password reset link has been sent",
|
|
"email": email
|
|
}
|
|
|
|
|
|
@router.post("/{vendor_id}/customers/reset-password")
|
|
def reset_password(
|
|
vendor_id: int,
|
|
token: str,
|
|
new_password: str,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
Reset customer password using reset token.
|
|
|
|
Validates token and updates password.
|
|
"""
|
|
# Verify vendor exists
|
|
vendor = db.query(Vendor).filter(
|
|
Vendor.id == vendor_id,
|
|
Vendor.is_active == True
|
|
).first()
|
|
|
|
if not vendor:
|
|
raise VendorNotFoundException(str(vendor_id), identifier_type="id")
|
|
|
|
# TODO: Implement password reset logic
|
|
# - Validate reset token
|
|
# - Check token expiration
|
|
# - Update password
|
|
# - Invalidate token
|
|
|
|
logger.info(f"Password reset completed for vendor {vendor.vendor_code}")
|
|
|
|
return {"message": "Password reset successful"}
|
|
|
|
|
|
@router.get("/{vendor_id}/customers/me")
|
|
def get_current_customer(
|
|
vendor_id: int,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
Get current authenticated customer.
|
|
|
|
This endpoint can be called to verify authentication and get customer info.
|
|
Requires customer authentication via cookie or header.
|
|
"""
|
|
|
|
# Note: This would need Request object to check cookies
|
|
# For now, just indicate the endpoint exists
|
|
# Implementation depends on how you want to structure it
|
|
|
|
return {
|
|
"message": "Customer info endpoint - implementation depends on auth structure"
|
|
}
|