import { RoleType } from '@app/common/constants/role.type.enum'; import { differenceInSeconds } from '@app/common/helper/differenceInSeconds'; import { BadRequestException, ForbiddenException, Injectable, } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import * as argon2 from 'argon2'; import { RoleService } from 'src/role/services'; import { LessThan, MoreThan } from 'typeorm'; import { AuthService } from '../../../libs/common/src/auth/services/auth.service'; import { OtpType } from '../../../libs/common/src/constants/otp-type.enum'; import { HelperHashService } from '../../../libs/common/src/helper/services'; import { UserSessionRepository } from '../../../libs/common/src/modules/session/repositories/session.repository'; import { UserEntity } from '../../../libs/common/src/modules/user/entities/user.entity'; import { UserRepository } from '../../../libs/common/src/modules/user/repositories'; import { UserOtpRepository } from '../../../libs/common/src/modules/user/repositories/user.repository'; import { EmailService } from '../../../libs/common/src/util/email/email.service'; import { ForgetPasswordDto, UserOtpDto, VerifyOtpDto } from '../dtos'; import { UserSignUpDto } from '../dtos/user-auth.dto'; import { UserLoginDto } from '../dtos/user-login.dto'; @Injectable() export class UserAuthService { constructor( private readonly userRepository: UserRepository, private readonly sessionRepository: UserSessionRepository, private readonly otpRepository: UserOtpRepository, private readonly helperHashService: HelperHashService, private readonly authService: AuthService, private readonly emailService: EmailService, private readonly roleService: RoleService, private readonly configService: ConfigService, ) {} async signUp( userSignUpDto: UserSignUpDto, clientUuid?: string, ): Promise { const findUser = await this.findUser(userSignUpDto.email); if (findUser) { if (!findUser.isActive) { throw new BadRequestException('User is not active'); } throw new BadRequestException('User already registered with given email'); } const salt = this.helperHashService.randomSalt(10); // Hash the password using bcrypt const hashedPassword = await this.helperHashService.bcrypt( userSignUpDto.password, salt, ); try { const { regionUuid, hasAcceptedAppAgreement, ...rest } = userSignUpDto; if (!hasAcceptedAppAgreement) { throw new BadRequestException('Please accept the terms and conditions'); } const spaceMemberRole = await this.roleService.findRoleByType( RoleType.SPACE_MEMBER, ); const user = await this.userRepository.save({ ...rest, appAgreementAcceptedAt: new Date(), hasAcceptedAppAgreement, password: hashedPassword, roleType: { uuid: spaceMemberRole.uuid }, client: { uuid: clientUuid }, region: regionUuid ? { uuid: regionUuid, } : { regionName: 'United Arab Emirates', }, }); return user; } catch (error) { throw new BadRequestException(error.message || 'Failed to register user'); } } async findUser(email: string) { return await this.userRepository.findOne({ where: { email, }, }); } async forgetPassword(forgetPasswordDto: ForgetPasswordDto) { const findUser = await this.findUser(forgetPasswordDto.email); if (!findUser) { throw new BadRequestException('User not found'); } const salt = this.helperHashService.randomSalt(10); const password = this.helperHashService.bcrypt( forgetPasswordDto.password, salt, ); return await this.userRepository.update( { uuid: findUser.uuid }, { password }, ); } async userLogin(data: UserLoginDto) { try { let user: Omit; if (data.googleCode) { const googleUserData = await this.authService.login({ googleCode: data.googleCode, }); const userExists = await this.userRepository.exists({ where: { email: googleUserData['email'], }, }); user = await this.userRepository.findOne({ where: { email: googleUserData['email'], }, }); if (!userExists) { await this.signUp({ email: googleUserData['email'], firstName: googleUserData['given_name'], lastName: googleUserData['family_name'], password: googleUserData['email'], hasAcceptedAppAgreement: true, }); } data.email = googleUserData['email']; data.password = googleUserData['password']; } if (!data.googleCode) { user = await this.authService.validateUser( data.email, data.password, data.regionUuid, data.platform, ); } const session = await Promise.all([ await this.sessionRepository.update( { userId: user?.['id'] }, { isLoggedOut: true, }, ), await this.authService.createSession({ userId: user.uuid, loginTime: new Date(), isLoggedOut: false, }), ]); const res = await this.authService.login({ email: user.email, userId: user.uuid, uuid: user.uuid, role: user.roleType, hasAcceptedWebAgreement: user.hasAcceptedWebAgreement, hasAcceptedAppAgreement: user.hasAcceptedAppAgreement, project: user.project, sessionId: session[1].uuid, bookingPoints: user.bookingPoints, }); return res; } catch (error) { throw new BadRequestException(error.message || 'Invalid credentials'); } } async findOneById(id: string): Promise { return await this.userRepository.findOne({ where: { uuid: id } }); } async generateOTP(data: UserOtpDto): Promise<{ otpCode: string; cooldown: number; }> { try { const otpLimiter = new Date(); otpLimiter.setDate( otpLimiter.getDate() - this.configService.get('OTP_LIMITER'), ); const user = await this.userRepository.findOne({ where: { region: data.regionUuid ? { uuid: data.regionUuid } : undefined, email: data.email, isUserVerified: data.type === OtpType.PASSWORD ? true : undefined, }, }); if (!user) { throw new BadRequestException('User not found'); } await this.otpRepository.softDelete({ email: data.email, type: data.type, }); await this.otpRepository.delete({ email: data.email, type: data.type, createdAt: LessThan(otpLimiter), }); const countOfOtp = await this.otpRepository.count({ withDeleted: true, where: { email: data.email, type: data.type, createdAt: MoreThan(otpLimiter), }, }); const lastOtp = await this.otpRepository.findOne({ where: { email: data.email, type: data.type }, order: { createdAt: 'DESC' }, withDeleted: true, }); let cooldown = 30 * Math.pow(2, countOfOtp - 1); if (lastOtp) { const now = new Date(); const timeSinceLastOtp = differenceInSeconds(now, lastOtp.createdAt); if (timeSinceLastOtp < cooldown) { throw new BadRequestException({ message: `Please wait ${cooldown - timeSinceLastOtp} more seconds before requesting a new OTP.`, data: { cooldown: cooldown - timeSinceLastOtp, }, }); } } const otpCode = Math.floor(100000 + Math.random() * 900000).toString(); const expiryTime = new Date(); expiryTime.setMinutes(expiryTime.getMinutes() + 10); await this.otpRepository.save({ email: data.email, otpCode, expiryTime, type: data.type, }); const countOfOtpToReturn = await this.otpRepository.count({ withDeleted: true, where: { email: data.email, type: data.type, createdAt: MoreThan(otpLimiter), }, }); cooldown = 30 * Math.pow(2, countOfOtpToReturn - 1); const [otp1, otp2, otp3, otp4, otp5, otp6] = otpCode.split(''); await this.emailService.sendOtpEmailWithTemplate(data.email, { name: user.firstName, otp1, otp2, otp3, otp4, otp5, otp6, }); return { otpCode, cooldown }; } catch (error) { if (error instanceof BadRequestException) { throw error; } console.error('OTP generation error:', error); throw new BadRequestException( 'An unexpected error occurred while generating the OTP.', ); } } async verifyOTP( data: VerifyOtpDto, fromNewPassword: boolean = false, ): Promise { const otp = await this.otpRepository.findOne({ where: { email: data.email, type: data.type, otpCode: data.otpCode }, }); if (!otp) { const user = await this.userRepository.findOne({ where: { email: data.email, }, }); if (!user) { throw new BadRequestException('this email is not registered'); } throw new BadRequestException('You entered wrong otp'); } if (otp.otpCode !== data.otpCode) { throw new BadRequestException('You entered wrong otp'); } if (otp.expiryTime < new Date()) { await this.otpRepository.delete(otp.uuid); throw new BadRequestException('OTP expired'); } if (fromNewPassword) { await this.otpRepository.delete(otp.uuid); } if (data.type == OtpType.VERIFICATION) { await this.userRepository.update( { email: data.email }, { isUserVerified: true }, ); } return true; } async userList(): Promise { return await this.userRepository.find({ where: { isActive: true }, select: { firstName: true, lastName: true, email: true, isActive: true, }, }); } async refreshToken( userId: string, refreshToken: string, type: string, sessionId: string, ) { const user = await this.userRepository.findOne({ where: { uuid: userId } }); if (!user || !user.refreshToken) throw new ForbiddenException('Access Denied'); const refreshTokenMatches = await argon2.verify( user.refreshToken, refreshToken, ); if (!refreshTokenMatches) throw new ForbiddenException('Access Denied'); const tokens = await this.authService.getTokens({ email: user.email, userId: user.uuid, uuid: user.uuid, type, bookingPoints: user.bookingPoints, sessionId, }); await this.authService.updateRefreshToken(user.uuid, tokens.refreshToken); return tokens; } }