From 5fcff679169cdf182b5b50a9f9e8873653e79ced Mon Sep 17 00:00:00 2001 From: paritosh18 Date: Thu, 23 Apr 2026 18:41:37 +0530 Subject: [PATCH] add oprator apis --- serverless.operator.yml | 12 + serverless.yml | 1 + serverless/functions/host.yml | 3 - serverless/functions/operator.yml | 73 +++++ src/common/middlewares/jwt/authForOperator.ts | 112 ++++++++ .../host/handlers/operator/createPassword.ts | 58 ++++ src/modules/host/handlers/operator/login.ts | 55 ++++ src/modules/host/handlers/operator/signUp.ts | 53 ++++ .../host/handlers/operator/verifyOtp.ts | 51 ++++ .../host/services/operatorAuth.service.ts | 257 ++++++++++++++++++ .../minglaradmin/handlers/addPQQSuggestion.ts | 91 +++++++ 11 files changed, 763 insertions(+), 3 deletions(-) create mode 100644 serverless.operator.yml create mode 100644 serverless/functions/operator.yml create mode 100644 src/common/middlewares/jwt/authForOperator.ts create mode 100644 src/modules/host/handlers/operator/createPassword.ts create mode 100644 src/modules/host/handlers/operator/login.ts create mode 100644 src/modules/host/handlers/operator/signUp.ts create mode 100644 src/modules/host/handlers/operator/verifyOtp.ts create mode 100644 src/modules/host/services/operatorAuth.service.ts create mode 100644 src/modules/minglaradmin/handlers/addPQQSuggestion.ts diff --git a/serverless.operator.yml b/serverless.operator.yml new file mode 100644 index 0000000..9ff8e33 --- /dev/null +++ b/serverless.operator.yml @@ -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)} diff --git a/serverless.yml b/serverless.yml index afeec56..c0e4251 100644 --- a/serverless.yml +++ b/serverless.yml @@ -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)} diff --git a/serverless/functions/host.yml b/serverless/functions/host.yml index ba81f1d..528d2e9 100644 --- a/serverless/functions/host.yml +++ b/serverless/functions/host.yml @@ -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 diff --git a/serverless/functions/operator.yml b/serverless/functions/operator.yml new file mode 100644 index 0000000..3355005 --- /dev/null +++ b/serverless/functions/operator.yml @@ -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 diff --git a/src/common/middlewares/jwt/authForOperator.ts b/src/common/middlewares/jwt/authForOperator.ts new file mode 100644 index 0000000..b051628 --- /dev/null +++ b/src/common/middlewares/jwt/authForOperator.ts @@ -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; diff --git a/src/modules/host/handlers/operator/createPassword.ts b/src/modules/host/handlers/operator/createPassword.ts new file mode 100644 index 0000000..4aab4e9 --- /dev/null +++ b/src/modules/host/handlers/operator/createPassword.ts @@ -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 => { + 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, + }), + }; +}); diff --git a/src/modules/host/handlers/operator/login.ts b/src/modules/host/handlers/operator/login.ts new file mode 100644 index 0000000..16cc6bb --- /dev/null +++ b/src/modules/host/handlers/operator/login.ts @@ -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 => { + 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, + }), + }; +}); diff --git a/src/modules/host/handlers/operator/signUp.ts b/src/modules/host/handlers/operator/signUp.ts new file mode 100644 index 0000000..acf094b --- /dev/null +++ b/src/modules/host/handlers/operator/signUp.ts @@ -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 => { + 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, + }), + }; +}); diff --git a/src/modules/host/handlers/operator/verifyOtp.ts b/src/modules/host/handlers/operator/verifyOtp.ts new file mode 100644 index 0000000..6d2d9d3 --- /dev/null +++ b/src/modules/host/handlers/operator/verifyOtp.ts @@ -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 => { + 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, + }), + }; +}); diff --git a/src/modules/host/services/operatorAuth.service.ts b/src/modules/host/services/operatorAuth.service.ts new file mode 100644 index 0000000..7e80003 --- /dev/null +++ b/src/modules/host/services/operatorAuth.service.ts @@ -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; + } +} diff --git a/src/modules/minglaradmin/handlers/addPQQSuggestion.ts b/src/modules/minglaradmin/handlers/addPQQSuggestion.ts new file mode 100644 index 0000000..8d3971e --- /dev/null +++ b/src/modules/minglaradmin/handlers/addPQQSuggestion.ts @@ -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 => { + // 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, + }), + }; +});