Spaces:
Runtime error
Runtime error
| 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'; | |
| () | |
| 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<UserSessionType> { | |
| 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<void> { | |
| // 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<void> { | |
| 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(), | |
| }, | |
| }); | |
| } | |
| } | |