import { Injectable } from '@nestjs/common'; import { PrismaClient } from '@prisma/client'; import * as bcrypt from 'bcryptjs'; import { ROLE, USER_STATUS } from '../../../common/utils/constants/common.constant'; import ApiError from '../../../common/utils/helper/ApiError'; import { OtpGeneratorSixDigit } from '../../../common/utils/helper/OtpGenerator'; type OperatorSignupInput = { firstName?: string; lastName?: string; emailAddress?: string; isdCode?: string; mobileNumber?: string; }; @Injectable() export class OperatorAuthService { constructor(private prisma: PrismaClient) {} async findInvitedOperator(input: { emailAddress?: string; mobileNumber?: string; }) { const emailAddress = input.emailAddress?.trim().toLowerCase(); const mobileNumber = input.mobileNumber?.trim(); if (!emailAddress && !mobileNumber) { throw new ApiError(400, 'Email address or mobile number is required'); } const invitedOperator = await this.prisma.user.findFirst({ where: { roleXid: ROLE.OPERATOR, isActive: true, OR: [ ...(emailAddress ? [{ emailAddress }] : []), ...(mobileNumber ? [{ mobileNumber }] : []), ], }, select: { id: true, firstName: true, lastName: true, emailAddress: true, isdCode: true, mobileNumber: true, roleXid: true, userPassword: true, isEmailVerfied: true, userStatus: true, }, }); if (!invitedOperator) { throw new ApiError( 403, 'Operator record not found. Please contact your host.', ); } return invitedOperator; } async signUpOperator(input: OperatorSignupInput) { const emailAddress = input.emailAddress?.trim().toLowerCase(); const mobileNumber = input.mobileNumber?.trim(); const invitedOperator = await this.findInvitedOperator({ emailAddress, mobileNumber, }); if (invitedOperator.userPassword) { return { userId: invitedOperator.id, emailAddress: emailAddress || invitedOperator.emailAddress, mobileNumber: mobileNumber || invitedOperator.mobileNumber, otp: null, expiresOn: null, isNewOperator: false, }; } const otp = OtpGeneratorSixDigit.generateOtp(); const hashedOtp = await bcrypt.hash(otp, 10); const expiry = new Date(Date.now() + 5 * 60 * 1000); await this.prisma.$transaction(async (tx) => { await tx.user.update({ where: { id: invitedOperator.id }, data: { firstName: input.firstName?.trim() || invitedOperator.firstName || null, lastName: input.lastName?.trim() || invitedOperator.lastName || null, emailAddress: emailAddress || invitedOperator.emailAddress, isdCode: input.isdCode?.trim() || invitedOperator.isdCode || null, mobileNumber: mobileNumber || invitedOperator.mobileNumber, userStatus: USER_STATUS.INVITED, }, }); await tx.userOtp.updateMany({ where: { userXid: invitedOperator.id, otpType: 'Register', isActive: true, }, data: { isActive: false, isVerified: true, }, }); await tx.userOtp.create({ data: { userXid: invitedOperator.id, otpType: 'Register', otpCode: hashedOtp, expiresOn: expiry, isVerified: false, isActive: true, }, }); }); return { isNewOperator: true, }; } async verifyOperatorOtp(input: { emailAddress?: string; mobileNumber?: string; otp: string }) { const emailAddress = input.emailAddress?.trim().toLowerCase(); const mobileNumber = input.mobileNumber?.trim(); const otp = input.otp.trim(); const invitedOperator = await this.findInvitedOperator({ emailAddress, mobileNumber, }); const latestOtp = await this.prisma.userOtp.findFirst({ where: { userXid: invitedOperator.id, otpType: 'Register', isActive: true, isVerified: false, }, orderBy: { createdAt: 'desc' }, }); if (!latestOtp) { throw new ApiError(400, 'No OTP found.'); } if (new Date() > latestOtp.expiresOn) { throw new ApiError(400, 'OTP has expired.'); } const isMatch = await bcrypt.compare(otp, latestOtp.otpCode); if (!isMatch) { throw new ApiError(400, 'Invalid OTP.'); } await this.prisma.userOtp.update({ where: { id: latestOtp.id }, data: { isVerified: true, verifiedOn: new Date(), isActive: false, }, }); await this.prisma.user.update({ where: { id: invitedOperator.id }, data: { isEmailVerfied: emailAddress ? true : invitedOperator.isEmailVerfied, }, }); return invitedOperator; } async createOperatorPassword(userId: number, password: string) { const user = await this.prisma.user.findFirst({ where: { id: userId, roleXid: ROLE.OPERATOR, isActive: true, }, select: { id: true, emailAddress: true, userPassword: true, }, }); if (!user) { throw new ApiError(404, 'Operator not found'); } if (user.userPassword) { throw new ApiError(400, 'Password already exists. Please login.'); } const saltRounds = parseInt(process.env.SALT_ROUNDS || '10', 10); const hashedPassword = await bcrypt.hash(password, saltRounds); await this.prisma.user.update({ where: { id: user.id }, data: { userPassword: hashedPassword, userStatus: USER_STATUS.ACTIVE, isEmailVerfied: true, }, }); return { id: user.id, emailAddress: user.emailAddress, }; } async loginForOperator(emailAddress: string, userPassword: string) { const existingOperator = await this.prisma.user.findFirst({ where: { emailAddress, roleXid: ROLE.OPERATOR, isActive: true, userStatus: USER_STATUS.ACTIVE, }, select: { id: true, firstName: true, lastName: true, emailAddress: true, mobileNumber: true, roleXid: true, isActive: true, userStatus: true, userPassword: true, }, }); if (!existingOperator) { throw new ApiError(404, 'Operator not found'); } const isPasswordMatched = await bcrypt.compare( userPassword, existingOperator.userPassword || '', ); if (!isPasswordMatched) { throw new ApiError(401, 'Invalid credentials'); } delete existingOperator.userPassword; return existingOperator; } async verifyPasswordForOperator(emailAddress: string, userPassword: string) { const existingOperator = await this.prisma.user.findFirst({ where: { emailAddress, roleXid: ROLE.OPERATOR, isActive: true, userStatus: USER_STATUS.ACTIVE, }, select: { id: true, userPassword: true, }, }); if (!existingOperator) { throw new ApiError(404, 'Operator not found'); } const isPasswordMatched = await bcrypt.compare( userPassword, existingOperator.userPassword || '', ); return isPasswordMatched; } }