From c0e58fe1ce5df70d048f177dbf310453f124587b Mon Sep 17 00:00:00 2001 From: Mayank Mishra Date: Wed, 12 Nov 2025 16:03:57 +0530 Subject: [PATCH] made register and login apis for host --- package-lock.json | 130 +++- package.json | 10 +- prisma/prisma.ts | 6 + prisma/schema.prisma | 7 +- prisma/seed.ts | 291 ++++++++ serverless.yml | 63 ++ src/common/middlewares/jwt/authForHost.ts | 109 +-- .../middlewares/jwt/authForMinglarAdmin.ts | 74 +- src/common/middlewares/jwt/authForUser.ts | 74 +- src/common/utils/helper/CodeGenerator.ts | 37 + src/common/utils/helper/OtpGenerator.ts | 18 + src/common/utils/helper/sendOtp.ts | 91 +++ .../host/addPaymentDetails.validation.ts | 36 + .../host/hostCompanyDetails.validation.ts | 44 ++ .../utils/validation/host/login.validation.ts | 20 + src/config/config.ts | 1 - src/modules/host/dto/host.dto.ts | 26 +- src/modules/host/handlers/createPassword.ts | 65 ++ src/modules/host/handlers/loginForHost.ts | 79 +++ src/modules/host/handlers/registration.ts | 75 +++ src/modules/host/handlers/verifyOtp.ts | 52 ++ src/modules/host/services/host.service.ts | 116 +++- src/modules/host/services/token.service.ts | 159 +++++ swagger.json | 632 +++++++++++++++++- 24 files changed, 2052 insertions(+), 163 deletions(-) create mode 100644 prisma/prisma.ts create mode 100644 prisma/seed.ts create mode 100644 src/common/utils/helper/CodeGenerator.ts create mode 100644 src/common/utils/helper/OtpGenerator.ts create mode 100644 src/common/utils/helper/sendOtp.ts create mode 100644 src/common/utils/validation/host/addPaymentDetails.validation.ts create mode 100644 src/common/utils/validation/host/hostCompanyDetails.validation.ts create mode 100644 src/common/utils/validation/host/login.validation.ts create mode 100644 src/modules/host/handlers/createPassword.ts create mode 100644 src/modules/host/handlers/loginForHost.ts create mode 100644 src/modules/host/handlers/registration.ts create mode 100644 src/modules/host/handlers/verifyOtp.ts create mode 100644 src/modules/host/services/token.service.ts diff --git a/package-lock.json b/package-lock.json index 95e614e..177254f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" + } } } } diff --git a/package.json b/package.json index b8c59f6..2c7edf8 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/prisma/prisma.ts b/prisma/prisma.ts new file mode 100644 index 0000000..2f27bb4 --- /dev/null +++ b/prisma/prisma.ts @@ -0,0 +1,6 @@ +import { PrismaClient } from '@prisma/client'; +export const prisma = new PrismaClient(); +process.on('SIGINT', async () => { + await prisma.$disconnect(); + process.exit(0); +}); \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 6a71ec7..ad58f2e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -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") diff --git a/prisma/seed.ts b/prisma/seed.ts new file mode 100644 index 0000000..93d5cbf --- /dev/null +++ b/prisma/seed.ts @@ -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(); + }); diff --git a/serverless.yml b/serverless.yml index bf8c6f1..6898616 100644 --- a/serverless.yml +++ b/serverless.yml @@ -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 \ No newline at end of file diff --git a/src/common/middlewares/jwt/authForHost.ts b/src/common/middlewares/jwt/authForHost.ts index e74b21c..bc12c77 100644 --- a/src/common/middlewares/jwt/authForHost.ts +++ b/src/common/middlewares/jwt/authForHost.ts @@ -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; diff --git a/src/common/middlewares/jwt/authForMinglarAdmin.ts b/src/common/middlewares/jwt/authForMinglarAdmin.ts index d63c129..ff28b99 100644 --- a/src/common/middlewares/jwt/authForMinglarAdmin.ts +++ b/src/common/middlewares/jwt/authForMinglarAdmin.ts @@ -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; diff --git a/src/common/middlewares/jwt/authForUser.ts b/src/common/middlewares/jwt/authForUser.ts index b7ecad0..d88c4ab 100644 --- a/src/common/middlewares/jwt/authForUser.ts +++ b/src/common/middlewares/jwt/authForUser.ts @@ -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; diff --git a/src/common/utils/helper/CodeGenerator.ts b/src/common/utils/helper/CodeGenerator.ts new file mode 100644 index 0000000..74f0175 --- /dev/null +++ b/src/common/utils/helper/CodeGenerator.ts @@ -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; + } +} diff --git a/src/common/utils/helper/OtpGenerator.ts b/src/common/utils/helper/OtpGenerator.ts new file mode 100644 index 0000000..59d78dd --- /dev/null +++ b/src/common/utils/helper/OtpGenerator.ts @@ -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(); + } +} diff --git a/src/common/utils/helper/sendOtp.ts b/src/common/utils/helper/sendOtp.ts new file mode 100644 index 0000000..03f512d --- /dev/null +++ b/src/common/utils/helper/sendOtp.ts @@ -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 userโ€™s 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 { + // 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, + }; +} diff --git a/src/common/utils/validation/host/addPaymentDetails.validation.ts b/src/common/utils/validation/host/addPaymentDetails.validation.ts new file mode 100644 index 0000000..bbd30a1 --- /dev/null +++ b/src/common/utils/validation/host/addPaymentDetails.validation.ts @@ -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; diff --git a/src/common/utils/validation/host/hostCompanyDetails.validation.ts b/src/common/utils/validation/host/hostCompanyDetails.validation.ts new file mode 100644 index 0000000..d92825f --- /dev/null +++ b/src/common/utils/validation/host/hostCompanyDetails.validation.ts @@ -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(), + }) +); diff --git a/src/common/utils/validation/host/login.validation.ts b/src/common/utils/validation/host/login.validation.ts new file mode 100644 index 0000000..49128b5 --- /dev/null +++ b/src/common/utils/validation/host/login.validation.ts @@ -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; diff --git a/src/config/config.ts b/src/config/config.ts index ecb7662..1a6e381 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -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'), diff --git a/src/modules/host/dto/host.dto.ts b/src/modules/host/dto/host.dto.ts index fbdfd63..6bce7fc 100644 --- a/src/modules/host/dto/host.dto.ts +++ b/src/modules/host/dto/host.dto.ts @@ -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; + } +} \ No newline at end of file diff --git a/src/modules/host/handlers/createPassword.ts b/src/modules/host/handlers/createPassword.ts new file mode 100644 index 0000000..c2666f5 --- /dev/null +++ b/src/modules/host/handlers/createPassword.ts @@ -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 => { + // 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, + }), + }; +}); + diff --git a/src/modules/host/handlers/loginForHost.ts b/src/modules/host/handlers/loginForHost.ts new file mode 100644 index 0000000..bcac137 --- /dev/null +++ b/src/modules/host/handlers/loginForHost.ts @@ -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 => { + // 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, + }), + }; +}); + diff --git a/src/modules/host/handlers/registration.ts b/src/modules/host/handlers/registration.ts new file mode 100644 index 0000000..5224a29 --- /dev/null +++ b/src/modules/host/handlers/registration.ts @@ -0,0 +1,75 @@ +import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda'; +import { safeHandler } from '../../../common/utils/handlers/safeHandler'; +import { PrismaService } from '../../../common/database/prisma.service'; +import { 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 => { + // 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: {}, + }), + }; +}); + diff --git a/src/modules/host/handlers/verifyOtp.ts b/src/modules/host/handlers/verifyOtp.ts new file mode 100644 index 0000000..c18eb6e --- /dev/null +++ b/src/modules/host/handlers/verifyOtp.ts @@ -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 => { + // 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, + }), + }; +}); + diff --git a/src/modules/host/services/host.service.ts b/src/modules/host/services/host.service.ts index 83b66af..e953d5f 100644 --- a/src/modules/host/services/host.service.ts +++ b/src/modules/host/services/host.service.ts @@ -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 { + return this.prisma.user.findUnique({ where: { emailAddress: email } }); + } + + async verifyHostOtp(email: string, otp: string): Promise { + const user = await this.prisma.user.findUnique({ + where: { emailAddress: email }, + select: { + id: true, + emailAddress: true, + UserOtp: { + where: { isActive: true, isVerified: false }, + orderBy: { createdAt: 'desc' }, + take: 1, + }, + }, + }); + + if (!user) { + throw new ApiError(404, 'User not found.'); + } + + const userOtp = user.UserOtp[0]; + + if (!userOtp) { + throw new ApiError(400, 'No OTP found.'); + } + + if (new Date() > userOtp.expiresOn) { + throw new ApiError(400, 'OTP has expired.'); + } + + const isMatch = await bcrypt.compare(otp, userOtp.otpCode); + + if (!isMatch) { + throw new ApiError(400, 'Invalid OTP.'); + } + + await this.prisma.userOtp.update({ + where: { id: userOtp.id }, + data: { + isVerified: true, + verifiedOn: new Date(), + isActive: false, + }, + }); + + return true; + } + + async 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 { + // 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; + } } diff --git a/src/modules/host/services/token.service.ts b/src/modules/host/services/token.service.ts new file mode 100644 index 0000000..50de3a7 --- /dev/null +++ b/src/modules/host/services/token.service.ts @@ -0,0 +1,159 @@ +import { PrismaClient } from "@prisma/client"; +import jwt, { JwtPayload } from "jsonwebtoken"; +import moment from "moment"; +import config from "../../../config/config"; + +const prisma = new PrismaClient(); + +export class TokenService { + private generateToken( + user_xid: number, + expiresIn: Date, + type: string, + secret: string + ): { token: string; expires: Date } { + const token = jwt.sign( + { + sub: user_xid, + iat: moment().unix(), + exp: moment(expiresIn).unix(), + type, + }, + secret + ); + + return { token, expires: expiresIn }; + } + + async generateAuthToken( + user_xid: number, + ): Promise<{ + access: { token: string; expires: Date }; + refresh: { token: string; expires: Date }; + }> { + const accessTokenExpires = moment() + .add(config.jwt.accessExpirationMinutes, "minutes") + .toDate(); + + const refreshTokenExpires = moment() + .add(config.jwt.refreshExpirationDays, "days") + .toDate(); + + const accessToken = this.generateToken( + user_xid, + accessTokenExpires, + "access", + config.jwt.secret + ); + + const refreshToken = this.generateToken( + user_xid, + refreshTokenExpires, + "refresh", + config.jwt.secret + ); + + await prisma.token.create({ + data: { + token: refreshToken.token, + expiringAt: refreshToken.expires, + tokenType: "refresh", + isBlackListed: false, + + user: { + connect: { id: user_xid }, + }, + }, + }); + + return { + access: accessToken, + refresh: refreshToken, + }; + } + + async generateAuthTokenAdmin( + user_xid: number + ): Promise<{ + access: { token: string; expires: Date }; + refresh: { token: string; expires: Date }; + }> { + const accessTokenExpires = moment() + .add(config.jwt.accessExpirationMinutes, "minutes") + .toDate(); + + const refreshTokenExpires = moment() + .add(config.jwt.refreshExpirationDays, "days") + .toDate(); + + const accessToken = this.generateToken( + user_xid, + accessTokenExpires, + "access", + config.jwt.secret + ); + + const refreshToken = this.generateToken( + user_xid, + refreshTokenExpires, + "refresh", + config.jwt.secret + ); + + await prisma.token.create({ + data: { + token: refreshToken.token, + expiringAt: refreshToken.expires, + tokenType: "refresh", + isBlackListed: false, + user: { + connect: { id: user_xid }, + }, + }, + }); + + return { + access: accessToken, + refresh: refreshToken, + }; + } + + async revokeToken(user_xid: number, deviceId: string): Promise { + const existingToken = await prisma.token.findFirst({ + where: { + id: user_xid, + deviceId, + }, + }); + + if (!existingToken) return false; + + await prisma.token.delete({ where: { id: existingToken.id } }); + return true; + } + + async isTokenBlackListed(token: string): Promise { + const existing = await prisma.token.findUnique({ + where: { token }, + }); + return existing ? true : false; + } + + async verifyRefreshToken( + token: string + ): Promise { + try { + return jwt.verify(token, config.jwt.secret); + } catch { + return null; + } + } + + async decodeToken(token: string): Promise { + try { + return jwt.decode(token); + } catch { + return null; + } + } +} diff --git a/swagger.json b/swagger.json index 476e7a6..300e5db 100644 --- a/swagger.json +++ b/swagger.json @@ -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"] + } + } } -} \ No newline at end of file +}