""" Refactored Auth Operations with proper 3DS/3DS2 support Implements SetupIntent-first architecture for secure registration flows """ import logging from typing import Dict, Any from fastapi import APIRouter, Depends, HTTPException, status, Request from app.services.auth_service import auth_service, AuthService from app.schemas.auth import UserRegistration, UserLogin, UserResponse from app.models.users import User from shared.exceptions.auth_exceptions import ( UserCreationError, RegistrationError, PaymentOrchestrationError ) # Configure logging logger = logging.getLogger(__name__) # Create router router = APIRouter(prefix="/api/v1/auth", tags=["auth"]) async def get_auth_service() -> AuthService: """Dependency injection for auth service""" return auth_service @router.post("/start-registration", response_model=Dict[str, Any], summary="Start secure registration with payment verification") async def start_registration( user_data: UserRegistration, request: Request, auth_service: AuthService = Depends(get_auth_service) ) -> Dict[str, Any]: """ Start secure registration flow with SetupIntent-first approach This is the FIRST step in the new registration architecture: 1. Creates payment customer 2. Attaches payment method 3. Creates SetupIntent for verification 4. Returns SetupIntent to frontend for 3DS handling If 3DS is required, frontend must confirm SetupIntent and call complete-registration If no 3DS required, user is created immediately and registration completes Args: user_data: User registration data with payment info Returns: Registration result (may require 3DS) Raises: HTTPException: 400 for validation errors, 500 for server errors """ try: logger.info(f"Starting secure registration flow, email={user_data.email}, plan={user_data.subscription_plan}") # Validate required fields if not user_data.email or not user_data.email.strip(): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Email is required" ) if not user_data.password or len(user_data.password) < 8: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Password must be at least 8 characters long" ) if not user_data.full_name or not user_data.full_name.strip(): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Full name is required" ) if user_data.subscription_plan and not user_data.payment_method_id: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Payment method ID is required for subscription registration" ) # Start secure registration flow result = await auth_service.start_secure_registration_flow(user_data) # Check if 3DS is required if result.get('requires_action', False): logger.info(f"Registration requires 3DS verification, email={user_data.email}, setup_intent_id={result.get('setup_intent_id')}") return { "requires_action": True, "action_type": "setup_intent_confirmation", "client_secret": result.get('client_secret'), "setup_intent_id": result.get('setup_intent_id'), "customer_id": result.get('customer_id'), "payment_customer_id": result.get('payment_customer_id'), "plan_id": result.get('plan_id'), "payment_method_id": result.get('payment_method_id'), "billing_cycle": result.get('billing_cycle'), "coupon_info": result.get('coupon_info'), "trial_info": result.get('trial_info'), "email": result.get('email'), "message": "Payment verification required. Frontend must confirm SetupIntent to handle 3DS." } else: user = result.get('user') user_id = user.id if user else None logger.info(f"Registration completed without 3DS, email={user_data.email}, user_id={user_id}, subscription_id={result.get('subscription_id')}") # Return complete registration result user_data_response = None if user: user_data_response = { "id": str(user.id), "email": user.email, "full_name": user.full_name, "is_active": user.is_active } return { "requires_action": False, "user": user_data_response, "subscription_id": result.get('subscription_id'), "payment_customer_id": result.get('payment_customer_id'), "status": result.get('status'), "message": "Registration completed successfully" } except RegistrationError as e: logger.error(f"Registration flow failed: {str(e)}, email: {user_data.email}", exc_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Registration failed: {str(e)}" ) from e except PaymentOrchestrationError as e: logger.error(f"Payment orchestration failed: {str(e)}, email: {user_data.email}", exc_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Payment setup failed: {str(e)}" ) from e except Exception as e: logger.error(f"Unexpected registration error: {str(e)}, email: {user_data.email}", exc_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Registration error: {str(e)}" ) from e @router.post("/complete-registration", response_model=Dict[str, Any], summary="Complete registration after 3DS verification") async def complete_registration( verification_data: Dict[str, Any], request: Request, auth_service: AuthService = Depends(get_auth_service) ) -> Dict[str, Any]: """ Complete registration after frontend confirms SetupIntent (3DS handled) This is the SECOND step in the registration architecture: 1. Called after frontend confirms SetupIntent 2. Called after user completes 3DS authentication (if required) 3. Verifies SetupIntent status 4. Creates subscription with verified payment method 5. Creates user record 6. Saves onboarding progress Args: verification_data: SetupIntent verification data Returns: Complete registration result Raises: HTTPException: 400 for validation errors, 500 for server errors """ try: # Validate required fields if not verification_data.get('setup_intent_id'): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="SetupIntent ID is required" ) if not verification_data.get('user_data'): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="User data is required" ) # Extract user data user_data_dict = verification_data['user_data'] user_data = UserRegistration(**user_data_dict) logger.info(f"Completing registration after SetupIntent verification, email={user_data.email}, setup_intent_id={verification_data['setup_intent_id']}") # Complete registration with verified payment result = await auth_service.complete_registration_with_verified_payment( verification_data['setup_intent_id'], user_data ) logger.info(f"Registration completed successfully after 3DS, user_id={result['user'].id}, email={result['user'].email}, subscription_id={result.get('subscription_id')}") return { "success": True, "user": { "id": str(result['user'].id), "email": result['user'].email, "full_name": result['user'].full_name, "is_active": result['user'].is_active, "is_verified": result['user'].is_verified, "created_at": result['user'].created_at.isoformat() if result['user'].created_at else None, "role": result['user'].role }, "subscription_id": result.get('subscription_id'), "payment_customer_id": result.get('payment_customer_id'), "status": result.get('status'), "access_token": result.get('access_token'), "refresh_token": result.get('refresh_token'), "message": "Registration completed successfully after 3DS verification" } except RegistrationError as e: logger.error(f"Registration completion after 3DS failed: {str(e)}, setup_intent_id: {verification_data.get('setup_intent_id')}, email: {user_data_dict.get('email') if user_data_dict else 'unknown'}", exc_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Registration completion failed: {str(e)}" ) from e except Exception as e: logger.error(f"Unexpected registration completion error: {str(e)}, setup_intent_id: {verification_data.get('setup_intent_id')}", exc_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Registration completion error: {str(e)}" ) from e @router.post("/login", response_model=Dict[str, Any], summary="User login with subscription validation") async def login( login_data: UserLogin, request: Request, auth_service: AuthService = Depends(get_auth_service) ) -> Dict[str, Any]: """ User login endpoint with subscription validation This endpoint: 1. Validates user credentials 2. Checks if user has active subscription (if required) 3. Returns authentication tokens 4. Updates last login timestamp Args: login_data: User login credentials (email and password) Returns: Authentication tokens and user information Raises: HTTPException: 401 for invalid credentials, 403 for subscription issues """ try: logger.info(f"Login attempt, email={login_data.email}") # Validate required fields if not login_data.email or not login_data.email.strip(): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Email is required" ) if not login_data.password or len(login_data.password) < 8: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Password must be at least 8 characters long" ) # Call auth service to perform login result = await auth_service.login_user(login_data) logger.info(f"Login successful, email={login_data.email}, user_id={result['user'].id}") return { "success": True, "user": { "id": str(result['user'].id), "email": result['user'].email, "full_name": result['user'].full_name, "is_active": result['user'].is_active, "last_login": result['user'].last_login.isoformat() if result['user'].last_login else None }, "tokens": result.get('tokens', {}), "subscription": result.get('subscription', {}), "message": "Login successful" } except HTTPException: # Re-raise HTTP exceptions (like 401 for invalid credentials) raise except Exception as e: logger.error(f"Login failed: {str(e)}, email: {login_data.email}", exc_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Login failed: {str(e)}" ) from e