Files
MinglarBackendNestJS/src/modules/host/services/operatorAuth.service.ts
2026-04-24 15:49:24 +05:30

287 lines
7.2 KiB
TypeScript

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;
}
}