From f20e4191ee93b7b41e4c4271b9d7f6a2623818eb Mon Sep 17 00:00:00 2001 From: Mayank Mishra Date: Fri, 16 Jan 2026 17:50:30 +0530 Subject: [PATCH] added gender name column and interest color and image column added activity seeder data till gamecraft made register and add personal info api for user mobile endpoints lambda and service --- prisma/schema.prisma | 5 +- prisma/seed.ts | 122 +++++++++++++----- .../user/addPersonalInfo.validation.ts | 23 ++++ .../minglaradmin/handlers/registration.ts | 2 - src/modules/user/dto/user.dto.ts | 13 ++ .../handlers/authentication/registration.ts | 122 ++++++++++++++++++ .../authentication/submitPersonalInfo.ts | 70 ++++++++++ src/modules/user/services/user.service.ts | 27 ++++ 8 files changed, 351 insertions(+), 33 deletions(-) create mode 100644 src/common/utils/validation/user/addPersonalInfo.validation.ts create mode 100644 src/modules/user/dto/user.dto.ts create mode 100644 src/modules/user/handlers/authentication/registration.ts create mode 100644 src/modules/user/handlers/authentication/submitPersonalInfo.ts create mode 100644 src/modules/user/services/user.service.ts diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c8191e6..3dbb2b8 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -16,8 +16,9 @@ model User { lastName String? @map("last_name") @db.VarChar(50) roleXid Int? @map("role_xid") dateOfBirth DateTime? @map("date_of_birth") + genderName String? @map("gender_name") @db.VarChar(20) role Roles? @relation(fields: [roleXid], references: [id], onDelete: Restrict) - emailAddress String @unique @map("email_address") @db.VarChar(150) + 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 userPassword String? @map("user_password") @db.VarChar(255) // hashed passwords @@ -355,6 +356,8 @@ model BankBranches { model Interests { id Int @id @default(autoincrement()) interestName String @unique @map("interest_name") @db.VarChar(50) + interestColor String @map("interest_color") @db.VarChar(20) + interestImage String @map("interest_image") @db.VarChar(500) displayOrder Int @map("display_order") isActive Boolean @default(true) @map("is_active") createdAt DateTime @default(now()) @map("created_at") diff --git a/prisma/seed.ts b/prisma/seed.ts index aa81aec..0958806 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -163,56 +163,118 @@ async function main() { // ✅ Interests + Activity Types const chillandzen = await prisma.interests.upsert({ - where: { interestName: 'Chill and Zen' }, + where: { interestName: 'Chill & Zen' }, update: {}, - create: { interestName: 'Chill and Zen', displayOrder: 1 }, + create: { interestName: 'Chill & Zen', displayOrder: 1 }, + }); + const artsyfeels = await prisma.interests.upsert({ + where: { interestName: 'Artsy Feels' }, + update: {}, + create: { interestName: 'Artsy Feels', displayOrder: 2 }, }); const sweatmode = await prisma.interests.upsert({ - where: { interestName: 'Sweat Mode On' }, + where: { interestName: 'Sweat Mode' }, update: {}, - create: { interestName: 'Sweat Mode On', displayOrder: 2 }, + create: { interestName: 'Sweat Mode', displayOrder: 3 }, }); - const trackracer = await prisma.interests.upsert({ - where: { interestName: 'Track Racer' }, + const gamecraft = await prisma.interests.upsert({ + where: { interestName: 'Gamecraft' }, update: {}, - create: { interestName: 'Track Racer', displayOrder: 3 }, + create: { interestName: 'Gamecraft', displayOrder: 4 }, }); - const circuitracer = await prisma.interests.upsert({ - where: { interestName: 'Circuit Racer' }, + const wildandfree = await prisma.interests.upsert({ + where: { interestName: 'Wild & Free' }, update: {}, - create: { interestName: 'Circuit Racer', displayOrder: 4 }, + create: { interestName: 'Wild & Free', displayOrder: 5 }, }); - const thermalGliding = await prisma.interests.upsert({ - where: { interestName: 'Thermal Gliding' }, + const splashlife = await prisma.interests.upsert({ + where: { interestName: 'Splash Life' }, update: {}, - create: { interestName: 'Thermal Gliding', displayOrder: 5 }, + create: { interestName: 'Splash Life', displayOrder: 6 }, }); - const partycentral = await prisma.interests.upsert({ - where: { interestName: 'Party Central' }, + const cultureandheritage = await prisma.interests.upsert({ + where: { interestName: 'Culture & Heritage' }, update: {}, - create: { interestName: 'Party Central', displayOrder: 6 }, + create: { interestName: 'Culture & Heritage', displayOrder: 7 }, }); - const aqua = await prisma.interests.upsert({ - where: { interestName: 'Aqua' }, + const Gastronomé = await prisma.interests.upsert({ + where: { interestName: 'Gastronomé' }, update: {}, - create: { interestName: 'Aqua', displayOrder: 7 }, + create: { interestName: 'Gastronomé', displayOrder: 8 }, }); - const foodie = await prisma.interests.upsert({ - where: { interestName: 'Foodie' }, + const sportsarena = await prisma.interests.upsert({ + where: { interestName: 'Sports Arena' }, update: {}, - create: { interestName: 'Foodie', displayOrder: 8 }, + create: { interestName: 'Sports Arena', displayOrder: 9 }, + }); + const nightlifeevents = await prisma.interests.upsert({ + where: { interestName: 'Nightlife & Events' }, + update: {}, + create: { interestName: 'Nightlife & Events', displayOrder: 10 }, + }); + const furfam = await prisma.interests.upsert({ + where: { interestName: 'Fur Fam' }, + update: {}, + create: { interestName: 'Fur Fam', displayOrder: 11 }, + }); + const dogoodfeelgood = await prisma.interests.upsert({ + where: { interestName: 'Do Good, Feel Good' }, + update: {}, + create: { interestName: 'Do Good, Feel Good', displayOrder: 12 }, }); await prisma.activityTypes.createMany({ data: [ - { interestXid: aqua.id, activityTypeName: 'Scuba-Diving', energyLevelXid: highEnergy.id }, - { interestXid: sweatmode.id, activityTypeName: 'Cloudboarding', energyLevelXid: highEnergy.id }, - { interestXid: partycentral.id, activityTypeName: 'Soaring Glider', energyLevelXid: highEnergy.id }, - { interestXid: sweatmode.id, activityTypeName: 'Speedway Racer', energyLevelXid: highEnergy.id }, - { interestXid: aqua.id, activityTypeName: 'Aerial Surfing', energyLevelXid: highEnergy.id }, - { interestXid: foodie.id, activityTypeName: 'Wine Tasting', energyLevelXid: lowEnergy.id }, - { interestXid: trackracer.id, activityTypeName: 'Track Racer', energyLevelXid: highEnergy.id }, - { interestXid: thermalGliding.id, activityTypeName: 'Thermal Gliding', energyLevelXid: mediumEnergy.id }, + // --------Chill & Zen-------- + { interestXid: chillandzen.id, activityTypeName: 'Yoga', energyLevelXid: lowEnergy.id }, + { interestXid: chillandzen.id, activityTypeName: 'Meditation', energyLevelXid: lowEnergy.id }, + { interestXid: chillandzen.id, activityTypeName: 'Spa Retreat', energyLevelXid: lowEnergy.id }, + { interestXid: chillandzen.id, activityTypeName: 'Bath Experience', energyLevelXid: lowEnergy.id }, + { interestXid: chillandzen.id, activityTypeName: 'Stargazing', energyLevelXid: lowEnergy.id }, + { interestXid: chillandzen.id, activityTypeName: 'Nail Spa/Art', energyLevelXid: lowEnergy.id }, + // --------Artsy Feels-------- + { interestXid: artsyfeels.id, activityTypeName: 'Canvas Painting', energyLevelXid: lowEnergy.id }, + { interestXid: artsyfeels.id, activityTypeName: 'Textile Painting', energyLevelXid: lowEnergy.id }, + { interestXid: artsyfeels.id, activityTypeName: 'Music and Instruments', energyLevelXid: mediumEnergy.id }, + { interestXid: artsyfeels.id, activityTypeName: 'Pottery', energyLevelXid: mediumEnergy.id }, + { interestXid: artsyfeels.id, activityTypeName: 'Knitting / Crocheting', energyLevelXid: lowEnergy.id }, + { interestXid: artsyfeels.id, activityTypeName: 'Lipstick Customisation', energyLevelXid: lowEnergy.id }, + { interestXid: artsyfeels.id, activityTypeName: 'Tufting', energyLevelXid: mediumEnergy.id }, + { interestXid: artsyfeels.id, activityTypeName: 'Acting', energyLevelXid: mediumEnergy.id }, + { interestXid: artsyfeels.id, activityTypeName: 'Art', energyLevelXid: lowEnergy.id }, + { interestXid: artsyfeels.id, activityTypeName: 'Tattoos', energyLevelXid: lowEnergy.id }, + // --------Sweat Mode-------- + { interestXid: sweatmode.id, activityTypeName: 'Dance', energyLevelXid: highEnergy.id }, + { interestXid: sweatmode.id, activityTypeName: 'Kickboxing', energyLevelXid: highEnergy.id }, + { interestXid: sweatmode.id, activityTypeName: 'Gym with Personal Trainer', energyLevelXid: highEnergy.id }, + { interestXid: sweatmode.id, activityTypeName: 'Aerobic', energyLevelXid: highEnergy.id }, + { interestXid: sweatmode.id, activityTypeName: 'Skating', energyLevelXid: mediumEnergy.id }, + { interestXid: sweatmode.id, activityTypeName: 'Martial Arts', energyLevelXid: highEnergy.id }, + { interestXid: sweatmode.id, activityTypeName: 'Trampoline Park', energyLevelXid: highEnergy.id }, + { interestXid: sweatmode.id, activityTypeName: 'Wall Climbing', energyLevelXid: highEnergy.id }, + { interestXid: sweatmode.id, activityTypeName: 'Rope Course', energyLevelXid: highEnergy.id }, + { interestXid: sweatmode.id, activityTypeName: 'Running', energyLevelXid: highEnergy.id }, + //---------Game Craft--------- + { interestXid: gamecraft.id, activityTypeName: 'Billiard / Snooker', energyLevelXid: lowEnergy.id }, + { interestXid: gamecraft.id, activityTypeName: 'Squash', energyLevelXid: highEnergy.id }, + { interestXid: gamecraft.id, activityTypeName: 'Rage Room', energyLevelXid: highEnergy.id }, + { interestXid: gamecraft.id, activityTypeName: 'E-Sports', energyLevelXid: lowEnergy.id }, + { interestXid: gamecraft.id, activityTypeName: 'Table Tennis', energyLevelXid: mediumEnergy.id }, + { interestXid: gamecraft.id, activityTypeName: 'VR Games', energyLevelXid: mediumEnergy.id }, + { interestXid: gamecraft.id, activityTypeName: 'Escape Room', energyLevelXid: mediumEnergy.id }, + { interestXid: gamecraft.id, activityTypeName: 'Paintball', energyLevelXid: highEnergy.id }, + { interestXid: gamecraft.id, activityTypeName: 'Bowling', energyLevelXid: mediumEnergy.id }, + { interestXid: gamecraft.id, activityTypeName: 'Shooting Range', energyLevelXid: lowEnergy.id }, + { interestXid: gamecraft.id, activityTypeName: 'Bumper Cars', energyLevelXid: lowEnergy.id }, + { interestXid: gamecraft.id, activityTypeName: 'Ice Skating', energyLevelXid: mediumEnergy.id }, + { interestXid: gamecraft.id, activityTypeName: 'Snow City', energyLevelXid: lowEnergy.id }, + { interestXid: gamecraft.id, activityTypeName: 'Pole Artistry', energyLevelXid: highEnergy.id }, + { interestXid: gamecraft.id, activityTypeName: 'Hula Hoop', energyLevelXid: mediumEnergy.id }, + { interestXid: gamecraft.id, activityTypeName: 'Foosball', energyLevelXid: lowEnergy.id }, + { interestXid: gamecraft.id, activityTypeName: 'Go Karting', energyLevelXid: mediumEnergy.id }, + { interestXid: gamecraft.id, activityTypeName: 'Laser Maze', energyLevelXid: mediumEnergy.id }, + //---------Wild & Free--------- + ], skipDuplicates: true, }); diff --git a/src/common/utils/validation/user/addPersonalInfo.validation.ts b/src/common/utils/validation/user/addPersonalInfo.validation.ts new file mode 100644 index 0000000..b2475b6 --- /dev/null +++ b/src/common/utils/validation/user/addPersonalInfo.validation.ts @@ -0,0 +1,23 @@ +// validations/hostBankDetails.validation.ts +import { z } from "zod"; + +export const userPersonalInfoSchema = z.object({ + firstName: z + .string() + .nonempty("First name is required"), + + lastName: z + .string() + .optional(), + + genderName: z + .string() + .nonempty("Gender is required"), + + dateOfBirth: z + .string() + .nonempty("Date of birth is required"), + +}); + +export type UserPersonalInfoSchema = z.infer; diff --git a/src/modules/minglaradmin/handlers/registration.ts b/src/modules/minglaradmin/handlers/registration.ts index 8655167..d404488 100644 --- a/src/modules/minglaradmin/handlers/registration.ts +++ b/src/modules/minglaradmin/handlers/registration.ts @@ -27,7 +27,6 @@ export const handler = safeHandler(async ( if (!email) { throw new ApiError(400, 'Email is required'); } - console.log(email, " -: Email") const emailToLowerCase = email.toLowerCase() @@ -35,7 +34,6 @@ export const handler = safeHandler(async ( where: { emailAddress: emailToLowerCase, isActive: true, userStatus: USER_STATUS.INVITED }, select: { emailAddress: true, id: true, userPassword: true, roleXid: true }, }); - console.log(user, "sljdfjdf") if (!user) { throw new ApiError(403, 'You are not allowed to register directly. Please contact minglar admin.'); diff --git a/src/modules/user/dto/user.dto.ts b/src/modules/user/dto/user.dto.ts new file mode 100644 index 0000000..74776e0 --- /dev/null +++ b/src/modules/user/dto/user.dto.ts @@ -0,0 +1,13 @@ +export class AddPersonalInfoDTO { + firstName: string; + lastName?: string; + genderName: string; + dateOfBirth: string; + + constructor(firstName: string, genderName: string, dateOfBirth: string, lastName?: string) { + this.firstName = firstName; + this.lastName = lastName; + this.genderName = genderName; + this.dateOfBirth = dateOfBirth; + } +} \ No newline at end of file diff --git a/src/modules/user/handlers/authentication/registration.ts b/src/modules/user/handlers/authentication/registration.ts new file mode 100644 index 0000000..0174132 --- /dev/null +++ b/src/modules/user/handlers/authentication/registration.ts @@ -0,0 +1,122 @@ +import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda'; +import * as bcrypt from 'bcryptjs'; +import { prismaClient } from '../../../../common/database/prisma.lambda.service'; +import { ROLE } from '../../../../common/utils/constants/common.constant'; +import { safeHandler } from '../../../../common/utils/handlers/safeHandler'; +import ApiError from '../../../../common/utils/helper/ApiError'; +import { encryptUserId } from '../../../../common/utils/helper/CodeGenerator'; +import { OtpGeneratorSixDigit } from '../../../../common/utils/helper/OtpGenerator'; +export async function generateUserRefNumber(tx: any) { + const lastrecord = await tx.user.findFirst({ + orderBy: { + id: 'desc', + }, + select: { + id: true, + }, + }); + + const nextId = lastrecord ? lastrecord.id + 1 : 1; + + return `USR-${String(nextId).padStart(6, '0')}`;; +} + +export const handler = safeHandler(async ( + event: APIGatewayProxyEvent, + context?: Context +): Promise => { + // Parse request body + let body: { mobileNumber?: string }; + + try { + body = event.body ? JSON.parse(event.body) : {}; + } catch (error) { + throw new ApiError(400, 'Invalid JSON in request body'); + } + + const { mobileNumber } = body; + + if (!mobileNumber) { + throw new ApiError(400, 'Mobile number is required'); + } + + // Use a single transaction for user creation/lookup and OTP storage + 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 }, + }); + + if (user && user.userPasscode) { + throw new ApiError(409, 'User is already registered. Please login.'); + } + + let newUserLocal; + + const referenceNumber = await generateUserRefNumber(tx); + + if (user && !user.userPasscode) { + // reuse existing invited user record + newUserLocal = user; + } else { + // create new user record within the transaction + newUserLocal = await tx.user.create({ + data: { + mobileNumber: mobileNumber, + role: { + connect: { + id: ROLE.USER, // 👈 Role ID + }, + }, + userRefNumber: referenceNumber + }, + }); + } + + // Generate OTP (6-digit) and store within the same transaction + const otp = OtpGeneratorSixDigit.generateOtp(); + const hashedOtp = await bcrypt.hash(otp, 10); + const expiry = new Date(Date.now() + 5 * 60 * 1000); // 5 minutes + + // delete old active OTPs for this user/purpose + await tx.userOtp.deleteMany({ + where: { userXid: Number(newUserLocal.id), otpType: 'Register', isActive: true }, + }); + + await tx.userOtp.create({ + data: { + userXid: Number(newUserLocal.id), + otpType: 'Register', + otpCode: hashedOtp, + expiresOn: expiry, + isVerified: false, + isActive: true, + }, + }); + + const encryptedId = encryptUserId(String(newUserLocal.id)); + + return { newUser: newUserLocal, otp, encryptedId }; + }); + + if (!transactionResult || !transactionResult.otp) { + throw new ApiError(500, 'Failed to generate OTP'); + } + + // Send OTP email outside the DB transaction + // await sendOtpEmailForHost(transactionResult.newUser.emailAddress, transactionResult.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/user/handlers/authentication/submitPersonalInfo.ts b/src/modules/user/handlers/authentication/submitPersonalInfo.ts new file mode 100644 index 0000000..c1cbb77 --- /dev/null +++ b/src/modules/user/handlers/authentication/submitPersonalInfo.ts @@ -0,0 +1,70 @@ +import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda'; +import { prismaClient } from '../../../../common/database/prisma.lambda.service'; +import { verifyUserToken } from '../../../../common/middlewares/jwt/authForUser'; +import { safeHandler } from '../../../../common/utils/handlers/safeHandler'; +import ApiError from '../../../../common/utils/helper/ApiError'; +import { userPersonalInfoSchema } from '../../../../common/utils/validation/user/addPersonalInfo.validation'; +import { UserService } from '../../services/user.service'; + +const userService = new UserService(prismaClient); + +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.'); + } + + // Authenticate user using the shared authForHost function + const userInfo = await verifyUserToken(token); + const userId = userInfo.id; + + if (Number.isNaN(userId)) { + throw new ApiError(400, 'User id must be a number'); + } + + const user = await userService.getUserById(userId); + if (!user) { + throw new ApiError(404, 'User not found'); + } + + // Parse request body + let body: { firstName?: string; lastName?: string; genderName: string; dateOfBirth?: string; }; + + try { + body = event.body ? JSON.parse(event.body) : {}; + } catch (error) { + throw new ApiError(400, 'Invalid JSON in request body'); + } + + // ✅ Validate payload using Zod + const validationResult = userPersonalInfoSchema.safeParse({ + ...(body as object), + }); + + if (!validationResult.success) { + const errorMessages = validationResult.error.issues.map(e => e.message).join(', '); + throw new ApiError(400, `Validation failed: ${errorMessages}`); + } + + const validatedData = validationResult.data; + + await userService.addPersonalInfo({ + ...validatedData + }); + + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + body: JSON.stringify({ + success: true, + message: 'Personal Info added successfully', + }), + }; +}); \ No newline at end of file diff --git a/src/modules/user/services/user.service.ts b/src/modules/user/services/user.service.ts new file mode 100644 index 0000000..7857dfd --- /dev/null +++ b/src/modules/user/services/user.service.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaClient, User } from '@prisma/client'; +import { AddPersonalInfoDTO } from '../dto/user.dto'; +import ApiError from '@/common/utils/helper/ApiError'; +@Injectable() +export class UserService { + constructor(private prisma: PrismaClient) { } + + async getUserById(userId: number) { + return this.prisma.user.findUnique({ + where: { id: userId, isActive: true }, + }); + } + + async addPersonalInfo(data: AddPersonalInfoDTO){ + return await this.prisma.$transaction(async (tx) => { + + const addPersonalInfo = await tx.user.create({ + data, + }); + + if (!addPersonalInfo) { + throw new ApiError(400, 'Failed to add personal info'); + } + }) +} +} \ No newline at end of file