970 lines
45 KiB
Python
970 lines
45 KiB
Python
"""
|
|
Enhanced Authentication Service
|
|
Updated to use repository pattern with dependency injection and improved error handling
|
|
"""
|
|
|
|
import uuid
|
|
from datetime import datetime, timedelta, timezone
|
|
from typing import Dict, Any, Optional
|
|
|
|
from fastapi import HTTPException, status
|
|
import structlog
|
|
|
|
from app.repositories import UserRepository, TokenRepository
|
|
from app.schemas.auth import UserRegistration, UserLogin, TokenResponse, UserResponse
|
|
from app.models.users import User
|
|
from app.models.tokens import RefreshToken
|
|
from app.core.security import SecurityManager
|
|
from app.utils.subscription_fetcher import SubscriptionFetcher
|
|
from shared.messaging import UnifiedEventPublisher, EVENT_TYPES
|
|
from shared.database.unit_of_work import UnitOfWork
|
|
from shared.database.transactions import transactional
|
|
from shared.database.exceptions import DatabaseError, ValidationError, DuplicateRecordError
|
|
|
|
|
|
logger = structlog.get_logger()
|
|
|
|
|
|
# Legacy compatibility alias
|
|
AuthService = None # Will be set at the end of the file
|
|
|
|
|
|
class EnhancedAuthService:
|
|
"""Enhanced authentication service using repository pattern"""
|
|
|
|
def __init__(self, database_manager, event_publisher=None):
|
|
"""Initialize service with database manager and optional event publisher"""
|
|
self.database_manager = database_manager
|
|
self.event_publisher = event_publisher
|
|
|
|
async def register_user(
|
|
self,
|
|
user_data: UserRegistration
|
|
) -> TokenResponse:
|
|
"""Register a new user using repository pattern"""
|
|
try:
|
|
async with self.database_manager.get_session() as db_session:
|
|
async with UnitOfWork(db_session) as uow:
|
|
# Register repositories
|
|
user_repo = uow.register_repository("users", UserRepository, User)
|
|
token_repo = uow.register_repository("tokens", TokenRepository, RefreshToken)
|
|
|
|
# Check if user already exists
|
|
existing_user = await user_repo.get_by_email(user_data.email)
|
|
if existing_user:
|
|
raise DuplicateRecordError("User with this email already exists")
|
|
|
|
# Validate password strength
|
|
if not SecurityManager.validate_password(user_data.password):
|
|
raise ValueError("Password does not meet security requirements")
|
|
|
|
# Create user data
|
|
# Default to admin role for first-time registrations during onboarding flow
|
|
# Users creating their own bakery should have admin privileges
|
|
user_role = user_data.role if user_data.role else "admin"
|
|
hashed_password = SecurityManager.hash_password(user_data.password)
|
|
|
|
create_data = {
|
|
"email": user_data.email,
|
|
"full_name": user_data.full_name,
|
|
"hashed_password": hashed_password,
|
|
"is_active": True,
|
|
"is_verified": False,
|
|
"role": user_role
|
|
}
|
|
|
|
# Create user using repository
|
|
new_user = await user_repo.create_user(create_data)
|
|
|
|
logger.debug("User created with repository pattern",
|
|
user_id=new_user.id,
|
|
email=user_data.email,
|
|
role=user_role)
|
|
|
|
# Create tokens with different payloads
|
|
access_token_data = {
|
|
"user_id": str(new_user.id),
|
|
"email": new_user.email,
|
|
"full_name": new_user.full_name,
|
|
"is_verified": new_user.is_verified,
|
|
"is_active": new_user.is_active,
|
|
"role": new_user.role,
|
|
"type": "access"
|
|
}
|
|
|
|
refresh_token_data = {
|
|
"user_id": str(new_user.id),
|
|
"email": new_user.email,
|
|
"type": "refresh"
|
|
}
|
|
|
|
# Generate tokens
|
|
access_token = SecurityManager.create_access_token(user_data=access_token_data)
|
|
refresh_token_value = SecurityManager.create_refresh_token(user_data=refresh_token_data)
|
|
|
|
# Store refresh token using repository
|
|
token_data = {
|
|
"user_id": str(new_user.id),
|
|
"token": refresh_token_value,
|
|
"expires_at": datetime.now(timezone.utc) + timedelta(days=30),
|
|
"is_revoked": False
|
|
}
|
|
|
|
await token_repo.create_token(token_data)
|
|
|
|
# Record GDPR consent if provided
|
|
if (user_data.terms_accepted or user_data.privacy_accepted or
|
|
user_data.marketing_consent or user_data.analytics_consent):
|
|
try:
|
|
from app.models.consent import UserConsent, ConsentHistory
|
|
|
|
ip_address = None # Would need to pass from request context
|
|
user_agent = None # Would need to pass from request context
|
|
|
|
consent = UserConsent(
|
|
user_id=new_user.id,
|
|
terms_accepted=user_data.terms_accepted if user_data.terms_accepted is not None else True,
|
|
privacy_accepted=user_data.privacy_accepted if user_data.privacy_accepted is not None else True,
|
|
marketing_consent=user_data.marketing_consent if user_data.marketing_consent is not None else False,
|
|
analytics_consent=user_data.analytics_consent if user_data.analytics_consent is not None else False,
|
|
consent_version="1.0",
|
|
consent_method="registration",
|
|
ip_address=ip_address,
|
|
user_agent=user_agent,
|
|
consented_at=datetime.now(timezone.utc)
|
|
)
|
|
|
|
db_session.add(consent)
|
|
await db_session.flush()
|
|
|
|
# Create consent history entry
|
|
history = ConsentHistory(
|
|
user_id=new_user.id,
|
|
consent_id=consent.id,
|
|
action="granted",
|
|
consent_snapshot={
|
|
"terms_accepted": consent.terms_accepted,
|
|
"privacy_accepted": consent.privacy_accepted,
|
|
"marketing_consent": consent.marketing_consent,
|
|
"analytics_consent": consent.analytics_consent,
|
|
"consent_version": "1.0",
|
|
"consent_method": "registration"
|
|
},
|
|
ip_address=ip_address,
|
|
user_agent=user_agent,
|
|
consent_method="registration",
|
|
created_at=datetime.now(timezone.utc)
|
|
)
|
|
db_session.add(history)
|
|
|
|
logger.info("User consent recorded during registration",
|
|
user_id=new_user.id,
|
|
terms_accepted=consent.terms_accepted,
|
|
privacy_accepted=consent.privacy_accepted,
|
|
marketing_consent=consent.marketing_consent,
|
|
analytics_consent=consent.analytics_consent)
|
|
except Exception as e:
|
|
logger.error("Failed to record user consent during registration",
|
|
user_id=new_user.id,
|
|
error=str(e))
|
|
# Re-raise to ensure registration fails if consent can't be recorded
|
|
raise
|
|
|
|
# Payment customer creation via tenant service
|
|
# The auth service calls the tenant service to create payment customer
|
|
# This maintains proper separation of concerns while providing seamless user experience
|
|
|
|
try:
|
|
# Call tenant service to create payment customer
|
|
from shared.clients.tenant_client import TenantServiceClient
|
|
from app.core.config import settings
|
|
|
|
tenant_client = TenantServiceClient(settings)
|
|
|
|
# Prepare user data for tenant service
|
|
user_data_for_tenant = {
|
|
"user_id": str(new_user.id),
|
|
"email": user_data.email,
|
|
"full_name": user_data.full_name,
|
|
"name": user_data.full_name
|
|
}
|
|
|
|
# Call tenant service to create payment customer
|
|
payment_result = await tenant_client.create_payment_customer(
|
|
user_data_for_tenant,
|
|
user_data.payment_method_id
|
|
)
|
|
|
|
if payment_result and payment_result.get("success"):
|
|
# Store payment customer ID from tenant service response
|
|
new_user.payment_customer_id = payment_result.get("payment_customer_id")
|
|
|
|
logger.info("Payment customer created successfully via tenant service",
|
|
user_id=new_user.id,
|
|
payment_customer_id=new_user.payment_customer_id,
|
|
payment_method_id=user_data.payment_method_id)
|
|
else:
|
|
logger.warning("Payment customer creation via tenant service returned no success",
|
|
user_id=new_user.id,
|
|
result=payment_result)
|
|
|
|
except Exception as e:
|
|
logger.error("Payment customer creation via tenant service failed",
|
|
user_id=new_user.id,
|
|
error=str(e))
|
|
# Don't fail registration if payment customer creation fails
|
|
# This allows users to register even if payment system is temporarily unavailable
|
|
new_user.payment_customer_id = None
|
|
|
|
# Store payment method ID if provided (will be used by tenant service)
|
|
if user_data.payment_method_id:
|
|
new_user.default_payment_method_id = user_data.payment_method_id
|
|
logger.info("Payment method ID stored for later use by tenant service",
|
|
user_id=new_user.id,
|
|
payment_method_id=user_data.payment_method_id)
|
|
|
|
# Store subscription plan selection in onboarding progress BEFORE committing
|
|
# This ensures it's part of the same transaction
|
|
if user_data.subscription_plan or user_data.payment_method_id or user_data.billing_cycle or user_data.coupon_code:
|
|
try:
|
|
from app.repositories.onboarding_repository import OnboardingRepository
|
|
from app.models.onboarding import UserOnboardingProgress
|
|
|
|
# Use upsert_user_step instead of save_step_data to avoid double commits
|
|
onboarding_repo = OnboardingRepository(db_session)
|
|
plan_data = {
|
|
"subscription_plan": user_data.subscription_plan or "starter",
|
|
"subscription_tier": user_data.subscription_plan or "starter", # Store tier for enterprise onboarding logic
|
|
"billing_cycle": user_data.billing_cycle or "monthly",
|
|
"coupon_code": user_data.coupon_code,
|
|
"payment_method_id": user_data.payment_method_id,
|
|
"payment_customer_id": new_user.payment_customer_id, # Now created via tenant service
|
|
"saved_at": datetime.now(timezone.utc).isoformat()
|
|
}
|
|
|
|
# Create the onboarding step record with plan data
|
|
# Note: We use completed=True to mark user_registered as complete
|
|
# auto_commit=False to let UnitOfWork handle the commit
|
|
await onboarding_repo.upsert_user_step(
|
|
user_id=str(new_user.id),
|
|
step_name="user_registered",
|
|
completed=True,
|
|
step_data=plan_data,
|
|
auto_commit=False
|
|
)
|
|
|
|
logger.info("Subscription plan and parameters saved to onboarding progress",
|
|
user_id=new_user.id,
|
|
plan=user_data.subscription_plan,
|
|
billing_cycle=user_data.billing_cycle,
|
|
coupon_code=user_data.coupon_code,
|
|
payment_method_id=user_data.payment_method_id,
|
|
payment_customer_id=new_user.payment_customer_id)
|
|
except Exception as e:
|
|
logger.error("Failed to save subscription plan and parameters to onboarding progress",
|
|
user_id=new_user.id,
|
|
error=str(e))
|
|
# Re-raise to ensure registration fails if onboarding data can't be saved
|
|
raise
|
|
|
|
# Commit transaction (includes user, tokens, consent, and onboarding data)
|
|
await uow.commit()
|
|
|
|
# Publish registration event (non-blocking)
|
|
if self.event_publisher:
|
|
try:
|
|
await self.event_publisher.publish_business_event(
|
|
event_type="auth.user.registered",
|
|
tenant_id="system", # User registration is system-wide initially
|
|
data={
|
|
"user_id": str(new_user.id),
|
|
"email": new_user.email,
|
|
"full_name": new_user.full_name,
|
|
"role": new_user.role,
|
|
"registered_at": datetime.now(timezone.utc).isoformat(),
|
|
"subscription_plan": user_data.subscription_plan or "starter"
|
|
}
|
|
)
|
|
except Exception as e:
|
|
logger.warning("Failed to publish registration event: %s", str(e))
|
|
|
|
logger.info("User registered successfully using repository pattern",
|
|
user_id=new_user.id,
|
|
email=user_data.email)
|
|
|
|
from app.schemas.auth import UserData
|
|
return TokenResponse(
|
|
access_token=access_token,
|
|
refresh_token=refresh_token_value,
|
|
token_type="bearer",
|
|
expires_in=1800,
|
|
user=UserData(
|
|
id=str(new_user.id),
|
|
email=new_user.email,
|
|
full_name=new_user.full_name,
|
|
is_active=new_user.is_active,
|
|
is_verified=new_user.is_verified,
|
|
created_at=new_user.created_at.isoformat() if new_user.created_at else datetime.now(timezone.utc).isoformat(),
|
|
role=new_user.role
|
|
)
|
|
)
|
|
|
|
except (ValidationError, DuplicateRecordError):
|
|
raise
|
|
except Exception as e:
|
|
logger.error("Registration failed using repository pattern",
|
|
email=user_data.email,
|
|
error=str(e))
|
|
raise DatabaseError(f"Registration failed: {str(e)}")
|
|
|
|
async def login_user(
|
|
self,
|
|
login_data: UserLogin
|
|
) -> TokenResponse:
|
|
"""Login user using repository pattern"""
|
|
try:
|
|
async with self.database_manager.get_session() as db_session:
|
|
async with UnitOfWork(db_session) as uow:
|
|
# Register repositories
|
|
user_repo = uow.register_repository("users", UserRepository, User)
|
|
token_repo = uow.register_repository("tokens", TokenRepository, RefreshToken)
|
|
|
|
# Authenticate user using repository
|
|
user = await user_repo.authenticate_user(login_data.email, login_data.password)
|
|
if not user:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Invalid email or password"
|
|
)
|
|
|
|
if not user.is_active:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Account is deactivated"
|
|
)
|
|
|
|
# Revoke existing refresh tokens using repository
|
|
await token_repo.revoke_all_user_tokens(str(user.id))
|
|
|
|
logger.debug("Existing tokens revoked using repository pattern",
|
|
user_id=user.id)
|
|
|
|
# NEW: Fetch subscription data for JWT enrichment
|
|
# This happens ONCE at login, not per-request
|
|
from app.core.config import settings
|
|
subscription_fetcher = SubscriptionFetcher(
|
|
tenant_service_url=settings.TENANT_SERVICE_URL # Now properly configurable
|
|
)
|
|
|
|
# Get service token for inter-service communication
|
|
service_token = await self._get_service_token()
|
|
|
|
subscription_context = await subscription_fetcher.get_user_subscription_context(
|
|
user_id=str(user.id),
|
|
service_token=service_token
|
|
)
|
|
|
|
logger.debug("Fetched subscription context for JWT enrichment",
|
|
user_id=user.id,
|
|
subscription_tier=subscription_context.get("subscription", {}).get("tier"))
|
|
|
|
# Create tokens with different payloads
|
|
subscription_data = subscription_context.get("subscription") or {}
|
|
|
|
access_token_data = {
|
|
"user_id": str(user.id),
|
|
"email": user.email,
|
|
"full_name": user.full_name,
|
|
"is_verified": user.is_verified,
|
|
"is_active": user.is_active,
|
|
"role": user.role,
|
|
"type": "access",
|
|
# NEW: Add subscription data to JWT payload
|
|
"tenant_id": subscription_context.get("tenant_id"),
|
|
"tenant_role": subscription_context.get("tenant_role"),
|
|
"subscription": subscription_data,
|
|
"subscription_tier": subscription_data.get("tier", "starter"), # Add direct field for gateway
|
|
"subscription_from_jwt": True, # Flag for gateway to use JWT data
|
|
"tenant_access": subscription_context.get("tenant_access")
|
|
}
|
|
|
|
refresh_token_data = {
|
|
"user_id": str(user.id),
|
|
"email": user.email,
|
|
"type": "refresh",
|
|
"jti": str(uuid.uuid4())
|
|
}
|
|
|
|
# Generate tokens
|
|
access_token = SecurityManager.create_access_token(user_data=access_token_data)
|
|
refresh_token_value = SecurityManager.create_refresh_token(user_data=refresh_token_data)
|
|
|
|
# Store refresh token using repository
|
|
token_data = {
|
|
"user_id": str(user.id),
|
|
"token": refresh_token_value,
|
|
"expires_at": datetime.now(timezone.utc) + timedelta(days=30),
|
|
"is_revoked": False
|
|
}
|
|
|
|
await token_repo.create_token(token_data)
|
|
|
|
# Update last login using repository
|
|
await user_repo.update_last_login(str(user.id))
|
|
|
|
# Commit transaction
|
|
await uow.commit()
|
|
|
|
# Publish login event (non-blocking)
|
|
if self.event_publisher:
|
|
try:
|
|
await self.event_publisher.publish_business_event(
|
|
event_type="auth.user.login",
|
|
tenant_id="system",
|
|
data={
|
|
"user_id": str(user.id),
|
|
"email": user.email,
|
|
"login_at": datetime.now(timezone.utc).isoformat()
|
|
}
|
|
)
|
|
except Exception as e:
|
|
logger.warning("Failed to publish login event: %s", str(e))
|
|
|
|
logger.info("User logged in successfully using repository pattern",
|
|
user_id=user.id,
|
|
email=login_data.email)
|
|
|
|
from app.schemas.auth import UserData
|
|
return TokenResponse(
|
|
access_token=access_token,
|
|
refresh_token=refresh_token_value,
|
|
token_type="bearer",
|
|
expires_in=1800,
|
|
user=UserData(
|
|
id=str(user.id),
|
|
email=user.email,
|
|
full_name=user.full_name,
|
|
is_active=user.is_active,
|
|
is_verified=user.is_verified,
|
|
created_at=user.created_at.isoformat() if user.created_at else datetime.now(timezone.utc).isoformat(),
|
|
role=user.role
|
|
)
|
|
)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error("Login failed using repository pattern",
|
|
email=login_data.email,
|
|
error=str(e))
|
|
raise DatabaseError(f"Login failed: {str(e)}")
|
|
|
|
async def logout_user(self, user_id: str, refresh_token: str) -> bool:
|
|
"""Logout user using repository pattern"""
|
|
try:
|
|
async with self.database_manager.get_session() as session:
|
|
token_repo = TokenRepository(RefreshToken, session)
|
|
|
|
# Revoke specific refresh token using repository
|
|
success = await token_repo.revoke_token(user_id, refresh_token)
|
|
|
|
if success:
|
|
logger.info("User logged out successfully using repository pattern",
|
|
user_id=user_id)
|
|
return True
|
|
|
|
return False
|
|
|
|
except Exception as e:
|
|
logger.error("Logout failed using repository pattern",
|
|
user_id=user_id,
|
|
error=str(e))
|
|
return False
|
|
|
|
async def refresh_access_token(self, refresh_token: str) -> Dict[str, Any]:
|
|
"""Refresh access token using repository pattern"""
|
|
try:
|
|
# Verify refresh token
|
|
payload = SecurityManager.decode_token(refresh_token)
|
|
user_id = payload.get("user_id")
|
|
|
|
if not user_id:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Invalid refresh token"
|
|
)
|
|
|
|
async with self.database_manager.get_session() as session:
|
|
user_repo = UserRepository(User, session)
|
|
token_repo = TokenRepository(RefreshToken, session)
|
|
|
|
# Validate refresh token using repository
|
|
is_valid = await token_repo.validate_refresh_token(refresh_token, user_id)
|
|
if not is_valid:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Invalid or expired refresh token"
|
|
)
|
|
|
|
# Get user using repository
|
|
user = await user_repo.get_by_id(user_id)
|
|
if not user or not user.is_active:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="User not found or inactive"
|
|
)
|
|
|
|
# NEW: Fetch FRESH subscription data for token refresh
|
|
# This ensures subscription changes propagate within token expiry period
|
|
subscription_fetcher = SubscriptionFetcher(
|
|
tenant_service_url=settings.TENANT_SERVICE_URL # Now properly configurable
|
|
)
|
|
|
|
service_token = await self._get_service_token()
|
|
|
|
subscription_context = await subscription_fetcher.get_user_subscription_context(
|
|
user_id=str(user.id),
|
|
service_token=service_token
|
|
)
|
|
|
|
logger.debug("Fetched fresh subscription context for token refresh",
|
|
user_id=user.id,
|
|
subscription_tier=subscription_context.get("subscription", {}).get("tier"))
|
|
|
|
# Create new access token with updated subscription data
|
|
access_token_data = {
|
|
"user_id": str(user.id),
|
|
"email": user.email,
|
|
"full_name": user.full_name,
|
|
"is_verified": user.is_verified,
|
|
"is_active": user.is_active,
|
|
"role": user.role,
|
|
"type": "access",
|
|
# NEW: Add fresh subscription data to JWT payload
|
|
"tenant_id": subscription_context.get("tenant_id"),
|
|
"tenant_role": subscription_context.get("tenant_role"),
|
|
"subscription": subscription_context.get("subscription"),
|
|
"tenant_access": subscription_context.get("tenant_access")
|
|
}
|
|
|
|
new_access_token = SecurityManager.create_access_token(user_data=access_token_data)
|
|
|
|
logger.debug("Access token refreshed successfully using repository pattern",
|
|
user_id=user_id)
|
|
|
|
return {
|
|
"access_token": new_access_token,
|
|
"token_type": "bearer",
|
|
"expires_in": 1800
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error("Token refresh failed using repository pattern: %s", str(e))
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Token refresh failed"
|
|
)
|
|
|
|
async def verify_user_token(self, token: str) -> Dict[str, Any]:
|
|
"""Verify access token and return user info"""
|
|
try:
|
|
payload = SecurityManager.verify_token(token)
|
|
if not payload:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Invalid token"
|
|
)
|
|
|
|
# Handle service tokens (used for inter-service communication)
|
|
if payload.get("type") == "service":
|
|
logger.debug("Service token verified successfully",
|
|
service=payload.get("service"),
|
|
tenant_id=payload.get("tenant_id"))
|
|
return {
|
|
"valid": True,
|
|
"user_id": payload.get("user_id", f"{payload.get('service')}-service"),
|
|
"email": payload.get("email", f"{payload.get('service')}-service@internal"),
|
|
"role": payload.get("role", "admin"),
|
|
"exp": payload.get("exp"),
|
|
"service": payload.get("service"),
|
|
"tenant_id": payload.get("tenant_id")
|
|
}
|
|
|
|
# Handle regular user tokens
|
|
return payload
|
|
|
|
except Exception as e:
|
|
logger.error("Token verification error using repository pattern: %s", str(e))
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Invalid token"
|
|
)
|
|
|
|
async def get_user_profile(self, user_id: str) -> Optional[UserResponse]:
|
|
"""Get user profile using repository pattern"""
|
|
try:
|
|
async with self.database_manager.get_session() as session:
|
|
user_repo = UserRepository(User, session)
|
|
|
|
user = await user_repo.get_by_id(user_id)
|
|
if not user:
|
|
return None
|
|
|
|
return UserResponse(
|
|
id=str(user.id),
|
|
email=user.email,
|
|
full_name=user.full_name,
|
|
is_active=user.is_active,
|
|
is_verified=user.is_verified,
|
|
created_at=user.created_at,
|
|
role=user.role
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error("Failed to get user profile using repository pattern",
|
|
user_id=user_id,
|
|
error=str(e))
|
|
return None
|
|
|
|
async def update_user_profile(
|
|
self,
|
|
user_id: str,
|
|
update_data: Dict[str, Any]
|
|
) -> Optional[UserResponse]:
|
|
"""Update user profile using repository pattern"""
|
|
try:
|
|
async with self.database_manager.get_session() as session:
|
|
user_repo = UserRepository(User, session)
|
|
|
|
updated_user = await user_repo.update(user_id, update_data)
|
|
if not updated_user:
|
|
return None
|
|
|
|
logger.info("User profile updated using repository pattern",
|
|
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,
|
|
created_at=updated_user.created_at,
|
|
role=updated_user.role
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error("Failed to update user profile using repository pattern",
|
|
user_id=user_id,
|
|
error=str(e))
|
|
raise DatabaseError(f"Failed to update profile: {str(e)}")
|
|
|
|
async def change_password(
|
|
self,
|
|
user_id: str,
|
|
old_password: str,
|
|
new_password: str
|
|
) -> bool:
|
|
"""Change user password using repository pattern"""
|
|
try:
|
|
async with self.database_manager.get_session() as session:
|
|
user_repo = UserRepository(User, session)
|
|
token_repo = TokenRepository(RefreshToken, session)
|
|
|
|
# Get user and verify old password
|
|
user = await user_repo.get_by_id(user_id)
|
|
if not user:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="User not found"
|
|
)
|
|
|
|
if not SecurityManager.verify_password(old_password, user.hashed_password):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Invalid old password"
|
|
)
|
|
|
|
# Validate new password strength
|
|
if not SecurityManager.validate_password(new_password):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="New password does not meet security requirements"
|
|
)
|
|
|
|
# Hash new password and update
|
|
new_hashed_password = SecurityManager.hash_password(new_password)
|
|
await user_repo.update(user_id, {"hashed_password": new_hashed_password})
|
|
|
|
# Revoke all existing tokens for security
|
|
await token_repo.revoke_all_user_tokens(user_id)
|
|
|
|
logger.info("Password changed successfully using repository pattern",
|
|
user_id=user_id)
|
|
|
|
return True
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error("Failed to change password using repository pattern",
|
|
user_id=user_id,
|
|
error=str(e))
|
|
raise DatabaseError(f"Failed to change password: {str(e)}")
|
|
|
|
async def verify_user_email(self, user_id: str, verification_token: str) -> bool:
|
|
"""Verify user email using repository pattern"""
|
|
try:
|
|
async with self.database_manager.get_session() as session:
|
|
user_repo = UserRepository(User, session)
|
|
|
|
# In a real implementation, you'd verify the verification_token
|
|
# For now, just mark user as verified
|
|
updated_user = await user_repo.update(user_id, {"is_verified": True})
|
|
|
|
if updated_user:
|
|
logger.info("User email verified using repository pattern",
|
|
user_id=user_id)
|
|
return True
|
|
|
|
return False
|
|
|
|
except Exception as e:
|
|
logger.error("Failed to verify email using repository pattern",
|
|
user_id=user_id,
|
|
error=str(e))
|
|
return False
|
|
|
|
async def deactivate_user(self, user_id: str, admin_user_id: str) -> bool:
|
|
"""Deactivate user account using repository pattern"""
|
|
try:
|
|
async with self.database_manager.get_session() as session:
|
|
user_repo = UserRepository(User, session)
|
|
token_repo = TokenRepository(RefreshToken, session)
|
|
|
|
# Update user status
|
|
updated_user = await user_repo.update(user_id, {"is_active": False})
|
|
if not updated_user:
|
|
return False
|
|
|
|
# Revoke all tokens
|
|
await token_repo.revoke_all_user_tokens(user_id)
|
|
|
|
logger.info("User deactivated using repository pattern",
|
|
user_id=user_id,
|
|
admin_user_id=admin_user_id)
|
|
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error("Failed to deactivate user using repository pattern",
|
|
user_id=user_id,
|
|
error=str(e))
|
|
return False
|
|
|
|
async def _get_service_token(self, tenant_id: Optional[str] = None) -> str:
|
|
"""
|
|
Get service token for inter-service communication.
|
|
This is used to fetch subscription data from tenant service.
|
|
|
|
Args:
|
|
tenant_id: Optional tenant ID for tenant-scoped service operations
|
|
|
|
Returns:
|
|
JWT service token
|
|
"""
|
|
try:
|
|
# Create a proper service token with JWT using SecurityManager
|
|
service_token = SecurityManager.create_service_token("auth-service", tenant_id)
|
|
|
|
logger.debug("Generated service token for tenant service communication", tenant_id=tenant_id)
|
|
return service_token
|
|
except Exception as e:
|
|
logger.error(f"Failed to get service token: {e}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="Failed to authenticate with tenant service"
|
|
)
|
|
|
|
|
|
async def create_subscription_via_tenant_service(
|
|
self,
|
|
user_id: str,
|
|
plan_id: str,
|
|
payment_method_id: str,
|
|
billing_cycle: str,
|
|
coupon_code: Optional[str] = None
|
|
) -> Optional[Dict[str, Any]]:
|
|
"""
|
|
Create a tenant-independent subscription via tenant service
|
|
|
|
This method calls the tenant service to create a subscription during user registration
|
|
that is not linked to any tenant. The subscription will be linked to a tenant
|
|
during the onboarding flow.
|
|
|
|
Args:
|
|
user_id: User ID
|
|
plan_id: Subscription plan ID
|
|
payment_method_id: Payment method ID
|
|
billing_cycle: Billing cycle (monthly/yearly)
|
|
coupon_code: Optional coupon code
|
|
|
|
Returns:
|
|
Dict with subscription creation results including:
|
|
- success: boolean
|
|
- subscription_id: string
|
|
- customer_id: string
|
|
- status: string
|
|
- plan: string
|
|
- billing_cycle: string
|
|
Returns None if creation fails
|
|
"""
|
|
try:
|
|
from shared.clients.tenant_client import TenantServiceClient
|
|
from shared.config.base import BaseServiceSettings
|
|
|
|
# Get the base settings to create tenant client
|
|
tenant_client = TenantServiceClient(BaseServiceSettings())
|
|
|
|
# Get user data for tenant service
|
|
user_data = await self.get_user_data_for_tenant_service(user_id)
|
|
|
|
logger.info("Creating tenant-independent subscription via tenant service",
|
|
user_id=user_id,
|
|
plan_id=plan_id)
|
|
|
|
# Call tenant service using the new dedicated method
|
|
result = await tenant_client.create_subscription_for_registration(
|
|
user_data=user_data,
|
|
plan_id=plan_id,
|
|
payment_method_id=payment_method_id,
|
|
billing_cycle=billing_cycle,
|
|
coupon_code=coupon_code
|
|
)
|
|
|
|
if result:
|
|
logger.info("Tenant-independent subscription created successfully via tenant service",
|
|
user_id=user_id,
|
|
subscription_id=result.get('subscription_id'))
|
|
return result
|
|
else:
|
|
logger.error("Tenant-independent subscription creation failed via tenant service",
|
|
user_id=user_id)
|
|
return None
|
|
|
|
except Exception as e:
|
|
logger.error("Failed to create tenant-independent subscription via tenant service",
|
|
user_id=user_id,
|
|
error=str(e))
|
|
return None
|
|
|
|
async def get_user_data_for_tenant_service(self, user_id: str) -> Dict[str, Any]:
|
|
"""
|
|
Get user data formatted for tenant service calls
|
|
|
|
Args:
|
|
user_id: User ID
|
|
|
|
Returns:
|
|
Dict with user data including email, name, etc.
|
|
"""
|
|
try:
|
|
# Get user from database
|
|
async with self.database_manager.get_session() as db_session:
|
|
async with UnitOfWork(db_session) as uow:
|
|
user_repo = uow.register_repository("users", UserRepository, User)
|
|
|
|
user = await user_repo.get_by_id(user_id)
|
|
|
|
if not user:
|
|
raise ValueError(f"User {user_id} not found")
|
|
|
|
return {
|
|
"user_id": str(user.id),
|
|
"email": user.email,
|
|
"full_name": user.full_name,
|
|
"name": user.full_name
|
|
}
|
|
except Exception as e:
|
|
logger.error("Failed to get user data for tenant service",
|
|
user_id=user_id,
|
|
error=str(e))
|
|
raise
|
|
|
|
async def save_subscription_to_onboarding_progress(
|
|
self,
|
|
user_id: str,
|
|
subscription_id: str,
|
|
registration_data: UserRegistration
|
|
) -> None:
|
|
"""
|
|
Save subscription data to the user's onboarding progress
|
|
|
|
This method stores subscription information in the onboarding progress
|
|
so it can be retrieved later during the tenant creation step.
|
|
|
|
Args:
|
|
user_id: User ID
|
|
subscription_id: Subscription ID created by tenant service
|
|
registration_data: Original registration data including plan, payment method, etc.
|
|
"""
|
|
try:
|
|
from app.repositories.onboarding_repository import OnboardingRepository
|
|
from app.models.onboarding import UserOnboardingProgress
|
|
|
|
# Prepare subscription data to store
|
|
subscription_data = {
|
|
"subscription_id": subscription_id,
|
|
"plan_id": registration_data.subscription_plan,
|
|
"payment_method_id": registration_data.payment_method_id,
|
|
"billing_cycle": registration_data.billing_cycle or "monthly",
|
|
"coupon_code": registration_data.coupon_code,
|
|
"created_at": datetime.now(timezone.utc).isoformat()
|
|
}
|
|
|
|
logger.info("Saving subscription data to onboarding progress",
|
|
user_id=user_id,
|
|
subscription_id=subscription_id)
|
|
|
|
# Save to onboarding progress
|
|
async with self.database_manager.get_session() as db_session:
|
|
async with UnitOfWork(db_session) as uow:
|
|
onboarding_repo = uow.register_repository(
|
|
"onboarding",
|
|
OnboardingRepository,
|
|
UserOnboardingProgress
|
|
)
|
|
|
|
# Save or update the subscription step data
|
|
await onboarding_repo.save_step_data(
|
|
user_id=user_id,
|
|
step_name="subscription",
|
|
step_data=subscription_data,
|
|
auto_commit=False
|
|
)
|
|
|
|
# Commit the transaction
|
|
await uow.commit()
|
|
|
|
logger.info("Subscription data saved successfully to onboarding progress",
|
|
user_id=user_id,
|
|
subscription_id=subscription_id)
|
|
|
|
except Exception as e:
|
|
logger.error("Failed to save subscription data to onboarding progress",
|
|
user_id=user_id,
|
|
subscription_id=subscription_id,
|
|
error=str(e))
|
|
# Don't raise - we don't want to fail the registration if this fails
|
|
# The subscription was already created, so the user can still proceed
|
|
|
|
# Legacy compatibility - alias EnhancedAuthService as AuthService
|
|
AuthService = EnhancedAuthService
|
|
|
|
|
|
class EnhancedUserService(EnhancedAuthService):
|
|
"""User service alias for backward compatibility"""
|
|
pass |