made register and login apis for host

This commit is contained in:
2025-11-12 16:03:57 +05:30
parent 5399c8b987
commit c0e58fe1ce
24 changed files with 2052 additions and 163 deletions

130
package-lock.json generated
View File

@@ -21,11 +21,14 @@
"@types/http-status": "^1.1.2",
"ajv": "8.12.0",
"aws-lambda": "^1.0.7",
"bcrypt": "^6.0.0",
"bcryptjs": "^2.4.3",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"date-fns": "^4.1.0",
"helmet": "^7.1.0",
"http-status": "^2.1.0",
"moment": "^2.30.1",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
@@ -33,7 +36,8 @@
"rxjs": "^7.8.1",
"serverless": "4.17.0",
"swagger-ui-express": "^5.0.0",
"yup": "^1.7.1"
"yup": "^1.7.1",
"zod": "^4.1.12"
},
"devDependencies": {
"@nestjs/cli": "^10.3.0",
@@ -61,6 +65,7 @@
"ts-loader": "^9.5.1",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"tsx": "^4.20.6",
"typescript": "^5.3.3"
}
},
@@ -813,7 +818,6 @@
"os": [
"aix"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -830,7 +834,6 @@
"os": [
"android"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -847,7 +850,6 @@
"os": [
"android"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -864,7 +866,6 @@
"os": [
"android"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -881,7 +882,6 @@
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -898,7 +898,6 @@
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -915,7 +914,6 @@
"os": [
"freebsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -932,7 +930,6 @@
"os": [
"freebsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -949,7 +946,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -966,7 +962,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -983,7 +978,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -1000,7 +994,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -1017,7 +1010,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -1034,7 +1026,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -1051,7 +1042,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -1068,7 +1058,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -1085,7 +1074,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -1102,7 +1090,6 @@
"os": [
"netbsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -1119,7 +1106,6 @@
"os": [
"netbsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -1136,7 +1122,6 @@
"os": [
"openbsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -1153,7 +1138,6 @@
"os": [
"openbsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -1170,7 +1154,6 @@
"os": [
"openharmony"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -1187,7 +1170,6 @@
"os": [
"sunos"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -1204,7 +1186,6 @@
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -1221,7 +1202,6 @@
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -1238,7 +1218,6 @@
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -4131,6 +4110,27 @@
"baseline-browser-mapping": "dist/cli.js"
}
},
"node_modules/bcrypt": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz",
"integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==",
"hasInstallScript": true,
"dependencies": {
"node-addon-api": "^8.3.0",
"node-gyp-build": "^4.8.4"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/bcrypt/node_modules/node-addon-api": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
"integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
"engines": {
"node": "^18 || ^20 || >= 21"
}
},
"node_modules/bcryptjs": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
@@ -4996,6 +4996,15 @@
"node": ">= 8"
}
},
"node_modules/date-fns": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -5342,7 +5351,6 @@
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
"dev": true,
"hasInstallScript": true,
"peer": true,
"bin": {
"esbuild": "bin/esbuild"
},
@@ -6311,6 +6319,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/get-tsconfig": {
"version": "4.13.0",
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz",
"integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==",
"dev": true,
"dependencies": {
"resolve-pkg-maps": "^1.0.0"
},
"funding": {
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
"node_modules/glob": {
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
@@ -8235,6 +8255,14 @@
"mkdirp": "bin/cmd.js"
}
},
"node_modules/moment": {
"version": "2.30.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
"engines": {
"node": "*"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -8329,6 +8357,16 @@
}
}
},
"node_modules/node-gyp-build": {
"version": "4.8.4",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
"bin": {
"node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js",
"node-gyp-build-test": "build-test.js"
}
},
"node_modules/node-int64": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
@@ -9161,6 +9199,15 @@
"node": ">=4"
}
},
"node_modules/resolve-pkg-maps": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
"dev": true,
"funding": {
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
}
},
"node_modules/resolve.exports": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz",
@@ -10464,6 +10511,25 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
},
"node_modules/tsx": {
"version": "4.20.6",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz",
"integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==",
"dev": true,
"dependencies": {
"esbuild": "~0.25.0",
"get-tsconfig": "^4.7.5"
},
"bin": {
"tsx": "dist/cli.mjs"
},
"engines": {
"node": ">=18.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
}
},
"node_modules/tunnel": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
@@ -11168,6 +11234,14 @@
"engines": {
"node": "*"
}
},
"node_modules/zod": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz",
"integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
}
}

View File

@@ -22,7 +22,8 @@
"prisma:push": "prisma db push",
"prisma:migrate": "prisma migrate dev",
"prisma:studio": "prisma studio",
"prisma:seed": "ts-node prisma/seed.ts"
"prisma:seed": "ts-node prisma/seed.ts",
"seeder": "tsx prisma/seed.ts"
},
"dependencies": {
"@nestjs/common": "^10.3.0",
@@ -37,11 +38,14 @@
"@types/http-status": "^1.1.2",
"ajv": "8.12.0",
"aws-lambda": "^1.0.7",
"bcrypt": "^6.0.0",
"bcryptjs": "^2.4.3",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"date-fns": "^4.1.0",
"helmet": "^7.1.0",
"http-status": "^2.1.0",
"moment": "^2.30.1",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
@@ -49,7 +53,8 @@
"rxjs": "^7.8.1",
"serverless": "4.17.0",
"swagger-ui-express": "^5.0.0",
"yup": "^1.7.1"
"yup": "^1.7.1",
"zod": "^4.1.12"
},
"devDependencies": {
"@nestjs/cli": "^10.3.0",
@@ -77,6 +82,7 @@
"ts-loader": "^9.5.1",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"tsx": "^4.20.6",
"typescript": "^5.3.3"
},
"jest": {

6
prisma/prisma.ts Normal file
View File

@@ -0,0 +1,6 @@
import { PrismaClient } from '@prisma/client';
export const prisma = new PrismaClient();
process.on('SIGINT', async () => {
await prisma.$disconnect();
process.exit(0);
});

View File

@@ -1,6 +1,6 @@
generator client {
provider = "prisma-client-js"
binaryTargets = ["native", "rhel-openssl-3.0.x"] // Add Linux target
provider = "prisma-client-js"
binaryTargets = ["native", "rhel-openssl-3.0.x"] // Add Linux target
previewFeatures = ["multiSchema"]
}
@@ -19,7 +19,8 @@ model User {
emailAddress String @unique @map("email_address")
isdCode String? @map("isd_code")
mobileNumber String? @map("mobile_number")
userPasscode String? @map("user_password")
userPassword String? @map("user_password")
userPasscode String? @map("user_passcode")
isEmailVerfied Boolean? @default(false) @map("is_email_verified")
isMobileVerfied Boolean? @default(false) @map("is_mobile_verified")
isActive Boolean? @default(true) @map("is_active")

291
prisma/seed.ts Normal file
View File

@@ -0,0 +1,291 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
// ✅ Countries
const india = await prisma.countries.upsert({
where: { countryName: 'India' },
update: {},
create: {
countryName: 'India',
countryCode: 'IN',
countryFlag: '🇮🇳',
isActive: true,
},
});
const usa = await prisma.countries.upsert({
where: { countryName: 'United States' },
update: {},
create: {
countryName: 'United States',
countryCode: 'US',
countryFlag: '🇺🇸',
isActive: true,
},
});
// ✅ Currencies
await prisma.currencies.createMany({
data: [
{ countryXid: india.id, currencyName: 'Indian Rupee', currencySymbol: '₹' },
{ countryXid: usa.id, currencyName: 'US Dollar', currencySymbol: '$' },
],
skipDuplicates: true,
});
// ✅ States
const maharashtra = await prisma.states.upsert({
where: { stateName: 'Maharashtra' },
update: {},
create: { countryXid: india.id, stateName: 'Maharashtra' },
});
const california = await prisma.states.upsert({
where: { stateName: 'California' },
update: {},
create: { countryXid: usa.id, stateName: 'California' },
});
// ✅ Cities
await prisma.cities.createMany({
data: [
{ stateXid: maharashtra.id, cityName: 'Mumbai' },
{ stateXid: california.id, cityName: 'Los Angeles' },
],
skipDuplicates: true,
});
// ✅ Taxes
await prisma.taxes.createMany({
data: [
{ countryXid: india.id, taxName: 'GST', taxPer: 18 },
{ countryXid: usa.id, taxName: 'VAT', taxPer: 10 },
],
skipDuplicates: true,
});
// ✅ Banks
const hdfc = await prisma.banks.upsert({
where: { bankName: 'HDFC Bank' },
update: {},
create: { countryXid: india.id, bankName: 'HDFC Bank' },
});
const chase = await prisma.banks.upsert({
where: { bankName: 'Chase Bank' },
update: {},
create: { countryXid: usa.id, bankName: 'Chase Bank' },
});
// ✅ Bank Branches
await prisma.bankBranches.createMany({
data: [
{
bankXid: hdfc.id,
stateXid: maharashtra.id,
cityXid: (await prisma.cities.findFirst({ where: { cityName: 'Mumbai' } }))!.id,
branchAddress: 'HDFC Fort Branch, Mumbai',
ifscCode: 'HDFC0001234',
},
{
bankXid: chase.id,
stateXid: california.id,
cityXid: (await prisma.cities.findFirst({ where: { cityName: 'Los Angeles' } }))!.id,
branchAddress: 'Chase Downtown LA',
ifscCode: 'CHASUS12345',
},
],
skipDuplicates: true,
});
// ✅ Interests + Activity Types
const chillandzen = await prisma.interests.upsert({
where: { interestName: 'Chill and Zen' },
update: {},
create: { interestName: 'Chill and Zen', displayOrder: 1 },
});
const sweatmode = await prisma.interests.upsert({
where: { interestName: 'Sweat Mode' },
update: {},
create: { interestName: 'Sweat Mode', displayOrder: 2 },
});
const gameon = await prisma.interests.upsert({
where: { interestName: 'Game On' },
update: {},
create: { interestName: 'Game On', displayOrder: 3 },
});
const partycentral = await prisma.interests.upsert({
where: { interestName: 'Party Central' },
update: {},
create: { interestName: 'Party Central', displayOrder: 4 },
});
const artsy = await prisma.interests.upsert({
where: { interestName: 'Artsy' },
update: {},
create: { interestName: 'Artsy', displayOrder: 5 },
});
const foodiediaries = await prisma.interests.upsert({
where: { interestName: 'Foodie Diaries' },
update: {},
create: { interestName: 'Foodie Diaries', displayOrder: 6 },
});
await prisma.activityTypes.createMany({
data: [
{ interestXid: chillandzen.id, activityTypeName: 'Cricket' },
{ interestXid: chillandzen.id, activityTypeName: 'Football' },
],
skipDuplicates: true,
});
// ✅ Document Types
await prisma.documentType.createMany({
data: [
{ documentTypeName: 'GST Certificate', displayOrder: 1 },
{ documentTypeName: 'PAN / BIN Card', displayOrder: 2 },
{ documentTypeName: 'Registration Certification', displayOrder: 3 },
{ documentTypeName: 'Aadhaar Card', displayOrder: 4 },
{ documentTypeName: 'Fire Insurance', displayOrder: 5 },
{ documentTypeName: 'ATOAI Certification', displayOrder: 6 },
{ documentTypeName: 'FASSI Certification', displayOrder: 7 },
{ documentTypeName: 'Safety Certification', displayOrder: 8 },
{ documentTypeName: 'Others', displayOrder: 9 },
],
skipDuplicates: true,
});
// ✅ Amenities
await prisma.amenities.createMany({
data: [
{ amenitiesName: 'Wi-Fi' },
{ amenitiesName: 'Air Conditioner' },
{ amenitiesName: 'Phone Charger' },
{ amenitiesName: 'Mobile Network' },
],
skipDuplicates: true, // prevents error if already seeded
});
// ✅ Food Types
await prisma.foodTypes.createMany({
data: [
{ foodTypeName: 'Veg' },
{ foodTypeName: 'Non-Veg' },
{ foodTypeName: 'Vegan' },
{ foodTypeName: 'Jain' },
],
skipDuplicates: true,
});
// ✅ AGE RESTRICTIONS
await prisma.ageRestrictions.createMany({
data: [
{ ageRestrictionName: 'Below 18', minAge: 1, maxAge: 17 },
{ ageRestrictionName: 'Above 18', minAge: 18, maxAge: 200 },
],
skipDuplicates: true,
});
// ✅ ALLOWED ENTRY TYPES
await prisma.allowedEntryTypes.createMany({
data: [
{ allowedEntryTypeName: 'Solo' },
{ allowedEntryTypeName: 'Couple' },
{ allowedEntryTypeName: 'Group' },
],
skipDuplicates: true,
});
// ✅ ROLES
await prisma.roles.createMany({
data: [
{ roleName: 'Admin' },
{ roleName: 'Co_Admin' },
{ roleName: 'Account_Manager' },
{ roleName: 'Host' },
{ roleName: 'Operator' },
{ roleName: 'User' },
],
skipDuplicates: true,
});
// ✅ Frequencies
await prisma.frequencies.createMany({
data: [
{ frequencyName: 'Single Event' },
{ frequencyName: 'Multiple Events Per Day' },
{ frequencyName: 'Training' },
],
skipDuplicates: true,
});
// ✅ Navigation Modes
await prisma.navigationModes.createMany({
data: [
{ navigationModeName: 'Elephant Ride', navigationModeIcon: '🚗' },
{ navigationModeName: 'Horse Ride', navigationModeIcon: '🏍️' },
{ navigationModeName: 'Camel Ride', navigationModeIcon: '🚶' },
],
skipDuplicates: true,
});
// ✅ Transport Modes
await prisma.transportModes.createMany({
data: [
{ transportModeName: 'Open Jeep / Car / SUV - 4 seater', transportModeIcon: '🚌' },
{ transportModeName: 'Open Jeep / Car / SUV - 6 seater', transportModeIcon: '🚌' },
{ transportModeName: '4W Jeep / SUV - 4 seater', transportModeIcon: '🚆' },
{ transportModeName: '4W Jeep / SUV - 6 seater', transportModeIcon: '🚆' },
],
skipDuplicates: true,
});
// ✅ PQQ Categories + Questions + Answers
const category = await prisma.pQQCategories.upsert({
where: { categoryName: 'General' },
update: {},
create: {
categoryName: 'General',
subCategoryName: 'Basic',
categoryTitle: 'General Information',
displayOrder: 1,
isActive: true,
PQQQuestions: {
create: [
{
questionName: 'Do you have insurance?',
displayOrder: 1,
PQQAnswers: {
create: [
{ answerName: 'Yes', answerScore: '10', displayOrder: 1 },
{ answerName: 'No', answerScore: '0', displayOrder: 2 },
],
},
},
{
questionName: 'Do you have ISO certification?',
displayOrder: 2,
PQQAnswers: {
create: [
{ answerName: 'Yes', answerScore: '20', displayOrder: 1 },
{ answerName: 'No', answerScore: '0', displayOrder: 2 },
],
},
},
],
},
},
include: { PQQQuestions: true },
});
console.log('✅ Seed data inserted successfully!');
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});

View File

@@ -7,6 +7,11 @@ provider:
versionFunctions: false
environment:
DATABASE_URL: ${env:DATABASE_URL}
DB_USERNAME: ${env:DB_USERNAME}
DB_PASSWORD: ${env:DB_PASSWORD}
DB_DATABASE_NAME: ${env:DB_DATABASE_NAME}
DB_HOSTNAME: ${env:DB_HOSTNAME}
DB_PORT: ${env:DB_PORT}
BY_PASS_EMAIL: ${env:BY_PASS_EMAIL}
BYPASS_OTP: ${env:BYPASS_OTP}
BREVO_EMAIL_API_KEY: ${env:BREVO_EMAIL_API_KEY}
@@ -19,6 +24,8 @@ provider:
REFRESH_TOKEN_SECRET: ${env:REFRESH_TOKEN_SECRET}
JWT_SECRET: ${env:JWT_SECRET}
SALT_ROUNDS: ${env:SALT_ROUNDS}
NODE_ENV: ${env:NODE_ENV}
httpApi:
cors:
@@ -71,3 +78,59 @@ functions:
- httpApi:
path: /host
method: get
verifyOtp:
handler: src/modules/host/handlers/verifyOtp.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/verify-otp
method: post
loginForHost:
handler: src/modules/host/handlers/loginForHost.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/login
method: post
registrationOfHost:
handler: src/modules/host/handlers/registration.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/registration
method: post
createPasswordForHost:
handler: src/modules/host/handlers/createPassword.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/create-password
method: post

View File

@@ -8,7 +8,8 @@ import config from '../../../config/config';
const prisma = new PrismaClient();
interface DecodedToken {
id: number;
id?: number;
sub?: string | number;
role?: string;
iat: number;
exp: number;
@@ -26,7 +27,59 @@ declare module 'express-serve-static-core' {
}
/**
* Verifies JWT and validates Host user (role_xid = 3)
* Core authentication function - verifies JWT and validates Host user
* Can be used by both Express middleware and Lambda handlers
*/
export async function verifyHostToken(token: string): Promise<{ id: number; role?: string }> {
if (!token) {
throw new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate');
}
try {
const decoded = jwt.verify(token, config.jwt.secret) as unknown as DecodedToken;
const userId = decoded.id ?? (decoded.sub ? Number(decoded.sub) : null);
if (!userId) {
throw new ApiError(httpStatus.UNAUTHORIZED, 'Invalid token payload');
}
// ✅ Fetch user from Prisma (Host user only)
const user = await prisma.user.findUnique({
where: { id: userId },
include: { role: true },
});
if (!user) {
throw new ApiError(httpStatus.UNAUTHORIZED, 'User not found');
}
// ✅ Check if user is active
if (user.isActive === false) {
throw new ApiError(httpStatus.FORBIDDEN, 'Your account is deactivated by admin.');
}
// ✅ Check Host role (role_xid = 4)
if (user.roleXid !== 4) {
throw new ApiError(httpStatus.FORBIDDEN, 'Access restricted to host users only');
}
return { id: user.id, role: user.role?.roleName };
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
throw new ApiError(httpStatus.UNAUTHORIZED, 'Your session has expired. Please log in again.');
}
if (error instanceof ApiError) {
throw error;
}
throw new ApiError(httpStatus.FORBIDDEN, 'Invalid or expired authentication token.');
}
}
/**
* Verifies JWT and validates Host user (role_xid = 4)
*/
const verifyCallback = async (
req: Request,
@@ -35,62 +88,22 @@ const verifyCallback = async (
) => {
const token = req.header('x-auth-token') || req.cookies?.accessToken;
if (!token) {
return reject(new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate'));
}
try {
const decoded = jwt.verify(token, config.jwt.secret) as DecodedToken;
if (!decoded?.id) {
return reject(new ApiError(httpStatus.UNAUTHORIZED, 'Invalid token payload'));
}
// ✅ Fetch user from Prisma (Host user only)
const user = await prisma.user.findUnique({
where: { id: decoded.id },
include: { role: true },
});
if (!user) {
return reject(new ApiError(httpStatus.UNAUTHORIZED, 'User not found'));
}
// ✅ Check if user is active
if (!user.isActive) {
return reject(
new ApiError(httpStatus.FORBIDDEN, 'Your account is deactivated by admin.')
);
}
// ✅ Check Host role (role_xid = 3)
if (user.roleXid !== 3) {
return reject(
new ApiError(httpStatus.FORBIDDEN, 'Access restricted to host users only')
);
}
const userInfo = await verifyHostToken(token);
// Attach user to request
req.user = { id: user.id.toString(), role: user.role?.roleName };
req.user = { id: userInfo.id.toString(), role: userInfo.role };
resolve();
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
return reject(
new ApiError(httpStatus.UNAUTHORIZED, 'Your session has expired. Please log in again.')
);
}
return reject(
new ApiError(httpStatus.FORBIDDEN, 'Invalid or expired authentication token.')
);
return reject(error as Error);
}
};
/**
* Express middleware — use as `auth()` in routes
*/
const auth =
const authForHost =
() =>
async (req: Request, res: Response, next: NextFunction) => {
return new Promise((resolve, reject) => {
@@ -100,4 +113,4 @@ const auth =
.catch((err) => next(err));
};
export default auth;
export default authForHost;

View File

@@ -26,24 +26,19 @@ declare module 'express-serve-static-core' {
}
/**
* Verifies JWT and validates Host user (role_xid = 3)
* Core authentication function - verifies JWT and validates Host user
* Can be used by both Express middleware and Lambda handlers
*/
const verifyCallback = async (
req: Request,
resolve: (value?: unknown) => void,
reject: (reason?: Error) => void
) => {
const token = req.header('x-auth-token') || req.cookies?.accessToken;
export async function verifyHostToken(token: string): Promise<{ id: number; role?: string }> {
if (!token) {
return reject(new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate'));
throw new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate');
}
try {
const decoded = jwt.verify(token, config.jwt.secret) as DecodedToken;
if (!decoded?.id) {
return reject(new ApiError(httpStatus.UNAUTHORIZED, 'Invalid token payload'));
throw new ApiError(httpStatus.UNAUTHORIZED, 'Invalid token payload');
}
// ✅ Fetch user from Prisma (Host user only)
@@ -53,44 +48,59 @@ const verifyCallback = async (
});
if (!user) {
return reject(new ApiError(httpStatus.UNAUTHORIZED, 'User not found'));
throw new ApiError(httpStatus.UNAUTHORIZED, 'User not found');
}
// ✅ Check if user is active
if (!user.isActive) {
return reject(
new ApiError(httpStatus.FORBIDDEN, 'Your account is deactivated by admin.')
);
if (user.isActive === false) {
throw new ApiError(httpStatus.FORBIDDEN, 'Your account is deactivated by admin.');
}
// ✅ Check Admin role (role_xid = 2)
if (user.roleXid !== 2) {
return reject(
new ApiError(httpStatus.FORBIDDEN, 'Access restricted to host users only')
);
// ✅ Check Host role (role_xid = 1)
if (user.roleXid !== 1) {
throw new ApiError(httpStatus.FORBIDDEN, 'Access restricted to host users only');
}
return { id: user.id, role: user.role?.roleName };
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
throw new ApiError(httpStatus.UNAUTHORIZED, 'Your session has expired. Please log in again.');
}
if (error instanceof ApiError) {
throw error;
}
throw new ApiError(httpStatus.FORBIDDEN, 'Invalid or expired authentication token.');
}
}
/**
* Verifies JWT and validates Host user (role_xid = 4)
*/
const verifyCallback = async (
req: Request,
resolve: (value?: unknown) => void,
reject: (reason?: Error) => void
) => {
const token = req.header('x-auth-token') || req.cookies?.accessToken;
try {
const userInfo = await verifyHostToken(token);
// Attach user to request
req.user = { id: user.id.toString(), role: user.role?.roleName };
req.user = { id: userInfo.id.toString(), role: userInfo.role };
resolve();
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
return reject(
new ApiError(httpStatus.UNAUTHORIZED, 'Your session has expired. Please log in again.')
);
}
return reject(
new ApiError(httpStatus.FORBIDDEN, 'Invalid or expired authentication token.')
);
return reject(error as Error);
}
};
/**
* Express middleware — use as `auth()` in routes
*/
const auth =
const authForHost =
() =>
async (req: Request, res: Response, next: NextFunction) => {
return new Promise((resolve, reject) => {
@@ -100,4 +110,4 @@ const auth =
.catch((err) => next(err));
};
export default auth;
export default authForHost;

View File

@@ -26,24 +26,19 @@ declare module 'express-serve-static-core' {
}
/**
* Verifies JWT and validates Host user (role_xid = 3)
* Core authentication function - verifies JWT and validates Host user
* Can be used by both Express middleware and Lambda handlers
*/
const verifyCallback = async (
req: Request,
resolve: (value?: unknown) => void,
reject: (reason?: Error) => void
) => {
const token = req.header('x-auth-token') || req.cookies?.accessToken;
export async function verifyHostToken(token: string): Promise<{ id: number; role?: string }> {
if (!token) {
return reject(new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate'));
throw new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate');
}
try {
const decoded = jwt.verify(token, config.jwt.secret) as DecodedToken;
if (!decoded?.id) {
return reject(new ApiError(httpStatus.UNAUTHORIZED, 'Invalid token payload'));
throw new ApiError(httpStatus.UNAUTHORIZED, 'Invalid token payload');
}
// ✅ Fetch user from Prisma (Host user only)
@@ -53,44 +48,59 @@ const verifyCallback = async (
});
if (!user) {
return reject(new ApiError(httpStatus.UNAUTHORIZED, 'User not found'));
throw new ApiError(httpStatus.UNAUTHORIZED, 'User not found');
}
// ✅ Check if user is active
if (!user.isActive) {
return reject(
new ApiError(httpStatus.FORBIDDEN, 'Your account is deactivated by admin.')
);
if (user.isActive === false) {
throw new ApiError(httpStatus.FORBIDDEN, 'Your account is deactivated by admin.');
}
// ✅ Check User role (role_xid = 1)
if (user.roleXid !== 1) {
return reject(
new ApiError(httpStatus.FORBIDDEN, 'Access restricted to host users only')
);
// ✅ Check Host role (role_xid = 6)
if (user.roleXid !== 6) {
throw new ApiError(httpStatus.FORBIDDEN, 'Access restricted to host users only');
}
return { id: user.id, role: user.role?.roleName };
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
throw new ApiError(httpStatus.UNAUTHORIZED, 'Your session has expired. Please log in again.');
}
if (error instanceof ApiError) {
throw error;
}
throw new ApiError(httpStatus.FORBIDDEN, 'Invalid or expired authentication token.');
}
}
/**
* Verifies JWT and validates Host user (role_xid = 4)
*/
const verifyCallback = async (
req: Request,
resolve: (value?: unknown) => void,
reject: (reason?: Error) => void
) => {
const token = req.header('x-auth-token') || req.cookies?.accessToken;
try {
const userInfo = await verifyHostToken(token);
// Attach user to request
req.user = { id: user.id.toString(), role: user.role?.roleName };
req.user = { id: userInfo.id.toString(), role: userInfo.role };
resolve();
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
return reject(
new ApiError(httpStatus.UNAUTHORIZED, 'Your session has expired. Please log in again.')
);
}
return reject(
new ApiError(httpStatus.FORBIDDEN, 'Invalid or expired authentication token.')
);
return reject(error as Error);
}
};
/**
* Express middleware — use as `auth()` in routes
*/
const auth =
const authForHost =
() =>
async (req: Request, res: Response, next: NextFunction) => {
return new Promise((resolve, reject) => {
@@ -100,4 +110,4 @@ const auth =
.catch((err) => next(err));
};
export default auth;
export default authForHost;

View File

@@ -0,0 +1,37 @@
import * as crypto from 'crypto';
const algorithm = 'aes-256-cbc';
const secretKey = crypto.scryptSync('your-secret-password', 'salt', 32);
const ivLength = 16;
// Encrypt function
export function encryptUserId(id: string): string {
const iv = crypto.randomBytes(ivLength);
const cipher = crypto.createCipheriv(algorithm, secretKey, iv);
let encrypted = cipher.update(id, 'utf8', 'hex');
encrypted += cipher.final('hex');
return `${iv.toString('hex')}:${encrypted}`;
}
// Decrypt function
export function decryptUserId(encryptedId: string): string | null {
try {
const parts = encryptedId.split(':');
if (parts.length !== 2) {
console.error('Invalid encryptedId format:', encryptedId);
return null;
}
const iv = Buffer.from(parts[0], 'hex');
const encryptedText = Buffer.from(parts[1], 'hex');
const decipher = crypto.createDecipheriv(algorithm, secretKey, iv);
let decrypted = decipher.update(encryptedText);
decrypted = Buffer.concat([decrypted, decipher.final()]);
return decrypted.toString('utf8');
} catch (error) {
console.error('Decryption failed:', error);
return null;
}
}

View File

@@ -0,0 +1,18 @@
import config from '../../../config/config';
export class OtpGenerator {
static generateOtp(): string {
if (config.byPassOTP) {
return '1234';
}
return Math.floor(1000 + Math.random() * 9000).toString();
}
}
export class OtpGeneratorSixDigit {
static generateOtp(): string {
if (config.byPassOTP) {
return '123456';
}
return Math.floor(100000 + Math.random() * 900000).toString();
}
}

View File

@@ -0,0 +1,91 @@
import * as bcrypt from "bcryptjs";
import { OtpGenerator, OtpGeneratorSixDigit } from "./OtpGenerator";
import { encryptUserId } from "./CodeGenerator";
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
export interface OtpResult {
otp: string; // Plain OTP (for sending)
hashedOtp: string; // Hashed OTP (for DB storage)
expiry: Date; // Expiry timestamp
encryptedId: string; // Encrypted user ID
emailMessage: string; // Message body for email
emailSubject: string; // Subject line for email
}
/**
* Generate OTP, store it in DB, and send email.
* @param userId The users ID
* @param email Recipient email
* @param emailPurpose For which flow (e.g. "Register", "Login", "ForgotPassword")
* @param otpLength OTP length (4 or 6)
* @param expiryMinutes Expiry time in minutes
*/
export async function generateOtpHelper(
userId: number,
email: string,
emailPurpose: "Register" | "Login" | "ForgotPassword",
otpLength: 4 | 6 = 4,
expiryMinutes: number = 5
): Promise<OtpResult> {
// Generate OTP
const otp =
otpLength === 6
? OtpGeneratorSixDigit.generateOtp()
: OtpGenerator.generateOtp();
console.log("Generated OTP:", otp);
// Hash OTP
const hashedOtp = await bcrypt.hash(otp, 10);
// Expiry time
const expiry = new Date(Date.now() + expiryMinutes * 60 * 1000);
// Encrypt user ID
const encryptedId = encryptUserId(userId.toString());
// 🔹 First delete old OTPs for this user & purpose
await prisma.userOtp.deleteMany({
where: {
userXid: userId,
otpType: emailPurpose,
isActive: true,
},
});
// Save OTP into user_otps table
await prisma.userOtp.create({
data: {
userXid: userId,
otpType: emailPurpose,
otpCode: hashedOtp,
expiresOn: expiry,
isVerified: false,
isActive: true,
// sendOn will default to now()
// createdAt will default to now()
},
});
// Build email content
const emailSubject = `${emailPurpose} OTP Verification`;
const emailMessage = `Your OTP for ${emailPurpose} is ${otp}. It will expire in ${expiryMinutes} minutes.`;
// Send email
// await sendBulkEmailForOTP([
// {
// to: [{ email: email }],
// subject: emailSubject,
// htmlContent: emailMessage,
// },
// ]);
return {
otp,
hashedOtp,
expiry,
encryptedId,
emailMessage,
emailSubject,
};
}

View File

@@ -0,0 +1,36 @@
// validations/hostBankDetails.validation.ts
import { z } from "zod";
export const hostBankDetailsSchema = z.object({
accountNumber: z
.number()
.int({ message: "Account number must be an integer" })
.positive({ message: "Account number must be a positive number" }),
accountHolderName: z
.string()
.nonempty("Account holder name is required")
.min(2, { message: "Account holder name must be at least 2 characters" }),
ifscCode: z
.string()
.nonempty("IFSC code is required")
.regex(/^[A-Z]{4}0[A-Z0-9]{6}$/, { message: "Invalid IFSC code format" }),
bankXid: z
.number()
.int({ message: "Bank ID must be an integer" })
.positive({ message: "Bank ID must be a positive number" }),
hostXid: z
.number()
.int({ message: "Host ID must be an integer" })
.positive({ message: "Host ID must be a positive number" }),
bankBranchXid: z
.number()
.int({ message: "Bank branch ID must be an integer" })
.positive({ message: "Bank branch ID must be a positive number" }),
});
export type HostBankDetailsSchema = z.infer<typeof hostBankDetailsSchema>;

View File

@@ -0,0 +1,44 @@
import { z } from "zod";
// Allowed document types (must match your DocumentType master table IDs)
export const REQUIRED_DOC_TYPES = {
PAN: 1,
GST: 2,
REGISTRATION: 3,
AADHAAR: 4,
};
export const hostCompanyDetailsSchema = z.object({
companyName: z.string().min(1, "Company name is required"),
address1: z.string().min(1, "Address1 is required"),
address2: z.string().optional(),
hostRefNumber: z.string().min(1, "Host reference number is required"),
cityXid: z.number().min(1, "City is required"),
stateXid: z.number().min(1, "State is required"),
countryXid: z.number().min(1,"Country is required"),
pinCode: z.string().min(4, "Pincode/Zipcode is required"),
logoPath: z.string().optional(),
isSubsidairy: z.boolean(),
registrationNumber: z.string().min(1, "Registration number is required"),
panNumber: z.string().min(1, "PAN number is required"),
gstNumber: z.string().optional(),
formationDate: z.string().refine((val) => !isNaN(Date.parse(val)), {
message: "Formation date must be a valid date",
}),
companyType: z.string().min(1, "Company type is required"),
websiteUrl: z.url().optional(),
instagramUrl: z.url().optional(),
facebookUrl: z.url().optional(),
linkedinUrl: z.url().optional(),
twitterUrl: z.url().optional(),
currencyXid: z.number().min(1, "Currency is required"),
});
// Validation for documents
export const hostDocumentsSchema = z.array(
z.object({
documentTypeXid: z.number(),
documentName: z.string(),
filePath: z.string(),
})
);

View File

@@ -0,0 +1,20 @@
// validations/hostBankDetails.validation.ts
import { z } from "zod";
export const loginForHostSchema = z.object({
emailAddress : z
.string()
.nonempty("Email is required"),
userPassword : z
.string()
.nonempty("Password is required")
.min(8, { message: "Password must be at least 8 characters" }),
});
export type loginForHostSchema = z.infer<typeof loginForHostSchema>;

View File

@@ -12,7 +12,6 @@ const envVarsSchema = yup
.oneOf(['production', 'development', 'test'])
.required(),
PORT: yup.number().default(3000),
BASEURL: yup.string().required('Base URL is required'),
// FRONTEND_URL: yup.string().required('Frontend URL is required'),
//JWT
JWT_SECRET: yup.string().required('JWT secret key is required'),

View File

@@ -21,7 +21,7 @@ export class CreateHostDto {
@IsOptional()
@IsString()
userPasscode?: string;
userPassword?: string;
@IsOptional()
@IsInt()
@@ -49,3 +49,27 @@ export class UpdateHostDto {
@IsBoolean()
isActive?: boolean;
}
export class GetHostLoginResponseDTO {
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;
}
}

View File

@@ -0,0 +1,65 @@
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<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 verifyHostToken(token);
const user_xid = userInfo.id;
// Parse request body
let body: { password?: string; confirmPassword?: string };
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const { password, confirmPassword } = body;
if (!password || !confirmPassword) {
throw new ApiError(400, 'Password and confirm password are required');
}
// Validate password match
if (password !== confirmPassword) {
throw new ApiError(400, 'Password and confirm password do not match');
}
// Validate password length
if (password.length < 8) {
throw new ApiError(400, 'Password must be at least 8 characters long');
}
await hostService.createPassword(user_xid, password);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Password created successfully',
data: null,
}),
};
});

View File

@@ -0,0 +1,79 @@
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<APIGatewayProxyResult> => {
// 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 || !userPassword) {
throw new ApiError(400, 'Email and password are required');
}
const loginForHost = await hostService.loginForHost(emailAddress, userPassword);
if (!loginForHost) {
throw new ApiError(400, 'Failed to login');
}
if (!loginForHost.userPassword) {
throw new ApiError(401, 'Invalid credentials');
}
const matchPassword = await bcrypt.compare(
userPassword,
loginForHost.userPassword
);
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,
}),
};
});

View File

@@ -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 { HostService } from '../services/host.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 hostService = new HostService(prismaService);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
// 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 hostService.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: {},
}),
};
});

View File

@@ -0,0 +1,52 @@
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 { TokenService } from '../services/token.service';
const prismaService = new PrismaService();
const hostService = new HostService(prismaService);
const tokenService = new TokenService();
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
// Parse request body
let body: { email?: string; otp?: string };
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const { email, otp } = body;
if (!email || !otp) {
throw new ApiError(400, 'Email and OTP are required');
}
await hostService.verifyHostOtp(email, otp);
const user = await hostService.getHostByEmail(email);
const generateTokenForHost = 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: generateTokenForHost.access.token,
refreshToken: generateTokenForHost.refresh.token,
data: null,
}),
};
});

View File

@@ -2,10 +2,13 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../../../common/database/prisma.service';
import { CreateHostDto, UpdateHostDto } from '../dto/host.dto';
import * as bcrypt from 'bcryptjs';
import ApiError from '../../../common/utils/helper/ApiError';
import { User } from '@prisma/client';
@Injectable()
export class HostService {
constructor(private prisma: PrismaService) {}
constructor(private prisma: PrismaService) { }
async createHost(data: CreateHostDto) {
return this.prisma.user.create({ data });
@@ -17,7 +20,9 @@ export class HostService {
async getHostById(id: number) {
const host = await this.prisma.user.findUnique({ where: { id } });
if (!host || host.roleXid !== 3) throw new Error('Host not found');
if (!host || host.roleXid !== 4) {
throw new ApiError(404, 'Host not found');
}
return host;
}
@@ -31,4 +36,111 @@ export class HostService {
async deleteHost(id: number) {
return this.prisma.user.delete({ where: { id } });
}
async getHostByEmail(email: string): Promise<User> {
return this.prisma.user.findUnique({ where: { emailAddress: email } });
}
async verifyHostOtp(email: string, otp: string): Promise<boolean> {
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 loginForHost(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 createPassword(user_xid: number, password: string): Promise<boolean> {
// Find user by id
const user = await this.prisma.user.findUnique({
where: { id: user_xid },
select: { id: true, emailAddress: true, userPassword: true },
});
if (!user) {
throw new ApiError(404, 'User not found');
}
// Check if password already exists
if (user.userPassword) {
throw new ApiError(400, 'Password already exists. Use update password instead.');
}
// Hash the password
const saltRounds = parseInt(process.env.SALT_ROUNDS || '10', 10);
const hashedPassword = await bcrypt.hash(password, saltRounds);
// Update user with hashed password
await this.prisma.user.update({
where: { id: user.id },
data: { userPassword: hashedPassword },
});
return true;
}
}

View File

@@ -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<boolean> {
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<boolean> {
const existing = await prisma.token.findUnique({
where: { token },
});
return existing ? true : false;
}
async verifyRefreshToken(
token: string
): Promise<string | JwtPayload | null> {
try {
return jwt.verify(token, config.jwt.secret);
} catch {
return null;
}
}
async decodeToken(token: string): Promise<string | JwtPayload | null> {
try {
return jwt.decode(token);
} catch {
return null;
}
}
}

View File

@@ -1,27 +1,631 @@
{
"openapi": "3.0.0",
"paths": {},
"info": {
"title": "Minglar API",
"description": "NestJS Backend for Minglar with Lambda-ready endpoints",
"description": "NestJS Backend for Minglar with AWS Lambda endpoints",
"version": "1.0.0",
"contact": {}
"contact": {
"name": "API Support"
}
},
"tags": [],
"servers": [
{
"url": "http://localhost:3000/",
"description": "Local Server"
"url": "https://api.minglar.com",
"description": "Production Server"
},
{
"url": "http://localhost:3000",
"description": "Local Development Server"
}
],
"components": {
"securitySchemes": {
"bearer": {
"scheme": "bearer",
"bearerFormat": "JWT",
"type": "http"
"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"
}
}
}
}
}
}
},
"schemas": {}
"/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",
"scheme": "bearer",
"bearerFormat": "JWT",
"description": "JWT token obtained from login endpoint"
}
},
"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"]
}
}
}
}
}