# 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" }