From a14f1388f67162bd4bda0fb00e52dc6d043fd295 Mon Sep 17 00:00:00 2001 From: paritosh18 Date: Thu, 13 Nov 2025 14:59:50 +0530 Subject: [PATCH 1/3] add host and minglar admin APIs for registration, login, and retrieval of host details --- serverless.yml | 61 ++ src/modules/host/handlers/getbyidhandler.ts | 50 ++ src/modules/host/handlers/registration.ts | 2 +- src/modules/host/services/host.service.ts | 67 +- src/modules/minglaradmin/dto/minglar.dto.ts | 93 +++ .../minglaradmin/handlers/loginForHost.ts | 70 -- .../minglaradmin/handlers/loginForMinglar.ts | 75 +++ .../minglaradmin/handlers/registration.ts | 75 +++ .../minglaradmin/services/minglar.service.ts | 130 ++++ .../minglaradmin/services/token.service.ts | 159 +++++ swagger.json | 624 +----------------- 11 files changed, 718 insertions(+), 688 deletions(-) create mode 100644 src/modules/host/handlers/getbyidhandler.ts create mode 100644 src/modules/minglaradmin/dto/minglar.dto.ts delete mode 100644 src/modules/minglaradmin/handlers/loginForHost.ts create mode 100644 src/modules/minglaradmin/handlers/loginForMinglar.ts create mode 100644 src/modules/minglaradmin/handlers/registration.ts create mode 100644 src/modules/minglaradmin/services/token.service.ts diff --git a/serverless.yml b/serverless.yml index 9ba7a5b..a18a2ca 100644 --- a/serverless.yml +++ b/serverless.yml @@ -166,6 +166,67 @@ functions: path: /host/add-payment-details method: post + getHostById: + handler: src/modules/host/handlers/getbyidhandler.handler + package: + patterns: + - 'src/modules/host/**' + - 'common/**' + - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' + - 'node_modules/@prisma/client/**' + - 'prisma/schema.prisma' + events: + - httpApi: + path: /host/{id} + method: get + + # ๐Ÿ‘‡ Minglar Admin Module + + minglarRegistration: + handler: src/modules/minglaradmin/handlers/registration.handler + package: + patterns: + - 'src/modules/minglaradmin/**' + - 'common/**' + - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' + - 'node_modules/@prisma/client/**' + - 'node_modules/@aws-sdk/**' + - 'prisma/schema.prisma' + events: + - httpApi: + path: /minglaradmin/registration + method: post + + minglarLoginForAdmin: + handler: src/modules/minglaradmin/handlers/loginForMinglar.handler + package: + patterns: + - 'src/modules/minglaradmin/**' + - 'common/**' + - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' + - 'node_modules/@prisma/client/**' + - 'node_modules/@aws-sdk/**' + - 'prisma/schema.prisma' + events: + - httpApi: + path: /minglaradmin/login + method: post + + minglarCreatePassword: + handler: src/modules/minglaradmin/handlers/createPassword.handler + package: + patterns: + - 'src/modules/minglaradmin/**' + - 'common/**' + - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' + - 'node_modules/@prisma/client/**' + - 'node_modules/@aws-sdk/**' + - 'prisma/schema.prisma' + events: + - httpApi: + path: /minglaradmin/create-password + method: post + addCompanyDetails: handler: src/modules/minglaradmin/handlers/addCompanyDetails.handler package: diff --git a/src/modules/host/handlers/getbyidhandler.ts b/src/modules/host/handlers/getbyidhandler.ts new file mode 100644 index 0000000..6ab2163 --- /dev/null +++ b/src/modules/host/handlers/getbyidhandler.ts @@ -0,0 +1,50 @@ +import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda'; +import { safeHandler } from '../../../common/utils/handlers/safeHandler'; +import { PrismaService } from '../../../common/database/prisma.service'; +import { HostService } from '../services/host.service'; +import ApiError from '../../../common/utils/helper/ApiError'; +import { verifyHostToken } from '@/common/middlewares/jwt/authForHost'; + +const prismaService = new PrismaService(); +const hostService = new HostService(prismaService); + +export const handler = safeHandler(async ( + event: APIGatewayProxyEvent, + context?: Context +): Promise => { + // Get host ID from path parameters + 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 verifyHostToken(token); + const id = Number(userInfo.id) + + if (!id) { + throw new ApiError(400, 'Host ID is required'); + } + + if (isNaN(id)) { + throw new ApiError(400, 'Invalid host ID format'); + } + + const hostDetails = await hostService.getHostById(id); + + if (!hostDetails) { + throw new ApiError(404, 'Host not found'); + } + + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + body: JSON.stringify({ + success: true, + message: 'Host details retrieved successfully', + data: hostDetails, + }), + }; +}); diff --git a/src/modules/host/handlers/registration.ts b/src/modules/host/handlers/registration.ts index 5224a29..67790cb 100644 --- a/src/modules/host/handlers/registration.ts +++ b/src/modules/host/handlers/registration.ts @@ -44,7 +44,7 @@ export const handler = safeHandler(async ( newUser = user; } else { // โœ… No user found โ†’ create new one - newUser = await hostService.createHostUser(email); + newUser = await hostService.createMinglarUser(email); } const otpResult = await generateOtpHelper( diff --git a/src/modules/host/services/host.service.ts b/src/modules/host/services/host.service.ts index 83acc5a..8a41e73 100644 --- a/src/modules/host/services/host.service.ts +++ b/src/modules/host/services/host.service.ts @@ -19,10 +19,71 @@ export class HostService { } async getHostById(id: number) { - const host = await this.prisma.user.findUnique({ where: { id } }); + const host = await this.prisma.user.findUnique({ + where: { id }, + include: { + HostHeader: { + select: { + id: true, + companyName: true, + hostRefNumber: true, + address1: true, + address2: true, + cityXid: true, + stateXid: true, + countryXid: true, + pinCode: true, + logoPath: true, + isSubsidairy: true, + registrationNumber: true, + panNumber: true, + gstNumber: true, + formationDate: true, + companyType: true, + websiteUrl: true, + instagramUrl: true, + facebookUrl: true, + linkedinUrl: true, + twitterUrl: true, + currencyXid: true, + stepper: true, + hostStatusInternal: true, + hostStatusDisplay: true, + adminStatusInternal: true, + adminStatusDisplay: true, + amStatus: true, + agreementAccepted: true, + accountManagerXid: true, + isApproved: true, + agreementStartDate: true, + durationNumber: true, + durationFrequency: true, + isCommisionBase: true, + commisionPer: true, + amountPerBooking: true, + isActive: true, + createdAt: true, + updatedAt: true, + deletedAt: true, + currencies: true, + cities: true, + states: true, + countries: true, + HostBankDetails: true, + HostDocuments: true, + HostSuggestion: true, + hostParent: true, + HostTrack: true, + Activities: true, + }, + }, + }, + }); + if (!host || host.roleXid !== 4) { throw new ApiError(404, 'Host not found'); } + return host; } @@ -108,9 +169,9 @@ export class HostService { return existingUser; } - async createHostUser(email: string) { + async createMinglarUser(email: string) { const newUser = await this.prisma.user.create({ - data: { emailAddress: email, roleXid: 4 }, + data: { emailAddress: email, roleXid: 1 }, }); return newUser; } diff --git a/src/modules/minglaradmin/dto/minglar.dto.ts b/src/modules/minglaradmin/dto/minglar.dto.ts new file mode 100644 index 0000000..35b75ec --- /dev/null +++ b/src/modules/minglaradmin/dto/minglar.dto.ts @@ -0,0 +1,93 @@ +// src/modules/host/dto/host.dto.ts +import { IsInt, IsOptional, IsString, IsBoolean, IsEmail } from 'class-validator'; + +export class CreateMinglarDto { + @IsString() + firstName: string; + + @IsString() + lastName: string; + + @IsEmail() + emailAddress: string; + + @IsOptional() + @IsString() + isdCode?: string; + + @IsOptional() + @IsString() + mobileNumber?: string; + + @IsOptional() + @IsString() + userPassword?: string; + + @IsOptional() + @IsInt() + roleXid?: number; + + @IsOptional() + @IsBoolean() + isActive?: boolean; +} + +export class UpdateMinglarDto { + @IsOptional() + @IsString() + firstName?: string; + + @IsOptional() + @IsString() + lastName?: string; + + @IsOptional() + @IsEmail() + emailAddress?: string; + + @IsOptional() + @IsBoolean() + isActive?: boolean; +} + +export class GetMinglarLoginResponseDTO { + id: number; + firstName: string | null; + lastName: string | null; + emailAddress: string; + mobileNumber: string | null; + isActive: boolean; + roleXid: number; + accessToken: string; + refreshToken: string; + + constructor(user: any, accessToken: string, refreshToken: string) { + this.id = user.id; + this.firstName = user.firstName; + this.lastName = user.lastName; + this.emailAddress = user.emailAddress; + this.mobileNumber = user.mobileNumber; + this.isActive = user.isActive; + this.roleXid = user.roleXid; + this.accessToken = accessToken; + this.refreshToken = refreshToken; + } +} + +export class AddPaymentDetailsDTO { + bankXid: number; + bankBranchXid: number; + accountNumber: number; + accountHolderName: string; + ifscCode: string; + hostXid: number; + + constructor(bankXid: number, bankBranchXid: number, accountNumber: number, accountHolderName: string, ifscCode: string, hostXid: number) { + this.bankXid = bankXid; + this.bankBranchXid = bankBranchXid; + this.accountNumber = accountNumber; + this.accountHolderName = accountHolderName; + this.ifscCode = ifscCode; + this.hostXid = hostXid; + } +} \ No newline at end of file diff --git a/src/modules/minglaradmin/handlers/loginForHost.ts b/src/modules/minglaradmin/handlers/loginForHost.ts deleted file mode 100644 index 1ee191a..0000000 --- a/src/modules/minglaradmin/handlers/loginForHost.ts +++ /dev/null @@ -1,70 +0,0 @@ -// import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda'; -// import { safeHandler } from '../../../common/utils/handlers/safeHandler'; -// import { PrismaService } from '../../../common/database/prisma.service'; -// import { HostService } from '../services/host.service'; -// import { TokenService } from '../services/token.service'; -// import { GetHostLoginResponseDTO } from '../dto/host.dto'; -// import ApiError from '../../../common/utils/helper/ApiError'; -// import * as bcrypt from 'bcryptjs'; - -// const prismaService = new PrismaService(); -// const hostService = new HostService(prismaService); -// const tokenService = new TokenService(); - -// export const handler = safeHandler(async ( -// event: APIGatewayProxyEvent, -// context?: Context -// ): Promise => { -// // Parse request body -// 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 } = body; - -// if (!emailAddress) { -// throw new ApiError(400, 'Email is required'); -// } - -// const loginForHost = await hostService.loginForHost(emailAddress); - -// if (!loginForHost) { -// throw new ApiError(400, 'Failed to login'); -// } - -// if (!matchPassword) { -// throw new ApiError(401, 'Invalid credentials'); -// } - -// const generateTokenForHost = await tokenService.generateAuthToken( -// loginForHost.id -// ); - -// if (!generateTokenForHost) { -// throw new ApiError(500, 'Failed to generate token'); -// } - -// const loginForHostResponse = new GetHostLoginResponseDTO( -// loginForHost, -// generateTokenForHost.access.token, -// generateTokenForHost.refresh.token -// ); - -// return { -// statusCode: 200, -// headers: { -// 'Content-Type': 'application/json', -// 'Access-Control-Allow-Origin': '*', -// }, -// body: JSON.stringify({ -// success: true, -// message: 'Login successful', -// data: loginForHostResponse, -// }), -// }; -// }); - diff --git a/src/modules/minglaradmin/handlers/loginForMinglar.ts b/src/modules/minglaradmin/handlers/loginForMinglar.ts new file mode 100644 index 0000000..ede215e --- /dev/null +++ b/src/modules/minglaradmin/handlers/loginForMinglar.ts @@ -0,0 +1,75 @@ +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 { TokenService } from "../services/token.service"; +import { GetMinglarLoginResponseDTO } from '../dto/minglar.dto'; +import ApiError from '../../../common/utils/helper/ApiError'; +import * as bcrypt from 'bcryptjs'; + +const prismaService = new PrismaService(); +const minglarSerivce = new MinglarService(prismaService); +const tokenService = new TokenService(); + +export const handler = safeHandler(async ( + event: APIGatewayProxyEvent, + context?: Context +): Promise => { + // Parse request body + 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) { + throw new ApiError(400, 'Email is required'); + } + + const loginForMinglar = await minglarSerivce.loginForMinglar(emailAddress ,userPassword); + + if (!loginForMinglar) { + throw new ApiError(400, 'Failed to login'); + } + + const matchPassword = await bcrypt.compare( + userPassword, + loginForMinglar.userPassword + ); + + if (!matchPassword) { + throw new ApiError(401, 'Invalid credentials'); + } + + const generateTokenForHost = await tokenService.generateAuthToken( + loginForMinglar.id + ); + + if (!generateTokenForHost) { + throw new ApiError(500, 'Failed to generate token'); + } + + const loginForHostResponse = new GetMinglarLoginResponseDTO( + loginForMinglar, + generateTokenForHost.access.token, + generateTokenForHost.refresh.token + ); + + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + body: JSON.stringify({ + success: true, + message: 'Login successful', + data: loginForHostResponse, + }), + }; +}); + diff --git a/src/modules/minglaradmin/handlers/registration.ts b/src/modules/minglaradmin/handlers/registration.ts new file mode 100644 index 0000000..11aeaf5 --- /dev/null +++ b/src/modules/minglaradmin/handlers/registration.ts @@ -0,0 +1,75 @@ +import { MinglarService } from './../services/minglar.service'; +import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda'; +import { safeHandler } from '../../../common/utils/handlers/safeHandler'; +import { PrismaService } from '../../../common/database/prisma.service'; +import ApiError from '../../../common/utils/helper/ApiError'; +import * as bcrypt from 'bcryptjs'; +import { generateOtpHelper } from '../../../common/utils/helper/sendOtp'; + +const prismaService = new PrismaService(); +const minglarService = new MinglarService(prismaService); + +export const handler = safeHandler(async ( + event: APIGatewayProxyEvent, + context?: Context +): Promise => { + // Parse request body + let body: { email?: string }; + + try { + body = event.body ? JSON.parse(event.body) : {}; + } catch (error) { + throw new ApiError(400, 'Invalid JSON in request body'); + } + + const { email } = body; + + if (!email) { + throw new ApiError(400, 'Email is required'); + } + + const user = await prismaService.user.findUnique({ + where: { emailAddress: email }, + select: { emailAddress: true, id: true, userPassword: true }, + }); + + if (user && user.userPassword) { + throw new ApiError(404, 'User is already registered. Please login.'); + } + + let newUser; + + if (user && !user.userPassword) { + // โœ… User already exists but without password โ†’ reuse record + newUser = user; + } else { + // โœ… No user found โ†’ create new one + newUser = await minglarService.createHostUser(email); + } + + const otpResult = await generateOtpHelper( + Number(newUser?.id), + newUser?.emailAddress, + 'Register', + 6, + 5 + ); + + if (!otpResult || !otpResult.otp) { + throw new ApiError(500, 'Failed to send OTP'); + } + + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + body: JSON.stringify({ + success: true, + message: 'OTP sent successfully.', + data: {}, + }), + }; +}); + diff --git a/src/modules/minglaradmin/services/minglar.service.ts b/src/modules/minglaradmin/services/minglar.service.ts index b3c697b..7581a3a 100644 --- a/src/modules/minglaradmin/services/minglar.service.ts +++ b/src/modules/minglaradmin/services/minglar.service.ts @@ -4,6 +4,8 @@ import ApiError from '../../../common/utils/helper/ApiError'; import * as bcrypt from 'bcryptjs'; import { z } from 'zod'; import { hostCompanyDetailsSchema } from '../../../common/utils/validation/host/hostCompanyDetails.validation'; +import { CreateMinglarDto, UpdateMinglarDto } from '../dto/minglar.dto'; +import { User } from '@prisma/client'; type HostCompanyDetailsInput = z.infer; @@ -47,6 +49,134 @@ export class MinglarService { return true; } + async createHost(data: CreateMinglarDto) { + return this.prisma.user.create({ data }); + } + + async getAllHosts() { + return this.prisma.user.findMany({ where: { roleXid: 3 } }); + } + + async getHostById(id: number) { + const host = await this.prisma.user.findUnique({ + where: { id }, + include: { + HostHeader: { + include: { + currencies: true, + cities: true, + states: true, + countries: true, + HostBankDetails: true, + HostDocuments: true, + HostSuggestion: true, + hostParent: true, + HostTrack: true, + Activities: true, + }, + }, + UserOtp: true, + Token: true, + }, + }); + + if (!host || host.roleXid !== 4) { + throw new ApiError(404, 'Host not found'); + } + + return host; + } + + async updateHost(id: number, data: UpdateMinglarDto) { + return this.prisma.user.update({ + where: { id }, + data, + }); + } + + async deleteHost(id: number) { + return this.prisma.user.delete({ where: { id } }); + } + + async getHostByEmail(email: string): Promise { + return this.prisma.user.findUnique({ where: { emailAddress: email } }); + } + + async verifyHostOtp(email: string, otp: string): Promise { + const user = await this.prisma.user.findUnique({ + where: { emailAddress: email }, + select: { + id: true, + emailAddress: true, + UserOtp: { + where: { isActive: true, isVerified: false }, + orderBy: { createdAt: 'desc' }, + take: 1, + }, + }, + }); + + if (!user) { + throw new ApiError(404, 'User not found.'); + } + + const userOtp = user.UserOtp[0]; + + if (!userOtp) { + throw new ApiError(400, 'No OTP found.'); + } + + if (new Date() > userOtp.expiresOn) { + throw new ApiError(400, 'OTP has expired.'); + } + + const isMatch = await bcrypt.compare(otp, userOtp.otpCode); + + if (!isMatch) { + throw new ApiError(400, 'Invalid OTP.'); + } + + await this.prisma.userOtp.update({ + where: { id: userOtp.id }, + data: { + isVerified: true, + verifiedOn: new Date(), + isActive: false, + }, + }); + + return true; + } + + async loginForMinglar(emailAddress: string, userPassword: string) { + const existingUser = await this.prisma.user.findUnique({ + where: { emailAddress: emailAddress }, + }); + + if (!existingUser) { + throw new ApiError(404, 'User not found'); + } + + if (existingUser.roleXid !== 4) { + throw new ApiError(403, 'Access denied. Not a host user.'); + } + + const matchPassword = await bcrypt.compare(userPassword, existingUser.userPassword); + if (!matchPassword) { + throw new ApiError(401, 'Invalid credentials'); + } + + return existingUser; + } + + async createHostUser(email: string) { + const newUser = await this.prisma.user.create({ + data: { emailAddress: email, roleXid: 4 }, + }); + return newUser; + } + + async addCompanyDetails( companyData: HostCompanyDetailsInput, documents: HostDocumentInput[] // Documents with S3 URLs diff --git a/src/modules/minglaradmin/services/token.service.ts b/src/modules/minglaradmin/services/token.service.ts new file mode 100644 index 0000000..50de3a7 --- /dev/null +++ b/src/modules/minglaradmin/services/token.service.ts @@ -0,0 +1,159 @@ +import { PrismaClient } from "@prisma/client"; +import jwt, { JwtPayload } from "jsonwebtoken"; +import moment from "moment"; +import config from "../../../config/config"; + +const prisma = new PrismaClient(); + +export class TokenService { + private generateToken( + user_xid: number, + expiresIn: Date, + type: string, + secret: string + ): { token: string; expires: Date } { + const token = jwt.sign( + { + sub: user_xid, + iat: moment().unix(), + exp: moment(expiresIn).unix(), + type, + }, + secret + ); + + return { token, expires: expiresIn }; + } + + async generateAuthToken( + user_xid: number, + ): Promise<{ + access: { token: string; expires: Date }; + refresh: { token: string; expires: Date }; + }> { + const accessTokenExpires = moment() + .add(config.jwt.accessExpirationMinutes, "minutes") + .toDate(); + + const refreshTokenExpires = moment() + .add(config.jwt.refreshExpirationDays, "days") + .toDate(); + + const accessToken = this.generateToken( + user_xid, + accessTokenExpires, + "access", + config.jwt.secret + ); + + const refreshToken = this.generateToken( + user_xid, + refreshTokenExpires, + "refresh", + config.jwt.secret + ); + + await prisma.token.create({ + data: { + token: refreshToken.token, + expiringAt: refreshToken.expires, + tokenType: "refresh", + isBlackListed: false, + + user: { + connect: { id: user_xid }, + }, + }, + }); + + return { + access: accessToken, + refresh: refreshToken, + }; + } + + async generateAuthTokenAdmin( + user_xid: number + ): Promise<{ + access: { token: string; expires: Date }; + refresh: { token: string; expires: Date }; + }> { + const accessTokenExpires = moment() + .add(config.jwt.accessExpirationMinutes, "minutes") + .toDate(); + + const refreshTokenExpires = moment() + .add(config.jwt.refreshExpirationDays, "days") + .toDate(); + + const accessToken = this.generateToken( + user_xid, + accessTokenExpires, + "access", + config.jwt.secret + ); + + const refreshToken = this.generateToken( + user_xid, + refreshTokenExpires, + "refresh", + config.jwt.secret + ); + + await prisma.token.create({ + data: { + token: refreshToken.token, + expiringAt: refreshToken.expires, + tokenType: "refresh", + isBlackListed: false, + user: { + connect: { id: user_xid }, + }, + }, + }); + + return { + access: accessToken, + refresh: refreshToken, + }; + } + + async revokeToken(user_xid: number, deviceId: string): Promise { + const existingToken = await prisma.token.findFirst({ + where: { + id: user_xid, + deviceId, + }, + }); + + if (!existingToken) return false; + + await prisma.token.delete({ where: { id: existingToken.id } }); + return true; + } + + async isTokenBlackListed(token: string): Promise { + const existing = await prisma.token.findUnique({ + where: { token }, + }); + return existing ? true : false; + } + + async verifyRefreshToken( + token: string + ): Promise { + try { + return jwt.verify(token, config.jwt.secret); + } catch { + return null; + } + } + + async decodeToken(token: string): Promise { + try { + return jwt.decode(token); + } catch { + return null; + } + } +} diff --git a/swagger.json b/swagger.json index 300e5db..476e7a6 100644 --- a/swagger.json +++ b/swagger.json @@ -1,631 +1,27 @@ { "openapi": "3.0.0", + "paths": {}, "info": { "title": "Minglar API", - "description": "NestJS Backend for Minglar with AWS Lambda endpoints", + "description": "NestJS Backend for Minglar with Lambda-ready endpoints", "version": "1.0.0", - "contact": { - "name": "API Support" - } + "contact": {} }, + "tags": [], "servers": [ { - "url": "https://api.minglar.com", - "description": "Production Server" - }, - { - "url": "http://localhost:3000", - "description": "Local Development Server" + "url": "http://localhost:3000/", + "description": "Local Server" } ], - "tags": [ - { - "name": "Host", - "description": "Host management endpoints" - }, - { - "name": "Authentication", - "description": "Authentication and authorization endpoints" - } - ], - "paths": { - "/host": { - "get": { - "tags": ["Host"], - "summary": "Get all hosts", - "description": "Retrieves a list of all host headers with basic information", - "operationId": "getHosts", - "responses": { - "200": { - "description": "Successful response", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/HostHeader" - } - }, - "example": [ - { - "hostParent": "Example Host", - "hostRefNumber": "HOST001", - "hostStatusDisplay": "Active", - "accountManager": "John Doe" - } - ] - } - } - }, - "500": { - "description": "Internal server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - } - } - }, - "/host/registration": { - "post": { - "tags": ["Authentication"], - "summary": "Register a new host", - "description": "Initiates host registration by sending an OTP to the provided email address", - "operationId": "registrationOfHost", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RegistrationRequest" - }, - "example": { - "email": "host@example.com" - } - } - } - }, - "responses": { - "200": { - "description": "OTP sent successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuccessResponse" - }, - "example": { - "success": true, - "message": "OTP sent successfully.", - "data": {} - } - } - } - }, - "400": { - "description": "Bad request - Email is required", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, - "example": { - "success": false, - "message": "Email is required", - "data": null, - "error": { - "code": 400, - "description": "Email is required", - "statusCode": 400 - } - } - } - } - }, - "404": { - "description": "User already registered", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, - "example": { - "success": false, - "message": "User is already registered. Please login.", - "data": null, - "error": { - "code": 404, - "description": "User is already registered. Please login.", - "statusCode": 404 - } - } - } - } - }, - "500": { - "description": "Internal server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - } - } - }, - "/host/verify-otp": { - "post": { - "tags": ["Authentication"], - "summary": "Verify OTP", - "description": "Verifies the OTP sent to the user's email during registration", - "operationId": "verifyOtp", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/VerifyOtpRequest" - }, - "example": { - "email": "host@example.com", - "otp": "123456" - } - } - } - }, - "responses": { - "200": { - "description": "OTP verified successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuccessResponse" - }, - "example": { - "success": true, - "message": "OTP verified successfully", - "data": null - } - } - } - }, - "400": { - "description": "Bad request - Invalid OTP or missing fields", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, - "examples": { - "missingFields": { - "value": { - "success": false, - "message": "Email and OTP are required", - "data": null, - "error": { - "code": 400, - "description": "Email and OTP are required", - "statusCode": 400 - } - } - }, - "invalidOtp": { - "value": { - "success": false, - "message": "Invalid OTP.", - "data": null, - "error": { - "code": 400, - "description": "Invalid OTP.", - "statusCode": 400 - } - } - }, - "expiredOtp": { - "value": { - "success": false, - "message": "OTP has expired.", - "data": null, - "error": { - "code": 400, - "description": "OTP has expired.", - "statusCode": 400 - } - } - } - } - } - } - }, - "404": { - "description": "User not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, - "example": { - "success": false, - "message": "User not found.", - "data": null, - "error": { - "code": 404, - "description": "User not found.", - "statusCode": 404 - } - } - } - } - }, - "500": { - "description": "Internal server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - } - } - }, - "/host/login": { - "post": { - "tags": ["Authentication"], - "summary": "Login for host", - "description": "Authenticates a host user and returns an access token", - "operationId": "loginForHost", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LoginRequest" - }, - "example": { - "emailAddress": "host@example.com", - "userPassword": "password123" - } - } - } - }, - "responses": { - "200": { - "description": "Login successful", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LoginResponse" - }, - "example": { - "success": true, - "message": "Login successful", - "data": { - "id": 1, - "firstName": "John", - "lastName": "Doe", - "emailAddress": "host@example.com", - "mobileNumber": "+1234567890", - "isActive": true, - "roleXid": 4, - "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." - } - } - } - } - }, - "400": { - "description": "Bad request - Missing fields or failed login", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, - "example": { - "success": false, - "message": "Email and password are required", - "data": null, - "error": { - "code": 400, - "description": "Email and password are required", - "statusCode": 400 - } - } - } - } - }, - "401": { - "description": "Unauthorized - Invalid credentials", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, - "example": { - "success": false, - "message": "Invalid credentials", - "data": null, - "error": { - "code": 401, - "description": "Invalid credentials", - "statusCode": 401 - } - } - } - } - }, - "403": { - "description": "Forbidden - Not a host user", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, - "example": { - "success": false, - "message": "Access denied. Not a host user.", - "data": null, - "error": { - "code": 403, - "description": "Access denied. Not a host user.", - "statusCode": 403 - } - } - } - } - }, - "404": { - "description": "User not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - }, - "example": { - "success": false, - "message": "User not found", - "data": null, - "error": { - "code": 404, - "description": "User not found", - "statusCode": 404 - } - } - } - } - }, - "500": { - "description": "Internal server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - } - } - } - }, "components": { "securitySchemes": { - "bearerAuth": { - "type": "http", + "bearer": { "scheme": "bearer", "bearerFormat": "JWT", - "description": "JWT token obtained from login endpoint" + "type": "http" } }, - "schemas": { - "HostHeader": { - "type": "object", - "properties": { - "hostParent": { - "type": "string", - "description": "Host parent name", - "example": "Example Host" - }, - "hostRefNumber": { - "type": "string", - "description": "Host reference number", - "example": "HOST001" - }, - "hostStatusDisplay": { - "type": "string", - "description": "Host status display", - "example": "Active" - }, - "accountManager": { - "type": "string", - "description": "Account manager name", - "example": "John Doe" - } - } - }, - "RegistrationRequest": { - "type": "object", - "required": ["email"], - "properties": { - "email": { - "type": "string", - "format": "email", - "description": "Email address for registration", - "example": "host@example.com" - } - } - }, - "VerifyOtpRequest": { - "type": "object", - "required": ["email", "otp"], - "properties": { - "email": { - "type": "string", - "format": "email", - "description": "Email address used during registration", - "example": "host@example.com" - }, - "otp": { - "type": "string", - "description": "6-digit OTP code sent to email", - "pattern": "^[0-9]{6}$", - "example": "123456" - } - } - }, - "LoginRequest": { - "type": "object", - "required": ["emailAddress", "userPassword"], - "properties": { - "emailAddress": { - "type": "string", - "format": "email", - "description": "Host user email address", - "example": "host@example.com" - }, - "userPassword": { - "type": "string", - "format": "password", - "description": "User password", - "minLength": 8, - "example": "password123" - } - } - }, - "LoginResponseData": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "description": "User ID", - "example": 1 - }, - "firstName": { - "type": "string", - "nullable": true, - "description": "User first name", - "example": "John" - }, - "lastName": { - "type": "string", - "nullable": true, - "description": "User last name", - "example": "Doe" - }, - "emailAddress": { - "type": "string", - "format": "email", - "description": "User email address", - "example": "host@example.com" - }, - "mobileNumber": { - "type": "string", - "nullable": true, - "description": "User mobile number", - "example": "+1234567890" - }, - "isActive": { - "type": "boolean", - "description": "User active status", - "example": true - }, - "roleXid": { - "type": "integer", - "description": "User role ID (4 for host)", - "example": 4 - }, - "accessToken": { - "type": "string", - "description": "JWT access token for authentication", - "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." - } - } - }, - "LoginResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "example": true - }, - "message": { - "type": "string", - "example": "Login successful" - }, - "data": { - "$ref": "#/components/schemas/LoginResponseData" - } - } - }, - "SuccessResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "example": true - }, - "message": { - "type": "string", - "example": "Operation successful" - }, - "data": { - "type": "object", - "description": "Response data (can be null or empty object)" - } - } - }, - "ErrorResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "example": false - }, - "message": { - "type": "string", - "description": "Error message", - "example": "An error occurred" - }, - "data": { - "type": "object", - "nullable": true, - "example": null - }, - "error": { - "type": "object", - "properties": { - "code": { - "type": "integer", - "description": "HTTP status code", - "example": 400 - }, - "description": { - "type": "string", - "description": "Error description", - "example": "Bad request" - }, - "statusCode": { - "type": "integer", - "description": "HTTP status code", - "example": 400 - }, - "debug": { - "type": "string", - "description": "Debug information (only in non-production environments)", - "example": "Stack trace..." - } - }, - "required": ["code", "description", "statusCode"] - } - }, - "required": ["success", "message", "data", "error"] - } - } + "schemas": {} } -} +} \ No newline at end of file From fb72feb20e39e941320f687af522638b3cb7e35b Mon Sep 17 00:00:00 2001 From: paritosh18 Date: Thu, 13 Nov 2025 15:56:13 +0530 Subject: [PATCH 2/3] add api --- serverless.yml | 14 + src/common/utils/helper/s3Upload.ts | 262 +++++++++++++----- .../common/handlers/getStepperHandler.ts | 98 +++++++ .../handlers/addCompanyDetails.ts | 2 +- test-stepper-handler.ts | 122 ++++++++ 5 files changed, 424 insertions(+), 74 deletions(-) create mode 100644 src/modules/common/handlers/getStepperHandler.ts create mode 100644 test-stepper-handler.ts diff --git a/serverless.yml b/serverless.yml index a18a2ca..8e9b092 100644 --- a/serverless.yml +++ b/serverless.yml @@ -180,6 +180,20 @@ functions: path: /host/{id} method: get + getStepperInfo: + handler: src/common/utils/handlers/getStepperHandler.handler + package: + patterns: + - 'src/**' + - 'common/**' + - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node' + - 'node_modules/@prisma/client/**' + - 'prisma/schema.prisma' + events: + - httpApi: + path: /stepper + method: get + # ๐Ÿ‘‡ Minglar Admin Module minglarRegistration: diff --git a/src/common/utils/helper/s3Upload.ts b/src/common/utils/helper/s3Upload.ts index 0221fe8..bb81142 100644 --- a/src/common/utils/helper/s3Upload.ts +++ b/src/common/utils/helper/s3Upload.ts @@ -1,75 +1,191 @@ -import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; -import ApiError from './ApiError'; -import * as crypto from 'crypto'; -import config from '@/config/config'; - -const AWS_REGION = config.aws.region; -const S3_BUCKET_NAME = config.aws.bucketName; - -const s3Client = new S3Client({ - region: AWS_REGION, -}); - -interface UploadFileParams { - fileData: string; // base64 encoded file - fileName: string; - folder?: string; // Optional folder path in S3 - contentType?: string; -} - -/** - * Upload file to S3 and return the S3 URL - */ -export async function uploadFileToS3(params: UploadFileParams): Promise { - try { - const { fileData, fileName, folder = 'documents', contentType = 'application/pdf' } = params; - - // Generate unique file name - const fileExtension = fileName.split('.').pop() || 'pdf'; - const uniqueFileName = `${crypto.randomUUID()}-${Date.now()}.${fileExtension}`; - const s3Key = folder ? `${folder}/${uniqueFileName}` : uniqueFileName; - - // Decode base64 file data - const fileBuffer = Buffer.from(fileData, 'base64'); - - // Upload to S3 - const command = new PutObjectCommand({ - Bucket: S3_BUCKET_NAME, - Key: s3Key, - Body: fileBuffer, - ContentType: contentType, - // Make file publicly readable (adjust based on your needs) - // ACL: 'public-read', // Uncomment if you want public access - }); - - await s3Client.send(command); - - // Return S3 URL - const s3Url = `https://${S3_BUCKET_NAME}.s3.${AWS_REGION}.amazonaws.com/${s3Key}`; - - return s3Url; - } catch (error) { - console.error('S3 upload error:', error); - throw new ApiError(500, 'Failed to upload file to S3'); - } -} - -/** - * Upload multiple files to S3 - */ -export async function uploadFilesToS3( - files: Array<{ fileData: string; fileName: string; contentType?: string }>, - folder?: string -): Promise { - const uploadPromises = files.map((file) => - uploadFileToS3({ - fileData: file.fileData, - fileName: file.fileName, - folder, - contentType: file.contentType, +import dotenv from 'dotenv'; +import path from 'path'; +import * as yup from 'yup'; + +dotenv.config({ path: path.join(__dirname, '../../.env') }); + +const envVarsSchema = yup + .object() + .shape({ + NODE_ENV: yup + .string() + .oneOf(['production', 'development', 'test']) + .required(), + PORT: yup.number().default(3000), + // FRONTEND_URL: yup.string().required('Frontend URL is required'), + //JWT + JWT_SECRET: yup.string().required('JWT secret key is required'), + JWT_ACCESS_EXPIRATION_MINUTES: yup + .number() + .default(30) + .required('minutes after which access tokens expire'), + JWT_REFRESH_EXPIRATION_DAYS: yup + .number() + .default(30) + .required('days after which refresh tokens expire'), + JWT_RESET_PASSWORD_EXPIRATION_MINUTES: yup + .number() + .default(10) + .required('minutes after which reset password token expires'), + JWT_VERIFY_EMAIL_EXPIRATION_MINUTES: yup + .number() + .default(10) + .required('minutes after which verify email token expires'), + AWS_REGION: yup.string().required('AWS region is required'), + S3_BUCKET_NAME: yup.string().required('S3 bucket name is required'), + //SMTP and BREVO + // BREVO_SMTP_HOST: yup + // .string() + // .nullable() + // .required('server that will send the emails'), + // BREVO_SMTP_PORT: yup + // .number() + // .nullable() + // .required('port to connect to the email server'), + // BREVO_SMTP_USER: yup + // .string() + // .nullable() + // .required('username for email server'), + // BREVO_SMTP_PASS: yup + // .string() + // .nullable() + // .required('password for email server'), + // BREVO_FROM_EMAIL: yup + // .string() + // .nullable() + // .required('the from field in the emails sent by the app'), + // BREVO_EMAIL_API_KEY: yup + // .string() + // .nullable() + // .required('the from field in the emails sent by the app api key'), + // BREVO_API_BASEURL: yup.string().required('Brevo base URL is required'), + // //one signal + // ONESIGNAL_APPID: yup.string().required('One signal app id is required'), + // ONESIGNAL_REST_APIKEY: yup + // .string() + // .required('One signal api key is required'), + //branch IO + // BRANCH_IO_KEY: yup.string().required('Branch IO key is required'), + + // DataBase + DB_USERNAME: yup.string().required('DB Username is required'), + DB_PASSWORD: yup.string().required('DB Password is required'), + DB_DATABASE_NAME: yup.string().required('Database name is required'), + DB_HOSTNAME: yup + .string() + .default('127.0.0.1') + .required('DB Hostname is required'), + DB_PORT: yup.number().default(3306).required('DB Port is required'), + //OTP Bypass + BYPASS_OTP: yup.boolean().default(false).required('Bypass OTP is required'), }) - ); - - return Promise.all(uploadPromises); + .noUnknown(true); + +// Validate and prepare the configuration +function getConfig() { + try { + // Validate the environment variables + const envVars = envVarsSchema.validateSync(process.env, { + abortEarly: false, // Validate all fields before throwing errors + stripUnknown: true, // Remove fields not in the schema + }); + + // Return the validated configuration + return { + env: envVars.NODE_ENV, + port: envVars.PORT, + jwt: { + secret: envVars.JWT_SECRET, + accessExpirationMinutes: envVars.JWT_ACCESS_EXPIRATION_MINUTES, + refreshExpirationDays: envVars.JWT_REFRESH_EXPIRATION_DAYS, + resetPasswordExpirationMinutes: + envVars.JWT_RESET_PASSWORD_EXPIRATION_MINUTES, + verifyEmailExpirationMinutes: + envVars.JWT_VERIFY_EMAIL_EXPIRATION_MINUTES, + }, + database: { + development: { + host: envVars.DB_HOSTNAME, + port: envVars.DB_PORT, + username: envVars.DB_USERNAME, + password: envVars.DB_PASSWORD, + database: envVars.DB_DATABASE_NAME, + logging: false, + }, + test: { + host: envVars.DB_HOSTNAME, + port: envVars.DB_PORT, + username: envVars.DB_USERNAME, + password: envVars.DB_PASSWORD, + database: envVars.DB_DATABASE_NAME, + logging: false, + socketPath: '/var/run/mysqld/mysqld.sock', + }, + production: { + host: envVars.DB_HOSTNAME, + port: envVars.DB_PORT, + username: envVars.DB_USERNAME, + password: envVars.DB_PASSWORD, + database: envVars.DB_DATABASE_NAME, + logging: false, + socketPath: '/var/run/mysqld/mysqld.sock', + }, + }, + aws: { + region: envVars.AWS_REGION, + bucketName: envVars.S3_BUCKET_NAME, + }, + byPassOTP: envVars.BYPASS_OTP, + // BaseURL: envVars.BASEURL, + // FRONTEND_URL: envVars.FRONTEND_URL, + // email: { + // smtp: { + // host: envVars?.BREVO_SMTP_HOST, + // port: envVars?.BREVO_SMTP_PORT, + // secure: envVars?.BREVO_SMTP_PORT == 465, // true for 465, false for other ports + // auth: { + // user: envVars?.BREVO_SMTP_USER, + // pass: envVars?.BREVO_SMTP_PASS, + // }, + // }, + // from: envVars?.BREVO_FROM_EMAIL, + // api_key: envVars?.BREVO_EMAIL_API_KEY, + // BrevobaseURL: envVars?.BREVO_API_BASEURL, + // }, + // oneSignal: { + // appID: envVars.ONESIGNAL_APPID, + // restApiKey: envVars.ONESIGNAL_REST_APIKEY, + // }, + // branchIO: { + // branchIOKey: envVars.BRANCH_IO_KEY, + // }, + }; + } catch (error: unknown) { + if (error instanceof yup.ValidationError) { + console.error('Validation Errors:', error.errors.join(', ')); + } else { + console.error('Unexpected error during configuration validation:', error); + } + + console.error( + 'Server shut down due to incomplete environment variable configuration.' + ); + process.exit(1); // Exit with error code 1 + } } - + +/** + * Created By : Angad Chauhan + * Created at : 31/1/25 + * Use : For google login .env file global variable + */ +// export const googleConfig = { +// clientID: process.env.GOOGLE_CLIENT_ID!, +// clientSecret: process.env.GOOGLE_CLIENT_SECRET!, +// callbackURL: process.env.GOOGLE_CALLBACK_URL!, +// }; + +// Validate and export configuration only if validation succeeds +const config = getConfig(); +export default config; + \ No newline at end of file diff --git a/src/modules/common/handlers/getStepperHandler.ts b/src/modules/common/handlers/getStepperHandler.ts new file mode 100644 index 0000000..a4cb3c7 --- /dev/null +++ b/src/modules/common/handlers/getStepperHandler.ts @@ -0,0 +1,98 @@ +import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda'; +import { safeHandler } from '../../../common/utils/handlers/safeHandler'; +import { PrismaService } from '../../../common/database/prisma.service'; +import ApiError from '../../../common/utils/helper/ApiError'; +import { verifyHostToken } from '../../../common/middlewares/jwt/authForHost'; + +const prismaService = new PrismaService(); + +export const handler = safeHandler(async ( + event: APIGatewayProxyEvent, + context?: Context +): Promise => { + // Extract token from headers + 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.'); + } + + // Verify token and get user info + const userInfo = await verifyHostToken(token); + const userId = Number(userInfo.id); + + if (!userId || isNaN(userId)) { + throw new ApiError(400, 'Invalid user ID'); + } + + // Fetch user with their HostHeader stepper info + const user = await prismaService.user.findUnique({ + where: { id: userId }, + select: { + id: true, + firstName: true, + lastName: true, + emailAddress: true, + roleXid: true, + HostHeader: { + select: { + id: true, + stepper: true, + }, + }, + }, + }); + + if (!user) { + throw new ApiError(404, 'User not found'); + } + + if (!user.HostHeader || user.HostHeader.length === 0) { + throw new ApiError(404, 'No HostHeader record found for this user'); + } + + // Return stepper info along with user and host details + const hostHeader = user.HostHeader[0]; + + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + body: JSON.stringify({ + success: true, + message: 'Stepper information retrieved successfully', + data: { + user: { + id: user.id, + firstName: user.firstName, + lastName: user.lastName, + emailAddress: user.emailAddress, + roleXid: user.roleXid, + }, + stepper: { + hostId: hostHeader.id, + currentStep: hostHeader.stepper, + stepperDescription: getStepperDescription(hostHeader.stepper), + }, + }, + }), + }; +}); + +/** + * Get a human-readable description of the stepper value + */ +function getStepperDescription(stepper: number): string { + const stepDescriptions: { [key: number]: string } = { + 1: 'Basic Company Information', + 2: 'Company Documents & Verification', + 3: 'Bank & Payment Details', + 4: 'Activities Setup', + 5: 'Pricing & Services', + 6: 'Review & Approval', + 7: 'Active & Live', + }; + + return stepDescriptions[stepper] || 'Unknown Step'; +} diff --git a/src/modules/minglaradmin/handlers/addCompanyDetails.ts b/src/modules/minglaradmin/handlers/addCompanyDetails.ts index 5dc373a..deecbd0 100644 --- a/src/modules/minglaradmin/handlers/addCompanyDetails.ts +++ b/src/modules/minglaradmin/handlers/addCompanyDetails.ts @@ -9,7 +9,7 @@ import { REQUIRED_DOC_TYPES, } from '../../../common/utils/validation/host/hostCompanyDetails.validation'; import { uploadFilesToS3 } from '../../../common/utils/helper/s3Upload'; -import { parseMultipartFormData } from '../../../common/utils/helper/parseMultipartFormData'; +import { parseMultipartFormData } from '../../../common/utils/validation/host/'; const prismaService = new PrismaService(); const minglarService = new MinglarService(prismaService); diff --git a/test-stepper-handler.ts b/test-stepper-handler.ts new file mode 100644 index 0000000..8084753 --- /dev/null +++ b/test-stepper-handler.ts @@ -0,0 +1,122 @@ +/** + * Test script for stepper handler + * Run with: npx ts-node test-stepper-handler.ts + */ + +import { PrismaClient } from '@prisma/client'; +import * as jwt from 'jsonwebtoken'; + +const prisma = new PrismaClient(); + +async function testStepperHandler() { + console.log('๐Ÿงช Testing Stepper Handler...\n'); + + try { + // 1. Find a host user with HostHeader data + console.log('๐Ÿ“ Step 1: Finding host user with HostHeader...'); + const hostUser = await prisma.user.findFirst({ + where: { roleXid: 4 }, + include: { + HostHeader: { + select: { + id: true, + stepper: true, + hostRefNumber: true, + companyName: true, + }, + }, + }, + }); + + if (!hostUser) { + console.log('โŒ No host user found (roleXid=4) in database.'); + return; + } + + if (!hostUser.HostHeader || hostUser.HostHeader.length === 0) { + console.log('โš ๏ธ Host user found but no HostHeader records.'); + console.log(` User ID: ${hostUser.id}, Email: ${hostUser.emailAddress}`); + return; + } + + const hostHeader = hostUser.HostHeader[0]; + console.log(`โœ… Found host user and HostHeader\n`); + console.log(` User ID: ${hostUser.id}`); + console.log(` Email: ${hostUser.emailAddress}`); + console.log(` First Name: ${hostUser.firstName}`); + console.log(` Host ID: ${hostHeader.id}`); + console.log(` Company: ${hostHeader.companyName}`); + console.log(` Ref Number: ${hostHeader.hostRefNumber}`); + console.log(` Current Stepper: ${hostHeader.stepper}\n`); + + // 2. Simulate what the handler returns + console.log('๐Ÿ“ Step 2: Simulating handler response...\n'); + + const stepDescriptions: { [key: number]: string } = { + 1: 'Basic Company Information', + 2: 'Company Documents & Verification', + 3: 'Bank & Payment Details', + 4: 'Activities Setup', + 5: 'Pricing & Services', + 6: 'Review & Approval', + 7: 'Active & Live', + }; + + const stepperDescription = + stepDescriptions[hostHeader.stepper] || 'Unknown Step'; + + const response = { + success: true, + message: 'Stepper information retrieved successfully', + data: { + user: { + id: hostUser.id, + firstName: hostUser.firstName, + lastName: hostUser.lastName, + emailAddress: hostUser.emailAddress, + roleXid: hostUser.roleXid, + }, + stepper: { + hostId: hostHeader.id, + currentStep: hostHeader.stepper, + stepperDescription: stepperDescription, + }, + }, + }; + + console.log('โœ… Handler Response:\n'); + console.log(JSON.stringify(response, null, 2)); + + // 3. Verify stepper value is numeric + console.log('\n๐Ÿ“ Step 3: Validation checks...\n'); + + if (typeof hostHeader.stepper !== 'number') { + console.log('โŒ Stepper is not a number'); + return; + } + console.log('โœ… Stepper is numeric:', hostHeader.stepper); + + if (hostHeader.stepper < 1 || hostHeader.stepper > 7) { + console.log( + 'โš ๏ธ Stepper value is out of expected range (1-7):', + hostHeader.stepper + ); + } else { + console.log('โœ… Stepper value in valid range (1-7)'); + } + + if (!stepperDescription.includes('Unknown')) { + console.log('โœ… Stepper description found:', stepperDescription); + } else { + console.log('โš ๏ธ Unknown stepper value'); + } + + console.log('\nโœ… Test passed! Handler is working correctly.'); + } catch (error) { + console.error('โŒ Test error:', error); + } finally { + await prisma.$disconnect(); + } +} + +testStepperHandler(); From 2ed5435a156eeb45afcdcd8cc9a5e04192ca0303 Mon Sep 17 00:00:00 2001 From: paritosh18 Date: Thu, 13 Nov 2025 16:04:20 +0530 Subject: [PATCH 3/3] resolvec --- .../handlers/addCompanyDetails.ts | 296 +++++++++--------- 1 file changed, 144 insertions(+), 152 deletions(-) diff --git a/src/modules/minglaradmin/handlers/addCompanyDetails.ts b/src/modules/minglaradmin/handlers/addCompanyDetails.ts index 0346349..4f955a0 100644 --- a/src/modules/minglaradmin/handlers/addCompanyDetails.ts +++ b/src/modules/minglaradmin/handlers/addCompanyDetails.ts @@ -1,185 +1,177 @@ -import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda'; +import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; import { safeHandler } from '../../../common/utils/handlers/safeHandler'; import { PrismaService } from '../../../common/database/prisma.service'; -import { MinglarService } from '../services/minglar.service'; +import { HostService } from '../../host/services/host.service'; import ApiError from '../../../common/utils/helper/ApiError'; -import { verifyHostToken } from '../../../common/middlewares/jwt/authForMinglarAdmin'; +import { verifyHostToken } from '../../../common/middlewares/jwt/authForHost'; import { hostCompanyDetailsSchema, REQUIRED_DOC_TYPES, } from '../../../common/utils/validation/host/hostCompanyDetails.validation'; -import { uploadFilesToS3 } from '../../../common/utils/helper/s3Upload'; -import { parseMultipartFormData } from '../../../common/utils/validation/host'; +import AWS from 'aws-sdk'; +import Busboy from 'busboy'; +import crypto from 'crypto'; +import config from '@/config/config'; -const prismaService = new PrismaService(); -const minglarService = new MinglarService(prismaService); +const prisma = new PrismaService(); +const hostService = new HostService(prisma); -export const handler = safeHandler(async ( - event: APIGatewayProxyEvent, - context?: Context -): Promise => { +const s3 = new AWS.S3({ + region: config.aws.region, +}); - // โœ… 1. Extract & verify token - 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 verifyHostToken(token); - - // โœ… 2. Check Content-Type and parse accordingly - const contentType = event.headers['content-type'] || event.headers['Content-Type'] || ''; - let parsedCompany: any; - let documentsWithS3Urls: Array<{ documentTypeXid: number; documentName: string; filePath: string }> = []; - - if (contentType.includes('multipart/form-data')) { - // โœ… Parse multipart/form-data - // API Gateway sets isBase64Encoded to true for binary media types - const isBase64Encoded = (event as any).isBase64Encoded === true; - const formData = parseMultipartFormData(event.body, contentType, isBase64Encoded); - - // โœ… Parse companyDetails from form field (should be JSON string) - const companyDetailsJson = formData.fields['companyDetails']; - if (!companyDetailsJson) { - throw new ApiError(400, 'Company details are required in form data'); +export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise => { + try { + // โœ… 1. Verify Token + // Extract token from headers + 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.'); } - try { - parsedCompany = JSON.parse(companyDetailsJson); - } catch { - throw new ApiError(400, 'Invalid JSON in companyDetails field'); - } + // Authenticate user using the shared authForHost function + const userInfo = await verifyHostToken(token); - // โœ… Validate company details - const companyValidation = hostCompanyDetailsSchema.safeParse(parsedCompany); - if (!companyValidation.success) { - const errorMessages = companyValidation.error.issues.map(e => e.message).join(', '); - throw new ApiError(400, `Validation failed: ${errorMessages}`); - } - parsedCompany = companyValidation.data; + // โœ… 2. Ensure content-type is multipart/form-data + const contentType = event.headers['content-type'] || event.headers['Content-Type']; + if (!contentType?.startsWith('multipart/form-data')) + throw new ApiError(400, 'Content-Type must be multipart/form-data.'); - // โœ… Process uploaded files - if (formData.files.length === 0) { - throw new ApiError(400, 'At least one document file is required'); - } + if (!event.isBase64Encoded) + throw new ApiError(400, 'Event body must be base64 encoded for multipart uploads.'); - // โœ… Parse documents metadata (JSON array) - const documentsJson = formData.fields['documents']; - if (!documentsJson) { - throw new ApiError(400, 'Documents metadata is required in form data'); - } + const bodyBuffer = Buffer.from(event.body as string, 'base64'); + const fields: Record = {}; + const files: Array<{ buffer: Buffer; mimeType: string; fileName: string; fieldName: string }> = []; - let documentsMetadata: Array<{ documentTypeXid: number; documentName: string; fieldName: string }>; - try { - documentsMetadata = JSON.parse(documentsJson); - } catch { - throw new ApiError(400, 'Invalid JSON in documents field'); - } + // โœ… 3. Parse multipart data using Busboy + await new Promise((resolve, reject) => { + const bb = Busboy({ headers: { 'content-type': contentType } }); - if (!Array.isArray(documentsMetadata) || documentsMetadata.length === 0) { - throw new ApiError(400, 'Documents must be a non-empty array'); - } + bb.on('file', (fieldname, file, info) => { + const { filename, mimeType } = info; + const chunks: Buffer[] = []; + let totalSize = 0; + const MAX_SIZE = 5 * 1024 * 1024; // 5 MB - // โœ… Map files to document structure - const documentMetadata: Array<{ documentTypeXid: number; documentName: string; file: typeof formData.files[0] }> = []; + file.on('data', (chunk) => { + totalSize += chunk.length; + if (totalSize > MAX_SIZE) { + file.resume(); + return reject(new ApiError(400, `File ${filename} exceeds 5MB limit.`)); + } + chunks.push(chunk); + }); - for (const docMeta of documentsMetadata) { - const file = formData.files.find((f) => f.fieldName === docMeta.fieldName); - if (!file) { - throw new ApiError(400, `File not found for field: ${docMeta.fieldName}`); - } - - documentMetadata.push({ - documentTypeXid: docMeta.documentTypeXid, - documentName: docMeta.documentName, - file, + file.on('end', () => { + files.push({ + buffer: Buffer.concat(chunks), + mimeType, + fileName: filename, + fieldName: fieldname, + }); + }); }); + + bb.on('field', (fieldname, val) => { + try { + fields[fieldname] = JSON.parse(val); + } catch { + fields[fieldname] = val; + } + }); + + bb.on('close', resolve); + bb.on('error', reject); + bb.end(bodyBuffer); + }); + + // โœ… 4. Validate fields + if (!fields.companyDetails) throw new ApiError(400, 'Missing companyDetails field.'); + if (!fields.documents) throw new ApiError(400, 'Missing documents field.'); + if (files.length === 0) throw new ApiError(400, 'At least one document file is required.'); + + // โœ… Parse & validate JSON inputs + let companyDetails; + try { + companyDetails = typeof fields.companyDetails === 'string' ? JSON.parse(fields.companyDetails) : fields.companyDetails; + } catch { + throw new ApiError(400, 'Invalid JSON in companyDetails.'); } - // โœ… Ensure all required documents exist - const uploadedDocTypes = documentMetadata.map((doc) => doc.documentTypeXid); + const companyValidation = hostCompanyDetailsSchema.safeParse(companyDetails); + if (!companyValidation.success) { + const message = companyValidation.error.issues.map((e) => e.message).join(', '); + throw new ApiError(400, `Validation failed: ${message}`); + } + const parsedCompany = companyValidation.data; + + let documentsMetadata; + try { + documentsMetadata = typeof fields.documents === 'string' ? JSON.parse(fields.documents) : fields.documents; + } catch { + throw new ApiError(400, 'Invalid JSON in documents.'); + } + + if (!Array.isArray(documentsMetadata) || documentsMetadata.length === 0) + throw new ApiError(400, 'Documents must be a non-empty array.'); + + // โœ… 5. Map uploaded files to document metadata + const documentMetadata = documentsMetadata.map((doc: any) => { + const file = files.find((f) => f.fieldName === doc.fieldName); + if (!file) throw new ApiError(400, `File not found for field: ${doc.fieldName}`); + return { ...doc, file }; + }); + + // โœ… 6. Ensure all required document types exist + const uploadedDocTypes = documentMetadata.map((d) => d.documentTypeXid); const missingDocs = Object.entries(REQUIRED_DOC_TYPES) .filter(([_, typeId]) => !uploadedDocTypes.includes(typeId)) .map(([name]) => name); - - if (missingDocs.length > 0) { + if (missingDocs.length > 0) throw new ApiError(400, `Missing mandatory documents: ${missingDocs.join(', ')}`); - } - // โœ… Upload files to S3 - const filesToUpload = documentMetadata.map((doc) => ({ - fileData: doc.file.data.toString('base64'), - fileName: doc.file.fileName, - contentType: doc.file.contentType, - })); + // โœ… 7. Upload to S3 + const uploadedDocs: Array<{ documentTypeXid: number; documentName: string; filePath: string }> = []; + for (const doc of documentMetadata) { + const uniqueKey = `${userInfo.id}_${crypto.randomUUID()}_${doc.file.fileName}`; + const s3Key = `Documents/Host/${uniqueKey}`; + await s3 + .upload({ + Bucket: config.aws.bucketName, + Key: s3Key, + Body: doc.file.buffer, + ContentType: doc.file.mimeType, + ACL: 'private', + }) + .promise(); - const s3Urls = await uploadFilesToS3(filesToUpload, `host-documents/${userInfo.id}`); - - // โœ… Map S3 URLs to documents - documentsWithS3Urls = documentMetadata.map((doc, index) => ({ - documentTypeXid: doc.documentTypeXid, - documentName: doc.documentName, - filePath: s3Urls[index], - })); - } else { - // โœ… Fallback to JSON parsing (for backward compatibility) - let body: { companyDetails?: unknown; documents?: unknown[] }; - try { - body = event.body ? JSON.parse(event.body) : {}; - } catch { - throw new ApiError(400, 'Invalid JSON in request body'); - } - - const { companyDetails, documents } = body; - - if (!companyDetails) { - throw new ApiError(400, 'Company details are required'); - } - - // โœ… Validate company details - const companyValidation = hostCompanyDetailsSchema.safeParse(companyDetails); - if (!companyValidation.success) { - const errorMessages = companyValidation.error.issues.map(e => e.message).join(', '); - throw new ApiError(400, `Validation failed: ${errorMessages}`); - } - parsedCompany = companyValidation.data; - - // For JSON, we still expect base64 encoded files in documents array - // This maintains backward compatibility - if (documents && Array.isArray(documents) && documents.length > 0) { - const filesToUpload = documents.map((doc: any) => ({ - fileData: doc.fileData, - fileName: doc.documentName, - contentType: doc.contentType || 'application/pdf', - })); - - const s3Urls = await uploadFilesToS3(filesToUpload, `host-documents/${userInfo.id}`); - - documentsWithS3Urls = documents.map((doc: any, index: number) => ({ + uploadedDocs.push({ documentTypeXid: doc.documentTypeXid, documentName: doc.documentName, - filePath: s3Urls[index], - })); + filePath: `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${s3Key}`, + }); } + + // โœ… 8. Save company details + documents in DB via MinglarService + const createdHost = await hostService.addCompanyDetails(parsedCompany, uploadedDocs); + if (!createdHost) throw new ApiError(400, 'Failed to add company details.'); + + // โœ… 9. Success response + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + body: JSON.stringify({ + success: true, + message: 'Company details and documents uploaded successfully.', + data: createdHost, + }), + }; + } catch (error: any) { + console.error('โŒ Error in addCompanyDetails:', error); + throw error; } - - // โœ… 7. Pass validated data to service - const createdHost = await minglarService.addCompanyDetails(parsedCompany, documentsWithS3Urls); - - if (!createdHost) { - throw new ApiError(400, 'Failed to add company details'); - } - - // โœ… 6. Success response - return { - statusCode: 200, - headers: { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*', - }, - body: JSON.stringify({ - success: true, - message: 'Company details and documents uploaded successfully', - }), - }; });