Files
bakery-ia/services/auth/app/api/auth_operations.py
2026-01-14 13:15:48 +01:00

943 lines
36 KiB
Python

"""
Authentication Operations API Endpoints
Business logic for login, register, token refresh, password reset, and email verification
"""
from fastapi import APIRouter, Depends, HTTPException, status, Request
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.ext.asyncio import AsyncSession
from typing import Dict, Any
import structlog
from app.schemas.auth import (
UserRegistration, UserLogin, TokenResponse, RefreshTokenRequest,
PasswordChange, PasswordReset, UserResponse
)
from pydantic import BaseModel
from typing import Optional
# Schema for SetupIntent completion data
class SetupIntentCompletionData(BaseModel):
email: str
password: str
full_name: str
setup_intent_id: str
plan_id: str
payment_method_id: str
billing_interval: str = "monthly"
coupon_code: Optional[str] = None
from app.services.auth_service import EnhancedAuthService
from app.models.users import User
from app.core.database import get_db
from shared.database.base import create_database_manager
from shared.monitoring.decorators import track_execution_time
from shared.monitoring.metrics import get_metrics_collector
from shared.auth.decorators import get_current_user_dep
from app.core.config import settings
logger = structlog.get_logger()
router = APIRouter(tags=["auth-operations"])
security = HTTPBearer()
def get_auth_service():
"""Dependency injection for EnhancedAuthService"""
database_manager = create_database_manager(settings.DATABASE_URL, "auth-service")
return EnhancedAuthService(database_manager)
@router.post("/api/v1/auth/register", response_model=TokenResponse)
@track_execution_time("enhanced_registration_duration_seconds", "auth-service")
async def register(
user_data: UserRegistration,
request: Request,
auth_service: EnhancedAuthService = Depends(get_auth_service)
):
"""Register new user using enhanced repository pattern"""
metrics = get_metrics_collector(request)
logger.info("Registration attempt using repository pattern",
email=user_data.email)
try:
# Enhanced input validation
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"
)
# Register user using enhanced service
result = await auth_service.register_user(user_data)
# Record successful registration
if metrics:
metrics.increment_counter("enhanced_registration_total", labels={"status": "success"})
logger.info("Registration successful using repository pattern",
user_id=result.user.id,
email=user_data.email)
return result
except HTTPException as e:
if metrics:
error_type = "validation_error" if e.status_code == 400 else "conflict" if e.status_code == 409 else "failed"
metrics.increment_counter("enhanced_registration_total", labels={"status": error_type})
logger.warning("Registration failed using repository pattern",
email=user_data.email,
error=e.detail)
raise
except Exception as e:
if metrics:
metrics.increment_counter("enhanced_registration_total", labels={"status": "error"})
logger.error("Registration system error using repository pattern",
email=user_data.email,
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Registration failed"
)
@router.post("/api/v1/auth/register-with-subscription")
async def register_with_subscription(
user_data: UserRegistration,
request: Request,
auth_service: EnhancedAuthService = Depends(get_auth_service)
):
"""
Register new user and create subscription in one call
NEW ARCHITECTURE: User is ONLY created AFTER payment verification
Flow:
1. Validate user data
2. Create payment customer via tenant service
3. Create SetupIntent via tenant service
4. If SetupIntent requires_action: Return SetupIntent data WITHOUT creating user
5. If no SetupIntent required: Create user, create subscription, return tokens
The subscription will be linked to a tenant during the onboarding flow.
"""
metrics = get_metrics_collector(request)
logger.info("Registration with subscription attempt using secure architecture",
email=user_data.email)
try:
# Enhanced input validation
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"
)
# NEW ARCHITECTURE: Create payment customer and SetupIntent BEFORE user creation
if user_data.subscription_plan and user_data.payment_method_id:
logger.info("Step 1: Creating payment customer and SetupIntent BEFORE user creation",
email=user_data.email,
plan=user_data.subscription_plan)
# Use tenant service orchestration endpoint for payment setup
# This creates payment customer and SetupIntent in one coordinated workflow
payment_setup_result = await auth_service.create_registration_payment_setup_via_tenant_service(
user_data=user_data
)
if not payment_setup_result or not payment_setup_result.get('success'):
logger.error("Payment setup failed",
email=user_data.email,
error="Payment setup returned no success")
if metrics:
metrics.increment_counter("enhanced_registration_with_subscription_total",
labels={"status": "failed_payment_setup"})
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Payment setup failed"
)
# CRITICAL: Check if SetupIntent requires 3DS authentication
if payment_setup_result.get('requires_action'):
# NEW ARCHITECTURE: Return SetupIntent data WITHOUT creating user
logger.info("Payment setup requires SetupIntent authentication - deferring user creation",
email=user_data.email,
action_type=payment_setup_result.get('action_type'),
setup_intent_id=payment_setup_result.get('setup_intent_id'))
if metrics:
metrics.increment_counter("enhanced_registration_with_subscription_total",
labels={"status": "requires_3ds"})
# Return SetupIntent data for frontend to handle 3DS
# NO user created yet, NO tokens returned
return {
"requires_action": True,
"action_type": payment_setup_result.get('action_type'),
"client_secret": payment_setup_result.get('client_secret'),
"setup_intent_id": payment_setup_result.get('setup_intent_id'),
"customer_id": payment_setup_result.get('customer_id'),
"payment_customer_id": payment_setup_result.get('payment_customer_id'),
"plan_id": payment_setup_result.get('plan_id'),
"payment_method_id": payment_setup_result.get('payment_method_id'),
"trial_period_days": payment_setup_result.get('trial_period_days'),
"email": payment_setup_result.get('email'),
"full_name": payment_setup_result.get('full_name'),
"billing_interval": payment_setup_result.get('billing_interval'),
"coupon_code": payment_setup_result.get('coupon_code'),
"message": payment_setup_result.get('message') or "Payment verification required before account creation"
}
else:
# No 3DS required - proceed with user creation
logger.info("No SetupIntent required - proceeding with user creation",
email=user_data.email)
else:
# No subscription data provided - proceed with user creation
logger.info("No subscription data provided - proceeding with user creation",
email=user_data.email)
# Step 2: Create user (ONLY if no SetupIntent required)
logger.info("Step 2: Creating user after payment verification",
email=user_data.email)
result = await auth_service.register_user(user_data)
user_id = result.user.id
logger.info("User created successfully",
user_id=user_id,
email=user_data.email)
# Step 3: If subscription was created (no 3DS), store in onboarding progress
if user_data.subscription_plan and user_data.payment_method_id:
subscription_id = payment_setup_result.get("subscription_id")
if subscription_id:
logger.info("Tenant-independent subscription created successfully",
user_id=user_id,
subscription_id=subscription_id)
# Store subscription data in onboarding progress
await auth_service.save_subscription_to_onboarding_progress(
user_id=user_id,
subscription_id=subscription_id,
registration_data=user_data
)
logger.info("Subscription data stored in onboarding progress",
user_id=user_id)
result.subscription_id = subscription_id
else:
logger.warning("No subscription ID returned, but user registration succeeded",
user_id=user_id)
# Record successful registration
if metrics:
metrics.increment_counter("enhanced_registration_with_subscription_total",
labels={"status": "success"})
logger.info("Registration with subscription completed successfully using secure architecture",
user_id=user_id,
email=user_data.email,
subscription_id=result.subscription_id)
return result
except HTTPException:
raise
except Exception as e:
if metrics:
error_type = "validation_error" if "validation" in str(e).lower() else "conflict" if "conflict" in str(e).lower() else "failed"
metrics.increment_counter("enhanced_registration_with_subscription_total",
labels={"status": error_type})
logger.error("Registration with subscription system error using secure architecture",
email=user_data.email,
error=str(e),
exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Registration with subscription failed: " + str(e)
)
# Record successful registration
if metrics:
metrics.increment_counter("enhanced_registration_with_subscription_total", labels={"status": "success"})
logger.info("Registration with subscription completed successfully using new architecture",
user_id=user_id,
email=user_data.email,
subscription_id=subscription_id)
# Add subscription_id to the response
result.subscription_id = subscription_id
# Check if subscription creation requires 3DS/SetupIntent authentication
if subscription_result and subscription_result.get('requires_action'):
result.requires_action = subscription_result.get('requires_action')
result.action_type = subscription_result.get('action_type')
result.client_secret = subscription_result.get('client_secret')
result.setup_intent_id = subscription_result.get('setup_intent_id')
result.payment_intent_id = subscription_result.get('payment_intent_id') # Legacy, deprecated
# Include data needed for post-3DS subscription completion
result.customer_id = subscription_result.get('customer_id')
result.plan_id = user_data.subscription_plan
result.payment_method_id = user_data.payment_method_id
result.trial_period_days = subscription_result.get('trial_period_days')
result.user_id = user_id
result.billing_interval = user_data.billing_cycle or "monthly"
result.message = subscription_result.get('message')
logger.info("Registration requires SetupIntent authentication",
user_id=user_id,
requires_action=result.requires_action,
action_type=result.action_type,
setup_intent_id=result.setup_intent_id)
return result
except HTTPException as e:
if metrics:
error_type = "validation_error" if e.status_code == 400 else "conflict" if e.status_code == 409 else "failed"
metrics.increment_counter("enhanced_registration_with_subscription_total", labels={"status": error_type})
logger.warning("Registration with subscription failed using new architecture",
email=user_data.email,
error=e.detail)
raise
except Exception as e:
if metrics:
metrics.increment_counter("enhanced_registration_with_subscription_total", labels={"status": "error"})
logger.error("Registration with subscription system error using new architecture",
email=user_data.email,
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Registration with subscription failed"
)
@router.post("/api/v1/auth/login", response_model=TokenResponse)
@track_execution_time("enhanced_login_duration_seconds", "auth-service")
async def login(
login_data: UserLogin,
request: Request,
auth_service: EnhancedAuthService = Depends(get_auth_service)
):
"""Login user using enhanced repository pattern"""
metrics = get_metrics_collector(request)
logger.info("Login attempt using repository pattern",
email=login_data.email)
try:
# Enhanced input validation
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:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Password is required"
)
# Login using enhanced service
result = await auth_service.login_user(login_data)
# Record successful login
if metrics:
metrics.increment_counter("enhanced_login_success_total")
logger.info("Login successful using repository pattern",
user_id=result.user.id,
email=login_data.email)
return result
except HTTPException as e:
if metrics:
reason = "validation_error" if e.status_code == 400 else "auth_failed"
metrics.increment_counter("enhanced_login_failure_total", labels={"reason": reason})
logger.warning("Login failed using repository pattern",
email=login_data.email,
error=e.detail)
raise
except Exception as e:
if metrics:
metrics.increment_counter("enhanced_login_failure_total", labels={"reason": "error"})
logger.error("Login system error using repository pattern",
email=login_data.email,
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Login failed"
)
@router.post("/api/v1/auth/refresh")
@track_execution_time("enhanced_token_refresh_duration_seconds", "auth-service")
async def refresh_token(
refresh_data: RefreshTokenRequest,
request: Request,
auth_service: EnhancedAuthService = Depends(get_auth_service)
):
"""Refresh access token using repository pattern"""
metrics = get_metrics_collector(request)
try:
result = await auth_service.refresh_access_token(refresh_data.refresh_token)
# Record successful refresh
if metrics:
metrics.increment_counter("enhanced_token_refresh_success_total")
logger.debug("Access token refreshed using repository pattern")
return result
except HTTPException as e:
if metrics:
metrics.increment_counter("enhanced_token_refresh_failure_total")
logger.warning("Token refresh failed using repository pattern", error=e.detail)
raise
except Exception as e:
if metrics:
metrics.increment_counter("enhanced_token_refresh_failure_total")
logger.error("Token refresh error using repository pattern", error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Token refresh failed"
)
@router.post("/api/v1/auth/verify")
@track_execution_time("enhanced_token_verify_duration_seconds", "auth-service")
async def verify_token(
credentials: HTTPAuthorizationCredentials = Depends(security),
request: Request = None,
auth_service: EnhancedAuthService = Depends(get_auth_service)
):
"""Verify access token using repository pattern"""
metrics = get_metrics_collector(request) if request else None
try:
if not credentials:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Authentication required"
)
result = await auth_service.verify_user_token(credentials.credentials)
# Record successful verification
if metrics:
metrics.increment_counter("enhanced_token_verify_success_total")
return {
"valid": True,
"user_id": result.get("user_id"),
"email": result.get("email"),
"role": result.get("role"),
"exp": result.get("exp"),
"message": None
}
except HTTPException as e:
if metrics:
metrics.increment_counter("enhanced_token_verify_failure_total")
logger.warning("Token verification failed using repository pattern", error=e.detail)
raise
except Exception as e:
if metrics:
metrics.increment_counter("enhanced_token_verify_failure_total")
logger.error("Token verification error using repository pattern", error=str(e))
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token"
)
@router.post("/api/v1/auth/logout")
@track_execution_time("enhanced_logout_duration_seconds", "auth-service")
async def logout(
refresh_data: RefreshTokenRequest,
request: Request,
credentials: HTTPAuthorizationCredentials = Depends(security),
auth_service: EnhancedAuthService = Depends(get_auth_service)
):
"""Logout user using repository pattern"""
metrics = get_metrics_collector(request)
try:
# Verify token to get user_id
payload = await auth_service.verify_user_token(credentials.credentials)
user_id = payload.get("user_id")
if not user_id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token"
)
success = await auth_service.logout_user(user_id, refresh_data.refresh_token)
if metrics:
status_label = "success" if success else "failed"
metrics.increment_counter("enhanced_logout_total", labels={"status": status_label})
logger.info("Logout using repository pattern",
user_id=user_id,
success=success)
return {"message": "Logout successful" if success else "Logout failed"}
except HTTPException:
raise
except Exception as e:
if metrics:
metrics.increment_counter("enhanced_logout_total", labels={"status": "error"})
logger.error("Logout error using repository pattern", error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Logout failed"
)
@router.post("/api/v1/auth/change-password")
async def change_password(
password_data: PasswordChange,
credentials: HTTPAuthorizationCredentials = Depends(security),
request: Request = None,
auth_service: EnhancedAuthService = Depends(get_auth_service)
):
"""Change user password using repository pattern"""
metrics = get_metrics_collector(request) if request else None
try:
if not credentials:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Authentication required"
)
# Verify current token
payload = await auth_service.verify_user_token(credentials.credentials)
user_id = payload.get("user_id")
if not user_id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token"
)
# Validate new password length
if len(password_data.new_password) < 8:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="New password must be at least 8 characters long"
)
# Change password using enhanced service
success = await auth_service.change_password(
user_id,
password_data.current_password,
password_data.new_password
)
if metrics:
status_label = "success" if success else "failed"
metrics.increment_counter("enhanced_password_change_total", labels={"status": status_label})
logger.info("Password changed using repository pattern",
user_id=user_id,
success=success)
return {"message": "Password changed successfully"}
except HTTPException:
raise
except Exception as e:
if metrics:
metrics.increment_counter("enhanced_password_change_total", labels={"status": "error"})
logger.error("Password change error using repository pattern", error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Password change failed"
)
@router.get("/api/v1/auth/me", response_model=UserResponse)
async def get_profile(
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Get user profile - works for JWT auth AND demo sessions"""
logger.info(f"📋 Profile request received",
user_id=current_user.get("user_id"),
is_demo=current_user.get("is_demo", False),
demo_session_id=current_user.get("demo_session_id", ""),
email=current_user.get("email", ""),
path="/api/v1/auth/me")
try:
user_id = current_user.get("user_id")
if not user_id:
logger.error(f"❌ No user_id in current_user context for profile request",
current_user=current_user)
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid user context"
)
logger.info(f"🔎 Fetching user profile for user_id: {user_id}",
is_demo=current_user.get("is_demo", False),
demo_session_id=current_user.get("demo_session_id", ""))
# Fetch user from database
from app.repositories import UserRepository
user_repo = UserRepository(User, db)
user = await user_repo.get_by_id(user_id)
if not user:
logger.error(f"🚨 User not found in database",
user_id=user_id,
is_demo=current_user.get("is_demo", False))
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User profile not found"
)
logger.info(f"🎉 User profile found",
user_id=user.id,
email=user.email,
full_name=user.full_name,
is_active=user.is_active)
return UserResponse(
id=str(user.id),
email=user.email,
full_name=user.full_name,
is_active=user.is_active,
is_verified=user.is_verified,
phone=user.phone,
language=user.language or "es",
timezone=user.timezone or "Europe/Madrid",
created_at=user.created_at,
last_login=user.last_login,
role=user.role,
tenant_id=current_user.get("tenant_id")
)
except HTTPException:
raise
except Exception as e:
logger.error("Get profile error", error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get profile"
)
@router.put("/api/v1/auth/me", response_model=UserResponse)
async def update_profile(
update_data: dict,
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Update user profile - works for JWT auth AND demo sessions"""
try:
user_id = current_user.get("user_id")
if not user_id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid user context"
)
# Prepare update data - filter out read-only fields
from app.repositories import UserRepository
user_repo = UserRepository(User, db)
# Update user profile
updated_user = await user_repo.update(user_id, update_data)
if not updated_user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
logger.info("Profile updated",
user_id=user_id,
updated_fields=list(update_data.keys()))
return UserResponse(
id=str(updated_user.id),
email=updated_user.email,
full_name=updated_user.full_name,
is_active=updated_user.is_active,
is_verified=updated_user.is_verified,
phone=updated_user.phone,
language=updated_user.language,
timezone=updated_user.timezone,
created_at=updated_user.created_at,
last_login=updated_user.last_login,
role=updated_user.role,
tenant_id=current_user.get("tenant_id")
)
except HTTPException:
raise
except Exception as e:
logger.error("Update profile error", error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update profile"
)
@router.post("/api/v1/auth/verify-email")
async def verify_email(
user_id: str,
verification_token: str,
auth_service: EnhancedAuthService = Depends(get_auth_service)
):
"""Verify user email using repository pattern"""
try:
success = await auth_service.verify_user_email(user_id, verification_token)
logger.info("Email verification using repository pattern",
user_id=user_id,
success=success)
return {"message": "Email verified successfully" if success else "Email verification failed"}
except Exception as e:
logger.error("Email verification error using repository pattern", error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Email verification failed"
)
@router.post("/api/v1/auth/reset-password")
async def reset_password(
reset_data: PasswordReset,
request: Request,
auth_service: EnhancedAuthService = Depends(get_auth_service)
):
"""Request password reset using repository pattern"""
metrics = get_metrics_collector(request)
try:
# In a full implementation, you'd send an email with a reset token
# For now, just log the request
if metrics:
metrics.increment_counter("enhanced_password_reset_total", labels={"status": "requested"})
logger.info("Password reset requested using repository pattern",
email=reset_data.email)
return {"message": "Password reset email sent if account exists"}
except Exception as e:
if metrics:
metrics.increment_counter("enhanced_password_reset_total", labels={"status": "error"})
logger.error("Password reset error using repository pattern", error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Password reset failed"
)
@router.post("/api/v1/auth/complete-registration-after-setup-intent")
@track_execution_time("registration_completion_duration_seconds", "auth-service")
async def complete_registration_after_setup_intent(
completion_data: SetupIntentCompletionData,
request: Request,
auth_service: EnhancedAuthService = Depends(get_auth_service)
):
"""
Complete user registration after SetupIntent confirmation
This endpoint is called by the frontend after 3DS authentication is complete.
It ensures users are only created after payment verification.
Args:
completion_data: Data from frontend including SetupIntent ID and user info
Returns:
TokenResponse with access_token, refresh_token, and user data
Raises:
HTTPException: 400 if SetupIntent not succeeded
HTTPException: 500 if registration fails
"""
metrics = get_metrics_collector(request)
logger.info("Completing registration after SetupIntent confirmation",
email=completion_data.email,
setup_intent_id=completion_data.setup_intent_id)
try:
# Step 1: Verify SetupIntent using tenant service orchestration
logger.info("Step 1: Verifying SetupIntent using orchestration service",
setup_intent_id=completion_data.setup_intent_id)
verification_result = await auth_service.verify_setup_intent_via_tenant_service(
completion_data.setup_intent_id
)
if not verification_result or verification_result.get('status') != 'succeeded':
status_code = status.HTTP_400_BAD_REQUEST
detail = f"SetupIntent not succeeded: {verification_result.get('status') if verification_result else 'unknown'}"
logger.warning("SetupIntent verification failed via orchestration service",
email=completion_data.email,
setup_intent_id=completion_data.setup_intent_id,
status=verification_result.get('status') if verification_result else 'unknown')
if metrics:
metrics.increment_counter("registration_completion_total", labels={"status": "failed_verification"})
raise HTTPException(status_code=status_code, detail=detail)
logger.info("SetupIntent verification succeeded via orchestration service",
setup_intent_id=completion_data.setup_intent_id)
# Step 2: Create user (ONLY after payment verification)
logger.info("Step 2: Creating user after successful payment verification",
email=completion_data.email)
user_data = UserRegistration(
email=completion_data.email,
password=completion_data.password,
full_name=completion_data.full_name,
subscription_plan=completion_data.plan_id,
payment_method_id=completion_data.payment_method_id,
billing_cycle=completion_data.billing_interval,
coupon_code=completion_data.coupon_code
)
registration_result = await auth_service.register_user(user_data)
logger.info("User created successfully after payment verification",
user_id=registration_result.user.id,
email=completion_data.email)
# Step 3: Create subscription (now that user exists)
logger.info("Step 3: Creating subscription for verified user",
user_id=registration_result.user.id,
plan_id=completion_data.plan_id)
subscription_result = await auth_service.create_subscription_via_tenant_service(
user_id=registration_result.user.id,
plan_id=completion_data.plan_id,
payment_method_id=completion_data.payment_method_id,
billing_cycle=completion_data.billing_interval,
coupon_code=completion_data.coupon_code
)
if not subscription_result or not subscription_result.get('success'):
logger.error("Subscription creation failed after successful user registration",
user_id=registration_result.user.id,
error="Subscription creation returned no success")
if metrics:
metrics.increment_counter("registration_completion_total", labels={"status": "failed_subscription"})
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Subscription creation failed after user registration"
)
logger.info("Subscription created successfully",
user_id=registration_result.user.id,
subscription_id=subscription_result.get('subscription_id'))
# Step 4: Return tokens and subscription data
registration_result.subscription_id = subscription_result.get('subscription_id')
if metrics:
metrics.increment_counter("registration_completion_total", labels={"status": "success"})
logger.info("Registration completed successfully after SetupIntent confirmation",
user_id=registration_result.user.id,
email=completion_data.email,
subscription_id=subscription_result.get('subscription_id'))
return registration_result
except HTTPException:
raise
except Exception as e:
if metrics:
metrics.increment_counter("registration_completion_total", labels={"status": "error"})
logger.error("Registration completion system error",
email=completion_data.email,
setup_intent_id=completion_data.setup_intent_id,
error=str(e),
exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Registration completion failed: " + str(e)
)
@router.get("/api/v1/auth/health")
async def health_check():
"""Health check endpoint for enhanced auth service"""
return {
"status": "healthy",
"service": "enhanced-auth-service",
"version": "2.0.0",
"features": ["repository-pattern", "dependency-injection", "enhanced-error-handling"]
}