add oprator apis

This commit is contained in:
paritosh18
2026-04-23 18:41:37 +05:30
parent 22e2e8e1b7
commit 5fcff67916
11 changed files with 763 additions and 3 deletions

View File

@@ -0,0 +1,58 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../common/utils/helper/ApiError';
import { verifyOperatorToken } from '../../../../common/middlewares/jwt/authForOperator';
import { OperatorAuthService } from '../../services/operatorAuth.service';
const operatorAuthService = new OperatorAuthService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context,
): Promise<APIGatewayProxyResult> => {
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(400, 'This is a protected route. Please provide a valid token.');
}
const userInfo = await verifyOperatorToken(token);
let body: { password?: string; confirmPassword?: string };
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const { password, confirmPassword } = body;
if (!password || !confirmPassword) {
throw new ApiError(400, 'Password and confirm password are required');
}
if (password !== confirmPassword) {
throw new ApiError(400, 'Password and confirm password do not match');
}
if (password.length < 8) {
throw new ApiError(400, 'Password must be at least 8 characters long');
}
await operatorAuthService.createOperatorPassword(userInfo.id, password);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Password created successfully',
data: null,
}),
};
});

View File

@@ -0,0 +1,55 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../common/utils/helper/ApiError';
import { GetHostLoginResponseDTO } from '../../dto/host.dto';
import { OperatorAuthService } from '../../services/operatorAuth.service';
import { TokenService } from '../../services/token.service';
const operatorAuthService = new OperatorAuthService(prismaClient);
const tokenService = new TokenService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context,
): Promise<APIGatewayProxyResult> => {
let body: { emailAddress?: string; userPassword?: string };
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const { emailAddress, userPassword } = body;
if (!emailAddress || !userPassword) {
throw new ApiError(400, 'Email and password are required');
}
const operator = await operatorAuthService.loginForOperator(
emailAddress.trim().toLowerCase(),
userPassword,
);
const generatedToken = await tokenService.generateAuthToken(operator.id);
const response = new GetHostLoginResponseDTO(
operator,
generatedToken.access.token,
generatedToken.refresh.token,
);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Login successful',
data: response,
}),
};
});

View File

@@ -0,0 +1,53 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../common/utils/helper/ApiError';
import { OperatorAuthService } from '../../services/operatorAuth.service';
const operatorAuthService = new OperatorAuthService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context,
): Promise<APIGatewayProxyResult> => {
let body: {
firstName?: string;
lastName?: string;
emailAddress?: string;
isdCode?: string;
mobileNumber?: string;
};
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const { firstName, lastName, emailAddress, isdCode, mobileNumber } = body;
if (!emailAddress && !mobileNumber) {
throw new ApiError(400, 'Email address or mobile number is required');
}
const signupResponse = await operatorAuthService.signUpOperator({
firstName,
lastName,
emailAddress,
isdCode,
mobileNumber,
});
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'OTP sent successfully',
data: signupResponse,
}),
};
});

View File

@@ -0,0 +1,51 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../common/utils/helper/ApiError';
import { OperatorAuthService } from '../../services/operatorAuth.service';
import { TokenService } from '../../services/token.service';
const operatorAuthService = new OperatorAuthService(prismaClient);
const tokenService = new TokenService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context,
): Promise<APIGatewayProxyResult> => {
let body: { emailAddress?: string; mobileNumber?: string; otp?: string };
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const { emailAddress, mobileNumber, otp } = body;
if ((!emailAddress && !mobileNumber) || !otp) {
throw new ApiError(400, 'Email address or mobile number and OTP are required');
}
const operator = await operatorAuthService.verifyOperatorOtp({
emailAddress,
mobileNumber,
otp,
});
const generatedToken = await tokenService.generateAuthToken(operator.id);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'OTP verified successfully',
accessToken: generatedToken.access.token,
refreshToken: generatedToken.refresh.token,
data: null,
}),
};
});

View File

@@ -0,0 +1,257 @@
import { Injectable } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import * as bcrypt from 'bcryptjs';
import ApiError from '../../../common/utils/helper/ApiError';
import { OtpGeneratorSixDigit } from '../../../common/utils/helper/OtpGenerator';
import { ROLE, USER_STATUS } from '../../../common/utils/constants/common.constant';
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) {
throw new ApiError(409, 'Operator account is already created. Please login.');
}
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 {
userId: invitedOperator.id,
emailAddress: emailAddress || invitedOperator.emailAddress,
mobileNumber: mobileNumber || invitedOperator.mobileNumber,
otp,
expiresOn: expiry,
};
}
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;
}
}

View File

@@ -0,0 +1,91 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { safeHandler } from '../../../common/utils/handlers/safeHandler';
import { PrismaService } from '../../../common/database/prisma.service';
import { MinglarService } from '../services/minglar.service';
import ApiError from '../../../common/utils/helper/ApiError';
import { verifyMinglarAdminToken } from '../../../common/middlewares/jwt/authForMinglarAdmin';
import { HOST_SUGGESTION_TITLES } from '../../../common/utils/constants/minglar.constant';
const prismaService = new PrismaService();
const minglarService = new MinglarService(prismaService);
interface AddSuggestionBody {
hostXid: number;
title: string;
comments: string;
activity_pqq_header_xid:number
}
/**
* Add suggestion handler for host applications
* Allows Minglar Admin, Co_Admin, and Account Manager to add suggestions
* Types: Setup Profile, Review Account, Add Payment Details, Agreement
*/
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
// Verify authentication token
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(401, 'This is a protected route. Please provide a valid token.');
}
// Verify token and get user info
const userInfo = await verifyMinglarAdminToken(token);
// Get user details
const user = await prismaService.user.findUnique({
where: { id: userInfo.id },
select: { id: true, roleXid: true }
});
if (!user) {
throw new ApiError(404, 'User not found');
}
// Parse request body
let body: AddSuggestionBody;
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const { title, comments , activity_pqq_header_xid} = body;
if (!title) {
throw new ApiError(400, 'Title is required');
}
if (!comments) {
throw new ApiError(400, 'Comments are required');
}
if(!activity_pqq_header_xid){
throw new ApiError(400 , "Activity Pqq HeaderXid Required");
}
// Validate title is one of the allowed types
const allowedTitles = Object.values(HOST_SUGGESTION_TITLES);
if (!allowedTitles.includes(title)) {
throw new ApiError(400, `Invalid title. Allowed values: ${allowedTitles.join(', ')}`);
}
// Add suggestion using service
await minglarService.addPqqSuggestion(title, comments, activity_pqq_header_xid);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Suggestion added successfully',
data: null,
}),
};
});