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

12
serverless.operator.yml Normal file
View File

@@ -0,0 +1,12 @@
service: minglar-operator
useDotenv: ${file(./serverless/common.yml):useDotenv}
params: ${file(./serverless/common.yml):params}
provider: ${file(./serverless/common.yml):provider}
build: ${file(./serverless/common.yml):build}
package: ${file(./serverless/common.yml):package}
plugins: ${file(./serverless/common.yml):plugins}
custom: ${file(./serverless/common.yml):custom}
functions:
- ${file(./serverless/functions/operator.yml)}

View File

@@ -155,6 +155,7 @@ package:
# Import function definitions from separate files organized by module
functions:
- ${file(./serverless/functions/host.yml)}
- ${file(./serverless/functions/operator.yml)}
- ${file(./serverless/functions/minglaradmin.yml)}
- ${file(./serverless/functions/prepopulate.yml)}
- ${file(./serverless/functions/user.yml)}

View File

@@ -431,7 +431,6 @@ resendOTPmail:
path: /resend-otp
method: post
mediaUploadTos3:
handler: src/modules/host/handlers/mediaUploadToS3.handler
memorySize: 512
@@ -447,7 +446,6 @@ mediaUploadTos3:
path: /media/upload/activity/{activityXid}
method: post
venueMediaUploadTos3:
handler: src/modules/host/handlers/mediaUploadForVenueToS3.handler
memorySize: 512
@@ -463,7 +461,6 @@ venueMediaUploadTos3:
path: /media/upload/venue/activity/{activityXid}
method: post
mediaDeleteFroms3:
handler: src/modules/host/handlers/mediaDeleteFromS3.handler
memorySize: 512

View File

@@ -0,0 +1,73 @@
# Operator Module Functions
operatorSignUp:
handler: src/modules/host/handlers/operator/signUp.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/operator/**'
- 'src/modules/host/services/operatorAuth.service.ts'
- 'src/modules/host/services/token.service.ts'
- 'src/common/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /operator/signup
method: post
operatorVerifyOtp:
handler: src/modules/host/handlers/operator/verifyOtp.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/operator/**'
- 'src/modules/host/services/operatorAuth.service.ts'
- 'src/modules/host/services/token.service.ts'
- 'src/common/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /operator/verify-otp
method: post
operatorCreatePassword:
handler: src/modules/host/handlers/operator/createPassword.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/operator/**'
- 'src/modules/host/services/operatorAuth.service.ts'
- 'src/modules/host/services/token.service.ts'
- 'src/common/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /operator/create-password
method: post
operatorLogin:
handler: src/modules/host/handlers/operator/login.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/operator/**'
- 'src/modules/host/services/operatorAuth.service.ts'
- 'src/modules/host/services/token.service.ts'
- 'src/common/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /operator/login
method: post

View File

@@ -0,0 +1,112 @@
import jwt from 'jsonwebtoken';
import httpStatus from 'http-status';
import { NextFunction, Request, Response } from 'express';
import { prisma } from '../../database/prisma.client';
import ApiError from '../../utils/helper/ApiError';
import config from '../../../config/config';
import { ROLE } from '../../utils/constants/common.constant';
interface DecodedToken {
id?: number;
sub?: string | number;
role?: string;
iat: number;
exp: number;
}
interface UserPayload {
id: string;
role?: string;
}
declare module 'express-serve-static-core' {
interface Request {
user?: UserPayload;
}
}
export async function verifyOperatorToken(
token: string,
): Promise<{ id: number; role?: string }> {
if (!token) {
throw new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate');
}
try {
const decoded = jwt.verify(token, config.jwt.secret) as unknown as DecodedToken;
const userId = decoded.id ?? (decoded.sub ? Number(decoded.sub) : null);
if (!userId) {
throw new ApiError(httpStatus.UNAUTHORIZED, 'Invalid token payload');
}
const user = await prisma.user.findUnique({
where: { id: userId },
include: { role: true },
});
const latestToken = await prisma.token.findFirst({
where: { userXid: userId },
orderBy: { id: 'desc' },
});
if (latestToken?.isBlackListed === true) {
throw new ApiError(401, 'This session is expired. Please login.');
}
if (!user) {
throw new ApiError(httpStatus.UNAUTHORIZED, 'User not found');
}
if (user.isActive === false) {
throw new ApiError(httpStatus.FORBIDDEN, 'Your account is deactivated by admin.');
}
if (user.roleXid !== ROLE.OPERATOR) {
throw new ApiError(httpStatus.FORBIDDEN, 'Access denied.');
}
return { id: user.id, role: user.role?.roleName };
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
throw new ApiError(
httpStatus.UNAUTHORIZED,
'Your session has expired. Please log in again.',
);
}
if (error instanceof ApiError) {
throw error;
}
throw new ApiError(httpStatus.FORBIDDEN, 'Invalid or expired authentication token.');
}
}
const verifyCallback = async (
req: Request,
resolve: (value?: unknown) => void,
reject: (reason?: Error) => void,
) => {
const token = req.header('x-auth-token') || req.cookies?.accessToken;
try {
const userInfo = await verifyOperatorToken(token);
req.user = { id: userInfo.id.toString(), role: userInfo.role };
resolve();
} catch (error) {
return reject(error as Error);
}
};
const authForOperator =
() =>
async (req: Request, res: Response, next: NextFunction) => {
return new Promise((resolve, reject) => {
verifyCallback(req, resolve, reject);
})
.then(() => next())
.catch((err) => next(err));
};
export default authForOperator;

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