From 112fdab04074591497d7ca8d661c8bf3e5882672 Mon Sep 17 00:00:00 2001 From: Mayank Mishra Date: Fri, 23 Jan 2026 17:56:46 +0530 Subject: [PATCH] Made mobile register and verify otp and submit personal info apis and added interest type images in the seeder --- prisma/schema.prisma | 2 +- prisma/seed.ts | 24 +++---- serverless.yml | 1 + serverless/functions/user.yml | 47 +++++++++++++ src/modules/host/services/host.service.ts | 2 +- .../handlers/authentication/registration.ts | 6 +- .../authentication/verifyOtpForUser.ts | 51 ++++++++++++++ src/modules/user/services/user.service.ts | 67 +++++++++++++++++-- 8 files changed, 174 insertions(+), 26 deletions(-) create mode 100644 serverless/functions/user.yml create mode 100644 src/modules/user/handlers/authentication/verifyOtpForUser.ts diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d8635d5..00f0296 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -20,7 +20,7 @@ model User { role Roles? @relation(fields: [roleXid], references: [id], onDelete: Restrict) emailAddress String? @unique @map("email_address") @db.VarChar(150) isdCode String? @map("isd_code") @db.VarChar(6) // +91, +1, +971 etc. - mobileNumber String? @map("mobile_number") @db.VarChar(15) // international safe limit + mobileNumber String? @unique @map("mobile_number") @db.VarChar(15) // international safe limit userPassword String? @map("user_password") @db.VarChar(255) // hashed passwords userPasscode String? @map("user_passcode") @db.VarChar(10) // 4–6 digit passcode profileImage String? @map("profile_image") @db.VarChar(500) // S3 key or URL diff --git a/prisma/seed.ts b/prisma/seed.ts index 1eb3644..f243848 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -165,62 +165,62 @@ async function main() { const chillandzen = await prisma.interests.upsert({ where: { interestName: 'Chill & Zen' }, update: {}, - create: { interestName: 'Chill & Zen', displayOrder: 1, interestColor: 'Blue', interestImage: 'https://tinyurl.com/c2d9vyat' }, + create: { interestName: 'Chill & Zen', displayOrder: 1, interestColor: 'Blue', interestImage: 'https://minglar-dev-bucket.s3.ap-south-1.amazonaws.com/StaticImages/InterestTypes/Chill+and+Zen.png' }, }); const artsyfeels = await prisma.interests.upsert({ where: { interestName: 'Artsy Feels' }, update: {}, - create: { interestName: 'Artsy Feels', displayOrder: 2, interestColor: 'Blue', interestImage: 'https://tinyurl.com/c2d9vyat' }, + create: { interestName: 'Artsy Feels', displayOrder: 2, interestColor: 'Blue', interestImage: 'https://minglar-dev-bucket.s3.ap-south-1.amazonaws.com/StaticImages/InterestTypes/Artsy+Feels.png' }, }); const sweatmode = await prisma.interests.upsert({ where: { interestName: 'Sweat Mode' }, update: {}, - create: { interestName: 'Sweat Mode', displayOrder: 3, interestColor: 'Blue', interestImage: 'https://tinyurl.com/c2d9vyat' }, + create: { interestName: 'Sweat Mode', displayOrder: 3, interestColor: 'Blue', interestImage: 'https://minglar-dev-bucket.s3.ap-south-1.amazonaws.com/StaticImages/InterestTypes/Sweat+Mode.png' }, }); const gamecraft = await prisma.interests.upsert({ where: { interestName: 'Gamecraft' }, update: {}, - create: { interestName: 'Gamecraft', displayOrder: 4, interestColor: 'Blue', interestImage: 'https://tinyurl.com/c2d9vyat' }, + create: { interestName: 'Gamecraft', displayOrder: 4, interestColor: 'Blue', interestImage: 'https://minglar-dev-bucket.s3.ap-south-1.amazonaws.com/StaticImages/InterestTypes/Gamecraft.png' }, }); const wildandfree = await prisma.interests.upsert({ where: { interestName: 'Wild & Free' }, update: {}, - create: { interestName: 'Wild & Free', displayOrder: 5, interestColor: 'Blue', interestImage: 'https://tinyurl.com/c2d9vyat' }, + create: { interestName: 'Wild & Free', displayOrder: 5, interestColor: 'Blue', interestImage: 'https://minglar-dev-bucket.s3.ap-south-1.amazonaws.com/StaticImages/InterestTypes/Wild+and+Free.png' }, }); const splashlife = await prisma.interests.upsert({ where: { interestName: 'Splash Life' }, update: {}, - create: { interestName: 'Splash Life', displayOrder: 6, interestColor: 'Blue', interestImage: 'https://tinyurl.com/c2d9vyat' }, + create: { interestName: 'Splash Life', displayOrder: 6, interestColor: 'Blue', interestImage: 'https://minglar-dev-bucket.s3.ap-south-1.amazonaws.com/StaticImages/InterestTypes/Splash+Life.png' }, }); const cultureandheritage = await prisma.interests.upsert({ where: { interestName: 'Culture & Heritage' }, update: {}, - create: { interestName: 'Culture & Heritage', displayOrder: 7, interestColor: 'Blue', interestImage: 'https://tinyurl.com/c2d9vyat' }, + create: { interestName: 'Culture & Heritage', displayOrder: 7, interestColor: 'Blue', interestImage: 'https://minglar-dev-bucket.s3.ap-south-1.amazonaws.com/StaticImages/InterestTypes/Cultures.jpg' }, }); const Gastronomé = await prisma.interests.upsert({ where: { interestName: 'Gastronomé' }, update: {}, - create: { interestName: 'Gastronomé', displayOrder: 8, interestColor: 'Blue', interestImage: 'https://tinyurl.com/c2d9vyat' }, + create: { interestName: 'Gastronomé', displayOrder: 8, interestColor: 'Blue', interestImage: 'https://minglar-dev-bucket.s3.ap-south-1.amazonaws.com/StaticImages/InterestTypes/Gastranome.jpg' }, }); const sportsarena = await prisma.interests.upsert({ where: { interestName: 'Sports Arena' }, update: {}, - create: { interestName: 'Sports Arena', displayOrder: 9, interestColor: 'Blue', interestImage: 'https://tinyurl.com/c2d9vyat' }, + create: { interestName: 'Sports Arena', displayOrder: 9, interestColor: 'Blue', interestImage: 'https://minglar-dev-bucket.s3.ap-south-1.amazonaws.com/StaticImages/InterestTypes/Sports+Arena.jpg' }, }); const nightlifeevents = await prisma.interests.upsert({ where: { interestName: 'Nightlife & Events' }, update: {}, - create: { interestName: 'Nightlife & Events', displayOrder: 10, interestColor: 'Blue', interestImage: 'https://tinyurl.com/c2d9vyat' }, + create: { interestName: 'Nightlife & Events', displayOrder: 10, interestColor: 'Blue', interestImage: 'https://minglar-dev-bucket.s3.ap-south-1.amazonaws.com/StaticImages/InterestTypes/Nightlife+and+Events.png' }, }); const furfam = await prisma.interests.upsert({ where: { interestName: 'Fur Fam' }, update: {}, - create: { interestName: 'Fur Fam', displayOrder: 11, interestColor: 'Blue', interestImage: 'https://tinyurl.com/c2d9vyat' }, + create: { interestName: 'Fur Fam', displayOrder: 11, interestColor: 'Blue', interestImage: 'https://minglar-dev-bucket.s3.ap-south-1.amazonaws.com/StaticImages/InterestTypes/pet+space+jpg.jpg' }, }); const dogoodfeelgood = await prisma.interests.upsert({ where: { interestName: 'Do Good, Feel Good' }, update: {}, - create: { interestName: 'Do Good, Feel Good', displayOrder: 12, interestColor: 'Blue', interestImage: 'https://tinyurl.com/c2d9vyat' }, + create: { interestName: 'Do Good, Feel Good', displayOrder: 12, interestColor: 'Blue', interestImage: 'https://minglar-dev-bucket.s3.ap-south-1.amazonaws.com/StaticImages/InterestTypes/do+good+feel+good.png' }, }); await prisma.activityTypes.createMany({ diff --git a/serverless.yml b/serverless.yml index 20e4f77..8aa9830 100644 --- a/serverless.yml +++ b/serverless.yml @@ -145,6 +145,7 @@ functions: - ${file(./serverless/functions/minglaradmin.yml)} - ${file(./serverless/functions/prepopulate.yml)} - ${file(./serverless/functions/pqq.yml)} + - ${file(./serverless/functions/user.yml)} plugins: - serverless-offline \ No newline at end of file diff --git a/serverless/functions/user.yml b/serverless/functions/user.yml new file mode 100644 index 0000000..5c75753 --- /dev/null +++ b/serverless/functions/user.yml @@ -0,0 +1,47 @@ +# Prepopulate Module Functions +# Reference data and lookup endpoints + +registerUser: + handler: src/modules/user/handlers/authentication/registration.handler + memorySize: 384 + package: + patterns: + - 'src/modules/user/**' + - ${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: /user/register + method: post + +submitPersonalInfo: + handler: src/modules/user/handlers/authentication/submitPersonalInfo.handler + memorySize: 384 + package: + patterns: + - 'src/modules/user/**' + - ${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: /user/submit-personal-info + method: post + +verifyOtpForUser: + handler: src/modules/user/handlers/authentication/verifyOtpForUser.handler + memorySize: 384 + package: + patterns: + - 'src/modules/user/**' + - ${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: /user/verify-otp + method: post diff --git a/src/modules/host/services/host.service.ts b/src/modules/host/services/host.service.ts index 1677522..e3a4690 100644 --- a/src/modules/host/services/host.service.ts +++ b/src/modules/host/services/host.service.ts @@ -296,7 +296,7 @@ export class HostService { async verifyHostOtp(email: string, otp: string): Promise { const user = await this.prisma.user.findUnique({ - where: { emailAddress: email }, + where: { emailAddress: email, isActive: true }, select: { id: true, emailAddress: true, diff --git a/src/modules/user/handlers/authentication/registration.ts b/src/modules/user/handlers/authentication/registration.ts index 0174132..710ab0d 100644 --- a/src/modules/user/handlers/authentication/registration.ts +++ b/src/modules/user/handlers/authentication/registration.ts @@ -44,13 +44,9 @@ export const handler = safeHandler(async ( const transactionResult = await prismaClient.$transaction(async (tx) => { const user = await tx.user.findFirst({ where: { mobileNumber: mobileNumber, isActive: true }, - select: { emailAddress: true, id: true, userPasscode: true, mobileNumber: true }, + select: { id: true, userPasscode: true, mobileNumber: true }, }); - if (user && user.userPasscode) { - throw new ApiError(409, 'User is already registered. Please login.'); - } - let newUserLocal; const referenceNumber = await generateUserRefNumber(tx); diff --git a/src/modules/user/handlers/authentication/verifyOtpForUser.ts b/src/modules/user/handlers/authentication/verifyOtpForUser.ts new file mode 100644 index 0000000..594422f --- /dev/null +++ b/src/modules/user/handlers/authentication/verifyOtpForUser.ts @@ -0,0 +1,51 @@ +import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda'; +import { safeHandler } from '../../../../common/utils/handlers/safeHandler'; +import { prismaClient } from '../../../../common/database/prisma.lambda.service'; +import { UserService } from '../../services/user.service'; +import ApiError from '../../../../common/utils/helper/ApiError'; +import { TokenService } from '../../../host/services/token.service'; + +const userService = new UserService(prismaClient); +const tokenService = new TokenService(prismaClient); + +export const handler = safeHandler(async ( + event: APIGatewayProxyEvent, + context?: Context +): Promise => { + // Parse request body + let body: { mobileNumber?: string; otp?: string }; + + try { + body = event.body ? JSON.parse(event.body) : {}; + } catch (error) { + throw new ApiError(400, 'Invalid JSON in request body'); + } + + const { mobileNumber, otp } = body; + + if (!mobileNumber || !otp) { + throw new ApiError(400, 'Mobile number and OTP are required'); + } + + await userService.verifyHostOtp(mobileNumber, otp); + const user = await userService.getUserByMobileNumber(mobileNumber); + const generateTokenForUser = await tokenService.generateAuthToken( + user.id + ); + + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + body: JSON.stringify({ + success: true, + message: 'OTP verified successfully', + accessToken: generateTokenForUser.access.token, + refreshToken: generateTokenForUser.refresh.token, + data: null, + }), + }; +}); + diff --git a/src/modules/user/services/user.service.ts b/src/modules/user/services/user.service.ts index 7857dfd..b16d4c3 100644 --- a/src/modules/user/services/user.service.ts +++ b/src/modules/user/services/user.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common'; import { PrismaClient, User } from '@prisma/client'; import { AddPersonalInfoDTO } from '../dto/user.dto'; import ApiError from '@/common/utils/helper/ApiError'; +import * as bcrypt from 'bcryptjs'; @Injectable() export class UserService { constructor(private prisma: PrismaClient) { } @@ -12,16 +13,68 @@ export class UserService { }); } - async addPersonalInfo(data: AddPersonalInfoDTO){ + async addPersonalInfo(data: AddPersonalInfoDTO) { return await this.prisma.$transaction(async (tx) => { - const addPersonalInfo = await tx.user.create({ + const addPersonalInfo = await tx.user.create({ data, - }); + }); - if (!addPersonalInfo) { + if (!addPersonalInfo) { throw new ApiError(400, 'Failed to add personal info'); - } - }) -} + } + }) + } + + async getUserByMobileNumber(mobileNumber: string): Promise { + return this.prisma.user.findFirst({ + where: { mobileNumber: mobileNumber, isActive: true }, + }); + } + + async verifyHostOtp(mobileNumber: string, otp: string): Promise { + const user = await this.prisma.user.findFirst({ + where: { mobileNumber: mobileNumber, isActive: true }, + select: { + id: true, + mobileNumber: 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; + } } \ No newline at end of file