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
This commit is contained in:
@@ -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")
|
||||
|
||||
122
prisma/seed.ts
122
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,
|
||||
});
|
||||
|
||||
@@ -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<typeof userPersonalInfoSchema>;
|
||||
@@ -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.');
|
||||
|
||||
13
src/modules/user/dto/user.dto.ts
Normal file
13
src/modules/user/dto/user.dto.ts
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
122
src/modules/user/handlers/authentication/registration.ts
Normal file
122
src/modules/user/handlers/authentication/registration.ts
Normal file
@@ -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<APIGatewayProxyResult> => {
|
||||
// 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: {},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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<APIGatewayProxyResult> => {
|
||||
// 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',
|
||||
}),
|
||||
};
|
||||
});
|
||||
27
src/modules/user/services/user.service.ts
Normal file
27
src/modules/user/services/user.service.ts
Normal file
@@ -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');
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user