import { Injectable, UnauthorizedException, ConflictException, BadRequestException, } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { User, SESSION_STATUS, OTP_TYPE, OTP_PURPOSE } from '@prisma/client'; import { UserCoreService } from 'src/core/user-core/user-core.service'; import { UserCredentialCoreService } from 'src/core/user-credential-core/user-credential-core.service'; import { UserSessionCoreService } from 'src/core/user-session-core/user-session-core.service'; import { UserOtpCoreService } from 'src/core/user-otp-core/user-otp-core.service'; import { UserInterestCoreService } from 'src/core/user-interest-core/user-interest-core.service'; import { CommonService } from 'src/shared/modules/common/common.service'; import { EmailService } from 'src/shared/modules/email/email.service'; import { SmsService } from 'src/shared/modules/sms/sms.service'; import { UserRegisterDto } from './dto/register.dto'; import { UserLoginDto } from './dto/login.dto'; import { UserRefreshTokenDto } from './dto/refresh-token.dto'; import { VerifyEmailOtpDto } from './dto/verify-email-otp.dto'; import { ResendEmailOtpDto } from './dto/resend-email-otp.dto'; import { RegisterPhoneDto } from './dto/register-phone.dto'; import { VerifyPhoneOtpDto } from './dto/verify-phone-otp.dto'; import { CompleteProfileDto } from './dto/complete-profile.dto'; import { UserSessionType } from 'src/shared/types/user-session.type'; import { accessTokenSignSettings, refreshTokenSignSettings, refreshTokenVerifySettings, TOKEN_TYPE, TOKEN_USER_TYPE, AuthMessages, } from 'src/shared/keys/auth.keys'; import { getUniqueId } from 'src/shared/modules/common/common.helper'; import { UNIQUE_ID_ENUM, USER_STATUS_ENUM } from 'src/keys'; @Injectable() export class UserAuthService { constructor( private readonly jwtService: JwtService, private readonly userCoreService: UserCoreService, private readonly userCredentialCoreService: UserCredentialCoreService, private readonly userSessionCoreService: UserSessionCoreService, private readonly userOtpCoreService: UserOtpCoreService, private readonly userInterestCoreService: UserInterestCoreService, private readonly commonService: CommonService, private readonly emailService: EmailService, private readonly smsService: SmsService, ) {} // PHASE 1: Initial Registration async register(registerDto: UserRegisterDto): Promise<{ message: string }> { const { email, password, firstName, lastName } = registerDto; // Check if user already exists const existingUser = await this.userCoreService .findFirst({ where: { email, isDeleted: false }, }) .catch(() => null); if (existingUser) { throw new ConflictException('User with this email already exists'); } // Create combined name fields const firstNameLastName = `${firstName} ${lastName}`; const lastNameFirstName = `${lastName} ${firstName}`; // Create user const user = await this.userCoreService.create({ data: { uniqueId: getUniqueId(UNIQUE_ID_ENUM.User), email, firstName, lastName, firstNameLastName, lastNameFirstName, status: USER_STATUS_ENUM.PENDING_EMAIL_VERIFICATION, emailVerified: false, phoneVerified: false, }, }); // Hash and store password const hashedPassword = await this.commonService.hashPassword(password); await this.userCredentialCoreService.create({ data: { userId: user.id, password: hashedPassword, }, }); // Generate and send OTP await this.generateAndSendOtp( user.id, email, OTP_TYPE.EMAIL, OTP_PURPOSE.EMAIL_VERIFICATION, firstName, ); return { message: 'Registration successful. Please check your email for OTP verification.', }; } // PHASE 2: Email Verification async verifyEmailOtp( verifyDto: VerifyEmailOtpDto, ): Promise<{ message: string }> { const { email, otp } = verifyDto; const user = await this.userCoreService.findFirst({ where: { email, isDeleted: false }, }); if (!user) { throw new BadRequestException('User not found'); } if (user.emailVerified) { throw new BadRequestException('Email already verified'); } await this.verifyOtp( user.id, otp, OTP_TYPE.EMAIL, OTP_PURPOSE.EMAIL_VERIFICATION, ); // Update user status await this.userCoreService.update({ where: { id: user.id }, data: { emailVerified: true, status: USER_STATUS_ENUM.PENDING_PHONE_VERIFICATION, }, }); return { message: 'Email verified successfully. Please register your phone number.', }; } async resendEmailOtp( resendDto: ResendEmailOtpDto, ): Promise<{ message: string }> { const { email } = resendDto; const user = await this.userCoreService.findFirst({ where: { email, isDeleted: false }, }); if (!user) { throw new BadRequestException('User not found'); } if (user.emailVerified) { throw new BadRequestException('Email already verified'); } await this.generateAndSendOtp( user.id, email, OTP_TYPE.EMAIL, OTP_PURPOSE.EMAIL_VERIFICATION, user.firstName || undefined, ); return { message: 'OTP resent successfully' }; } // PHASE 3: Phone Registration async registerPhone( registerPhoneDto: RegisterPhoneDto, ): Promise<{ message: string }> { const { email, code, phone } = registerPhoneDto; const user = await this.userCoreService.findFirst({ where: { email, isDeleted: false }, }); if (!user) { throw new BadRequestException('User not found'); } if (!user.emailVerified) { throw new BadRequestException('Please verify your email first'); } if (user.phoneVerified) { throw new BadRequestException('Phone already registered'); } // Check if phone already exists const existingPhone = await this.userCoreService .findFirst({ where: { phone, isDeleted: false }, }) .catch(() => null); if (existingPhone) { throw new ConflictException('Phone number already registered'); } // Update user with phone await this.userCoreService.update({ where: { id: user.id }, data: { phone, code }, }); // Generate and send OTP await this.generateAndSendOtp( user.id, `${phone}`, OTP_TYPE.SMS, OTP_PURPOSE.PHONE_VERIFICATION, ); return { message: 'Phone registered. Please check your SMS for OTP verification.', }; } // PHASE 4: Phone Verification async verifyPhoneOtp( verifyDto: VerifyPhoneOtpDto, ): Promise<{ message: string }> { const { email, otp } = verifyDto; const user = await this.userCoreService.findFirst({ where: { email, isDeleted: false }, }); if (!user) { throw new BadRequestException('User not found'); } if (!user.emailVerified) { throw new BadRequestException('Please verify your email first'); } if (user.phoneVerified) { throw new BadRequestException('Phone already verified'); } await this.verifyOtp( user.id, otp, OTP_TYPE.SMS, OTP_PURPOSE.PHONE_VERIFICATION, ); // Update user status await this.userCoreService.update({ where: { id: user.id }, data: { phoneVerified: true, status: USER_STATUS_ENUM.PENDING_PROFILE_COMPLETION, }, }); return { message: 'Phone verified successfully. Please complete your profile to activate your account.', }; } // PHASE 5: Complete Profile async completeProfile( completeDto: CompleteProfileDto, request: any, ): Promise<{ user: User; accessToken: string; refreshToken: string; }> { const { email, genreIds, dateOfBirth, country, city, bio } = completeDto; const user = await this.userCoreService.findFirst({ where: { email, isDeleted: false }, }); if (!user) { throw new BadRequestException('User not found'); } if (!user.emailVerified || !user.phoneVerified) { throw new BadRequestException('Please verify your email and phone first'); } if (user.status === USER_STATUS_ENUM.ENABLED) { throw new BadRequestException('Profile already completed'); } // Update user profile const updatedUser = await this.userCoreService.update({ where: { id: user.id }, data: { dateOfBirth: dateOfBirth ? new Date(dateOfBirth) : undefined, country, city, bio, status: USER_STATUS_ENUM.ENABLED, }, }); // Save user interests (if provided) if (genreIds && genreIds.length > 0) { for (const genreId of genreIds) { await this.userInterestCoreService.create({ data: { userId: user.id, genreId, }, }); } } // Generate tokens const { accessToken, refreshToken } = await this.getNewToken(updatedUser); // Get client info const clientInfo = this.commonService.getClientInfo(request); // Create session await this.userSessionCoreService.create({ data: { userId: updatedUser.id, accessToken, refreshToken, ipAddress: clientInfo.ipAddress, userAgent: clientInfo.userAgent, geoIpCountry: clientInfo.geoLocation?.country || '', city: clientInfo.geoLocation?.city || '', state: clientInfo.geoLocation?.region || '', latitude: clientInfo.geoLocation?.ll?.[0] || null, longitude: clientInfo.geoLocation?.ll?.[1] || null, status: SESSION_STATUS.CURRENT, loginAt: new Date(), }, }); // Send welcome email (don't block profile completion if email fails) try { await this.emailService.sendWelcomeEmail( email, user.firstName || 'User', user.lastName || '', ); } catch (emailError) { // Log error but don't fail the request console.error('Failed to send welcome email:', emailError); } return { user: updatedUser, accessToken, refreshToken, }; } // Login (Only for verified users) async login( loginDto: UserLoginDto, request: any, ): Promise<{ user: User; accessToken: string; refreshToken: string; }> { const { email, password } = loginDto; // Find user const user = await this.userCoreService .findFirst({ where: { email, isDeleted: false }, }) .catch(() => null); if (!user) { throw new UnauthorizedException('Invalid credentials'); } // Check if user is verified if (user.status !== USER_STATUS_ENUM.ENABLED) { throw new UnauthorizedException( 'Please complete your registration process', ); } // Get user credential const credential = await this.userCredentialCoreService .findFirst({ where: { userId: user.id, isDeleted: false }, }) .catch(() => null); if (!credential) { throw new UnauthorizedException('Invalid credentials'); } // Verify password const isPasswordValid = await this.commonService.comparePassword( password, credential.password, ); if (!isPasswordValid) { throw new UnauthorizedException('Invalid credentials'); } // Generate tokens const { accessToken, refreshToken } = await this.getNewToken(user); // Get client info const clientInfo = this.commonService.getClientInfo(request); // Create session await this.userSessionCoreService.create({ data: { userId: user.id, accessToken, refreshToken, ipAddress: clientInfo.ipAddress, userAgent: clientInfo.userAgent, geoIpCountry: clientInfo.geoLocation?.country || '', city: clientInfo.geoLocation?.city || '', state: clientInfo.geoLocation?.region || '', latitude: clientInfo.geoLocation?.ll?.[0] || null, longitude: clientInfo.geoLocation?.ll?.[1] || null, status: SESSION_STATUS.CURRENT, loginAt: new Date(), }, }); // Update last login await this.userCoreService.update({ where: { id: user.id }, data: { lastLogin: new Date() }, }); return { user, accessToken, refreshToken, }; } async logout(sessionData: UserSessionType): Promise<{ message: string }> { const { session } = sessionData; // Update session to mark as logged out await this.userSessionCoreService.update({ where: { id: session.id }, data: { status: SESSION_STATUS.EXPIRED, logoutAt: new Date(), expiredAt: new Date(), }, }); return { message: AuthMessages.LOGOUT_SUCCESS }; } async refreshToken(refreshTokenDto: UserRefreshTokenDto): Promise<{ accessToken: string; refreshToken: string; }> { const { refreshToken } = refreshTokenDto; let validateRefreshToken: any = null; try { validateRefreshToken = await this.jwtService.verifyAsync( refreshToken, refreshTokenVerifySettings, ); } catch { throw new UnauthorizedException('Invalid refresh token'); } // Verify user type if (validateRefreshToken.userType !== TOKEN_USER_TYPE.USER) { throw new UnauthorizedException('Invalid token type'); } const user = await this.userCoreService.findUnique({ where: { id: validateRefreshToken.id, isDeleted: false }, }); if (!user) { throw new UnauthorizedException('User not found'); } // Generate new tokens const newTokens = await this.getNewToken(user); // Update session with new tokens await this.userSessionCoreService.updateMany({ where: { userId: user.id, refreshToken, status: SESSION_STATUS.CURRENT, }, data: { accessToken: newTokens.accessToken, refreshToken: newTokens.refreshToken, }, }); return newTokens; } private async getNewToken(user: User): Promise<{ accessToken: string; refreshToken: string; }> { const payload = { id: user.id, email: user.email, userType: TOKEN_USER_TYPE.USER, }; const accessToken = await this.jwtService.signAsync( { ...payload, type: TOKEN_TYPE.ACCESS }, accessTokenSignSettings, ); const refreshToken = await this.jwtService.signAsync( { ...payload, type: TOKEN_TYPE.REFRESH }, refreshTokenSignSettings, ); return { accessToken, refreshToken }; } async performJWTStrategy({ request, user, }: { request: Request & { headers: { authorization: string } }; user: User; }): Promise { const accessToken = request.headers.authorization?.replace('Bearer ', ''); if (!accessToken) { throw new UnauthorizedException('Invalid token'); } // Get full user object const fullUser = await this.userCoreService.findUnique({ where: { id: user.id, isDeleted: false }, }); if (!fullUser) { throw new UnauthorizedException('User not found'); } // Find active session const session = await this.userSessionCoreService.findFirst({ where: { userId: user.id, accessToken, status: SESSION_STATUS.CURRENT, isDeleted: false, }, }); if (!session) { throw new UnauthorizedException('Session not found'); } return { user: fullUser, session, }; } // Helper: Generate and send OTP private async generateAndSendOtp( userId: string, destination: string, type: OTP_TYPE, purpose: OTP_PURPOSE, firstName?: string, ): Promise { // Generate 6-digit OTP const otp = Math.floor(100000 + Math.random() * 900000).toString(); // OTP expires in 10 minutes const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // Invalidate previous OTPs await this.userOtpCoreService.updateMany({ where: { userId, type, purpose, verified: false, isDeleted: false, }, data: { isDeleted: true }, }); // Create new OTP await this.userOtpCoreService.create({ data: { userId, otp, type, purpose, expiresAt, }, }); // Send OTP if (type === OTP_TYPE.EMAIL) { await this.emailService.sendOtpEmail(destination, otp, firstName); } else if (type === OTP_TYPE.SMS) { await this.smsService.sendOtpSms(destination, otp); } } // Helper: Verify OTP private async verifyOtp( userId: string, otp: string, type: OTP_TYPE, purpose: OTP_PURPOSE, ): Promise { const otpRecord = await this.userOtpCoreService .findFirst({ where: { userId, type, purpose, verified: false, isDeleted: false, }, orderBy: { createdAt: 'desc' }, }) .catch(() => null); if (!otpRecord) { throw new BadRequestException('OTP not found or already used'); } // Check if OTP is expired if (new Date() > otpRecord.expiresAt) { throw new BadRequestException('OTP has expired'); } // Check attempts if (otpRecord.attempts >= 5) { throw new BadRequestException( 'Too many attempts. Please request a new OTP', ); } // Verify OTP if (otpRecord.otp !== otp) { // Increment attempts await this.userOtpCoreService.update({ where: { id: otpRecord.id }, data: { attempts: otpRecord.attempts + 1 }, }); throw new BadRequestException('Invalid OTP'); } // Mark OTP as verified await this.userOtpCoreService.update({ where: { id: otpRecord.id }, data: { verified: true, verifiedAt: new Date(), }, }); } }