/** * @ai-summary Auth service business logic * @ai-context Coordinates between Auth0 Management API and local user profile database */ import { auth0ManagementClient } from '../../../core/auth/auth0-management.client'; import { UserProfileRepository } from '../../user-profile/data/user-profile.repository'; import { TermsAgreementRepository } from '../../terms-agreement/data/terms-agreement.repository'; import { pool } from '../../../core/config/database'; import { logger } from '../../../core/logging/logger'; import { SignupRequest, SignupResponse, VerifyStatusResponse, ResendVerificationResponse, SecurityStatusResponse, PasswordResetResponse, TermsData, } from './auth.types'; export class AuthService { constructor( private userProfileRepository: UserProfileRepository, private termsAgreementRepository: TermsAgreementRepository ) {} /** * Create a new user account * 1. Create user in Auth0 (which automatically sends verification email) * 2. Create local user profile and terms agreement atomically */ async signup(request: SignupRequest, termsData: TermsData): Promise { const { email, password } = request; try { // Create user in Auth0 Management API // Auth0 automatically sends verification email on user creation const auth0UserId = await auth0ManagementClient.createUser({ email, password, }); logger.info('Auth0 user created', { auth0UserId, email }); // Create local user profile and terms agreement in a transaction const client = await pool.connect(); try { await client.query('BEGIN'); // Create user profile const profileQuery = ` INSERT INTO user_profiles (auth0_sub, email, subscription_tier) VALUES ($1, $2, 'free') RETURNING id `; const profileResult = await client.query(profileQuery, [auth0UserId, email]); const profileId = profileResult.rows[0].id; logger.info('User profile created', { userId: profileId, email }); // Create terms agreement await this.termsAgreementRepository.create({ userId: auth0UserId, ipAddress: termsData.ipAddress, userAgent: termsData.userAgent, termsVersion: termsData.termsVersion, termsUrl: termsData.termsUrl, termsContentHash: termsData.termsContentHash, }, client); logger.info('Terms agreement created', { userId: auth0UserId }); await client.query('COMMIT'); } catch (error) { await client.query('ROLLBACK'); logger.error('Transaction failed, rolling back', { error, email }); throw error; } finally { client.release(); } return { userId: auth0UserId, email, message: 'Account created successfully. Please check your email to verify your account.', }; } catch (error) { logger.error('Signup failed', { email, error }); // Check for duplicate email error from Auth0 if (error instanceof Error && error.message.includes('already exists')) { throw new Error('Email already exists'); } throw error; } } /** * Check email verification status * Queries Auth0 for current verification status and updates local database if changed */ async getVerifyStatus(auth0Sub: string): Promise { try { // Get user details from Auth0 const userDetails = await auth0ManagementClient.getUser(auth0Sub); logger.info('Retrieved user verification status from Auth0', { auth0Sub, emailVerified: userDetails.emailVerified, }); // Update local database if verification status changed const localProfile = await this.userProfileRepository.getByAuth0Sub(auth0Sub); if (localProfile && localProfile.emailVerified !== userDetails.emailVerified) { await this.userProfileRepository.updateEmailVerified( auth0Sub, userDetails.emailVerified ); logger.info('Local email verification status updated', { auth0Sub, emailVerified: userDetails.emailVerified, }); } return { emailVerified: userDetails.emailVerified, email: userDetails.email, }; } catch (error) { logger.error('Failed to get verification status', { auth0Sub, error }); throw error; } } /** * Resend verification email * Calls Auth0 Management API to trigger verification email */ async resendVerification(auth0Sub: string): Promise { try { // Check if already verified const verified = await auth0ManagementClient.checkEmailVerified(auth0Sub); if (verified) { logger.info('Email already verified, skipping resend', { auth0Sub }); return { message: 'Email is already verified', }; } // Request Auth0 to resend verification email await auth0ManagementClient.resendVerificationEmail(auth0Sub); logger.info('Verification email resent', { auth0Sub }); return { message: 'Verification email sent. Please check your inbox.', }; } catch (error) { logger.error('Failed to resend verification email', { auth0Sub, error }); throw error; } } /** * Resend verification email by email address (public endpoint) * Looks up user by email and sends verification if not verified */ async resendVerificationByEmail(email: string): Promise { try { // Look up user by email in our database const userProfile = await this.userProfileRepository.getByEmail(email); if (!userProfile) { // Don't reveal if email exists - return success message regardless logger.info('Resend verification requested for unknown email', { email: email.substring(0, 3) + '***', }); return { message: 'If an account exists with this email, a verification link will be sent.', }; } // Check if already verified via Auth0 const verified = await auth0ManagementClient.checkEmailVerified(userProfile.auth0Sub); if (verified) { logger.info('Email already verified, skipping resend', { email: email.substring(0, 3) + '***' }); return { message: 'If an account exists with this email, a verification link will be sent.', }; } // Request Auth0 to resend verification email await auth0ManagementClient.resendVerificationEmail(userProfile.auth0Sub); logger.info('Verification email resent via public endpoint', { email: email.substring(0, 3) + '***', }); return { message: 'If an account exists with this email, a verification link will be sent.', }; } catch (error) { logger.error('Failed to resend verification email by email', { email: email.substring(0, 3) + '***', error, }); // Don't reveal error details - return generic success for security return { message: 'If an account exists with this email, a verification link will be sent.', }; } } /** * Get user status for routing decisions * Returns email verification and onboarding completion status */ async getUserStatus(auth0Sub: string): Promise<{ emailVerified: boolean; onboardingCompleted: boolean; email: string; }> { try { // Get user details from Auth0 const auth0User = await auth0ManagementClient.getUser(auth0Sub); // Get local profile for onboarding status const localProfile = await this.userProfileRepository.getByAuth0Sub(auth0Sub); // Sync email verification status if needed if (localProfile && localProfile.emailVerified !== auth0User.emailVerified) { await this.userProfileRepository.updateEmailVerified(auth0Sub, auth0User.emailVerified); } return { emailVerified: auth0User.emailVerified, onboardingCompleted: localProfile?.onboardingCompletedAt !== null, email: auth0User.email, }; } catch (error) { logger.error('Failed to get user status', { auth0Sub, error }); throw error; } } /** * Get security status for the user * Returns email verification status and passkey info */ async getSecurityStatus(auth0Sub: string): Promise { try { // Get user details from Auth0 const auth0User = await auth0ManagementClient.getUser(auth0Sub); logger.info('Retrieved security status', { auth0Sub: auth0Sub.substring(0, 8) + '...', emailVerified: auth0User.emailVerified, }); return { emailVerified: auth0User.emailVerified, email: auth0User.email, // Passkeys are enabled at the Auth0 connection level, not per-user // This is informational - actual passkey enrollment happens in Auth0 Universal Login passkeysEnabled: true, // Auth0 doesn't expose password last changed date via Management API // Would require Auth0 Logs API or user_metadata to track this passwordLastChanged: null, }; } catch (error) { logger.error('Failed to get security status', { auth0Sub, error }); throw error; } } /** * Request password reset email * Triggers Auth0 to send password reset email to user */ async requestPasswordReset(auth0Sub: string): Promise { try { // Get user email from Auth0 const auth0User = await auth0ManagementClient.getUser(auth0Sub); // Send password reset email via Auth0 await auth0ManagementClient.sendPasswordResetEmail(auth0User.email); logger.info('Password reset email requested', { auth0Sub: auth0Sub.substring(0, 8) + '...', email: auth0User.email.substring(0, 3) + '***', }); return { message: 'Password reset email sent. Please check your inbox.', success: true, }; } catch (error) { logger.error('Failed to request password reset', { auth0Sub, error }); throw error; } } }