# app/api/v1/vendor/auth.py """ Vendor team authentication endpoints. Implements dual token storage with path restriction: - Sets HTTP-only cookie with path=/vendor (restricted to vendor routes only) - Returns token in response for localStorage (API calls) This prevents: - Vendor cookies from being sent to admin routes - Admin cookies from being sent to vendor routes - Cross-context authentication confusion """ import logging from fastapi import APIRouter, Depends, Request, Response from pydantic import BaseModel from sqlalchemy.orm import Session from app.api.deps import get_current_vendor_api from app.core.database import get_db from app.core.environment import should_use_secure_cookies from app.exceptions import InvalidCredentialsException from app.services.auth_service import auth_service from middleware.vendor_context import get_current_vendor from models.database.user import User from models.schema.auth import LogoutResponse, UserLogin, VendorUserResponse router = APIRouter(prefix="/auth") logger = logging.getLogger(__name__) # Response model for vendor login class VendorLoginResponse(BaseModel): access_token: str token_type: str expires_in: int user: dict vendor: dict vendor_role: str @router.post("/login", response_model=VendorLoginResponse) def vendor_login( user_credentials: UserLogin, request: Request, response: Response, db: Session = Depends(get_db), ): """ Vendor team member login. Authenticates users who are part of a vendor team. Validates against vendor context if available. Sets token in two places: 1. HTTP-only cookie with path=/vendor (for browser page navigation) 2. Response body (for localStorage and API calls) Prevents admin users from logging into vendor portal. """ # Try to get vendor from middleware first vendor = get_current_vendor(request) # If no vendor from middleware, try to get from request body if not vendor and hasattr(user_credentials, "vendor_code"): vendor_code = getattr(user_credentials, "vendor_code", None) if vendor_code: vendor = auth_service.get_vendor_by_code(db, vendor_code) # Authenticate user login_result = auth_service.login_user(db=db, user_credentials=user_credentials) user = login_result["user"] # CRITICAL: Prevent admin users from using vendor login if user.role == "admin": logger.warning(f"Admin user attempted vendor login: {user.username}") raise InvalidCredentialsException( "Admins cannot access vendor portal. Please use admin portal." ) # Determine vendor and role vendor_role = "Member" if vendor: # Check if user has access to this vendor has_access, role = auth_service.get_user_vendor_role(db, user, vendor) if has_access: vendor_role = role else: logger.warning( f"User {user.username} attempted login to vendor {vendor.vendor_code} " f"but is not authorized" ) raise InvalidCredentialsException("You do not have access to this vendor") else: # No vendor context - find which vendor this user belongs to vendor, vendor_role = auth_service.find_user_vendor(user) if not vendor: raise InvalidCredentialsException("User is not associated with any vendor") logger.info( f"Vendor team login successful: {user.username} " f"for vendor {vendor.vendor_code} as {vendor_role}" ) # Create vendor-scoped access token with vendor information token_data = auth_service.auth_manager.create_access_token( user=user, vendor_id=vendor.id, vendor_code=vendor.vendor_code, vendor_role=vendor_role, ) # Set HTTP-only cookie for browser navigation # CRITICAL: path=/vendor restricts cookie to vendor routes only response.set_cookie( key="vendor_token", value=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=token_data["expires_in"], # Match JWT expiry path="/vendor", # RESTRICTED TO VENDOR ROUTES ONLY ) logger.debug( f"Set vendor_token cookie with {token_data['expires_in']}s expiry " f"(path=/vendor, httponly=True, secure={should_use_secure_cookies()})" ) # Return full login response with vendor-scoped token return VendorLoginResponse( access_token=token_data["access_token"], token_type=token_data["token_type"], expires_in=token_data["expires_in"], user={ "id": user.id, "username": user.username, "email": user.email, "role": user.role, "is_active": user.is_active, }, vendor={ "id": vendor.id, "vendor_code": vendor.vendor_code, "subdomain": vendor.subdomain, "name": vendor.name, "is_active": vendor.is_active, "is_verified": vendor.is_verified, }, vendor_role=vendor_role, ) @router.post("/logout", response_model=LogoutResponse) def vendor_logout(response: Response): """ Vendor team member logout. Clears the vendor_token cookie. Client should also remove token from localStorage. """ logger.info("Vendor logout") # Clear the cookie (must match path used when setting) response.delete_cookie( key="vendor_token", path="/vendor", ) logger.debug("Deleted vendor_token cookie") return LogoutResponse(message="Logged out successfully") @router.get("/me", response_model=VendorUserResponse) def get_current_vendor_user( user: User = Depends(get_current_vendor_api), db: Session = Depends(get_db) ): """ Get current authenticated vendor user. This endpoint can be called to verify authentication and get user info. Requires Authorization header (header-only authentication for API endpoints). """ return VendorUserResponse( id=user.id, username=user.username, email=user.email, role=user.role, is_active=user.is_active, )