193 Commits

Author SHA1 Message Date
paritosh18
507797d27a refactor: update serverless configuration and function definitions for Minglar service
- Renamed service from `minglarDev` to `minglar`.
- Updated Prisma layer reference to use qualified ARN for better deployment handling.
- Added new HTTP API endpoints for host management and onboarding processes.
- Removed unnecessary patterns from the package configurations in `minglaradmin.yml` to streamline the deployment process.
2025-12-05 13:17:48 +05:30
paritosh18
d8687edb9f feat: add Prisma layer build scripts and configuration for Lambda deployment 2025-11-30 12:17:20 +05:30
paritosh18
6011318986 refactor: remove unused middleware and utility files related to Minglar Admin authentication and constants
- Deleted `authForMinglarAdmin.ts` middleware as it is no longer needed.
- Removed `minglar.constant.ts` which contained constants for Minglar-related functionalities.
- Eliminated `prisma.service.ts` as it was redundant in the current architecture.
- Cleared out `ApiError.ts` utility that handled error responses for Prisma-related operations.
2025-11-30 11:30:10 +05:30
paritosh18
28175bbd7d Replace verifyHostToken with verifyMinglarAdminHostToken for user authentication in getCityByState handler 2025-11-29 20:42:56 +05:30
paritosh18
572420823c Refactor validation schemas to remove unnecessary required_error messages and update host user query to use correct field names 2025-11-29 19:45:43 +05:30
paritosh18
9706e5b66b Refactor handlers to use Zod for request body and query parameter validation
- Updated `resendOtp.ts` to parse and validate request body using Zod.
- Refactored `createPassword.ts` to utilize Zod for body validation.
- Modified `getAMDetail_ById.ts` to implement Zod for path parameter validation.
- Changed `acceptHostApplication.ts` to use Zod for request body validation.
- Updated `addPQQSuggestion.ts` to validate request body with Zod.
- Refactored `addSuggestion.ts` to use Zod for body validation.
- Modified `getAllHostApplicationForAM.ts` to implement Zod for query parameter validation.
- Changed `getByIdHostDetails.ts` to use Zod for path parameter validation.
- Updated `rejectHostApplicationAM.ts` to validate request body with Zod.
- Refactored `rejectPQQbyAM.ts` to use Zod for body validation.
- Modified `acceptHostAppMinglar.ts` to implement Zod for request body validation.
- Changed `assignAM.ts` to use Zod for request body validation.
- Updated `editAgreementDetails.ts` to validate request body with Zod.
- Refactored `getAllActivityOfHost.ts` to implement Zod for path and query parameter validation.
- Changed `rejectHostApplication.ts` to use Zod for request body validation.
- Updated `loginForMinglar.ts` to validate request body with Zod.
- Refactored `registration.ts` to implement Zod for request body validation.
- Modified `getAllCoadminAndAM.ts` to use Zod for query parameter validation.
- Changed `getAllInvitationDetails.ts` to implement Zod for query parameter validation.
- Updated `getAllInvitedCoadminAndAM.ts` to validate query parameters with Zod.
- Refactored `inviteTeammate.ts` to use Zod for request body validation.
- Modified `getBranchByBank.ts` to implement Zod for query parameter validation.
- Changed `getCityByState.ts` to use Zod for query parameter validation.
2025-11-29 19:39:28 +05:30
paritosh18
b9c90b488f Merge branch 'mayank' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-11-29 19:02:27 +05:30
d1eef782a2 fixed the profileimage key 2025-11-29 19:00:50 +05:30
c16a7bb95e fixed the validation for submit company details 2025-11-29 18:57:57 +05:30
paritosh18
626ea34a63 Merge branch 'mayank' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-11-29 18:00:40 +05:30
paritosh18
31e0b3ff2d Add search functionality to getAllHostApplicationForAM, getAllCoadminAndAM, getAllInvitationDetails, and getAllInvitedCoadminAndAM handlers 2025-11-29 18:00:15 +05:30
07f6f0159c access applied 2025-11-29 17:21:24 +05:30
b1a70acfa8 sending the presigned url in getAMDetailById 2025-11-29 16:24:15 +05:30
5ad17869be fixed the document type name inserting issue 2025-11-29 16:13:15 +05:30
8961e49dac Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into sprint1 2025-11-29 16:04:55 +05:30
8d8702dfb6 stroring the document type in the user documents table 2025-11-29 16:04:00 +05:30
paritosh18
576c749b34 add JWT expiration settings to environment variables in serverless configuration 2025-11-29 16:03:29 +05:30
paritosh18
649fccb81b update JWT access token expiration default to 1440 minutes 2025-11-29 15:54:33 +05:30
15b8afd9b2 fixed the profile image issue 2025-11-29 14:29:41 +05:30
84c4b1f2b9 sending the userrefNumber in all endpoints 2025-11-29 14:17:31 +05:30
paritosh18
933addd60f Merge branch 'mayank' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-11-29 13:55:48 +05:30
paritosh18
18ff254e8d enable reload handler for serverless-offline plugin 2025-11-29 13:55:23 +05:30
ae7b453abd made prisma config file in root 2025-11-29 13:55:09 +05:30
7bec950096 adding a random reference number in user table 2025-11-29 13:48:55 +05:30
6b2b211990 updated cascade 2025-11-29 12:20:42 +05:30
2d36ca5dd7 fixed the function name 2025-11-29 12:14:28 +05:30
f75993f738 made the file name clear 2025-11-29 12:06:13 +05:30
264f2fa29c made a pagination service 2025-11-29 12:04:50 +05:30
e0b841b437 fixing the submit pqq answer api 2025-11-29 10:29:58 +05:30
54613534db sending the basic user details in the create password of minglar 2025-11-28 20:41:03 +05:30
6cac8fb163 sending the profile image in presigned url 2025-11-28 17:47:43 +05:30
6147b0f476 Refactor HostHeader and HostParent models in Prisma schema for consistency and clarity. Update submitCompanyDetails API to improve JSON field normalization and error handling. Enhance host and parent document management with presigned URL generation for S3 uploads. Implement upsert logic for host documents and parent company details, ensuring proper handling of existing records. 2025-11-28 17:20:27 +05:30
71e3f2a933 sending the user details in the getby id of host 2025-11-28 16:02:30 +05:30
174f13300c Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-11-28 15:57:37 +05:30
3f324bc1fc made get host details by id for minglar admin 2025-11-28 15:57:25 +05:30
paritosh18
27e19bd921 Merge branch 'mayank' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-11-28 15:35:28 +05:30
paritosh18
82cbaddce1 feat: add serverless-offline plugin and new showSuggestion function
- Added serverless-offline plugin to package.json and serverless.yml for local development.
- Implemented new showSuggestion function in host.yml with appropriate memory size and event configuration.
- Removed deprecated getSuggestion function from minglaradmin.yml and updated related handlers.
- Enhanced safeHandler to convert Prisma errors to ApiError automatically, improving error handling.
- Introduced comprehensive Prisma error handling in ApiError.ts, mapping error codes to user-friendly messages and HTTP status codes.
2025-11-28 15:33:43 +05:30
80bd926e16 fixed the submit company details api 2025-11-28 15:27:28 +05:30
15c1458f02 made resend otp lambda 2025-11-28 12:23:08 +05:30
0cedcec109 Enhance activity status constants and update validation logic in submitCompanyDetails API. Added new statuses for PQQ and improved validation handling for draft submissions. Updated rejectPQQbyAM to reflect new status updates. 2025-11-28 12:07:45 +05:30
9e47d861a2 made the stepper updation in submit company details api 2025-11-28 11:41:35 +05:30
4f3adff517 submit company details api s3 uploading issue resolved and required docs removed 2025-11-27 20:51:04 +05:30
8ccbdc8f32 fixed the submit last pqq question api logic 2025-11-27 20:00:29 +05:30
75077c00da made reject pqq by am api 2025-11-27 19:47:04 +05:30
abe512b0c2 made updateSuggestionAsReviewed lambda api 2025-11-27 19:35:24 +05:30
326ea2f96b added transaction for calculating the pqq final answer score 2025-11-27 19:16:46 +05:30
b0bae33b6e Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-11-27 18:56:03 +05:30
030ec9225d updating the status to under review if the pqq overall score is more than 50% 2025-11-27 18:55:25 +05:30
paritosh18
83c3a39cc9 Refactor getAllHostActivityForAdmin handler: update paths and token verification method 2025-11-27 18:48:20 +05:30
paritosh18
6b0884b070 Merge branch 'mayank' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-11-27 17:27:03 +05:30
paritosh18
9e0f12117f Refactor host and minglaradmin handlers: update paths, rename functions, and add prepopulateRole handler for role retrieval 2025-11-27 17:26:33 +05:30
67da5b57e6 fixed the registration api 2025-11-27 16:56:28 +05:30
paritosh18
dcc0fbbcf6 Merge branch 'mayank' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-11-27 16:07:33 +05:30
fb392369a5 fixed the path of get score of pqq api 2025-11-27 16:07:20 +05:30
paritosh18
870aaca557 Refactor host and minglaradmin functions: rename handlers, update paths, and add new endpoints for activity and role management 2025-11-27 16:06:57 +05:30
62637ecc9a made minor fixes 2025-11-27 15:03:34 +05:30
f421a1e88a updated the path of import 2025-11-27 11:31:41 +05:30
7beebc16b3 Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-11-26 19:31:23 +05:30
paritosh18
6fa96c3fb0 Increase memory size for submitCompanyDetails and update package patterns in host and minglaradmin functions; refactor JWT middleware to use shared Prisma client instance 2025-11-26 19:30:57 +05:30
e63e906f62 fixed the path in host yml file 2025-11-26 19:21:49 +05:30
a01a93680a fixed the path in serverless for minglar endpoints 2025-11-26 18:53:10 +05:30
paritosh18
7be03b6c27 Refactor host API endpoint and integrate Prisma client
- Changed HTTP method for the /host/add-company-details endpoint from POST to PATCH in host.yml.
- Introduced a new Prisma client setup in prisma.client.ts for database interactions.
- Updated authForHost middleware to utilize the new Prisma client instance.
2025-11-26 18:04:26 +05:30
paritosh18
abae9d9ac2 Update Prisma dependencies and refactor host onboarding handlers
- Updated Prisma client and adapter versions in package.json and package-lock.json.
- Refactored host onboarding handlers to improve structure and naming conventions.
- Added new handlers for onboarding processes including login, signup, and OTP verification.
- Implemented new functionality for managing bank details and company submissions.
- Enhanced error handling and validation across various handlers.
2025-11-26 17:31:08 +05:30
a0ed3b9faa minglaradmin handlers folder structure made 2025-11-26 13:14:37 +05:30
14ddc94765 Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-11-26 12:00:59 +05:30
498c4cbe46 updated prisma version 2025-11-26 12:00:48 +05:30
paritosh18
d14948b344 smalll fixx of yml 2025-11-26 09:42:07 +05:30
paritosh18
c9b4269e9a Merge branch 'mayank' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-11-25 12:28:16 +05:30
930605e22e Enhance getAllOnboardingHostApplications to exclude hosts with DRAFT status 2025-11-25 12:26:01 +05:30
paritosh18
263d06949e Merge branch 'mayank' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-11-25 12:04:50 +05:30
8e25c5c4a2 made get all host applications for minglar admin and get all new host application for minglar admin 2025-11-25 12:02:15 +05:30
917a1f1ee9 made the pre populate of city and bank branch 2025-11-25 09:44:36 +05:30
paritosh18
089d022267 SMall FIx 2025-11-24 23:53:31 +05:30
paritosh18
d65f7f5368 resolved Merge 2025-11-24 23:25:20 +05:30
7056f32e24 made get score api for pqq 2025-11-24 23:19:18 +05:30
c4e470be05 Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-11-24 19:19:34 +05:30
paritosh18
fca4991577 Add filtering for account manager in getAllHostApplications based on user role 2025-11-24 19:19:14 +05:30
2ab046fbd2 fixed minor issues 2025-11-24 19:19:02 +05:30
paritosh18
12420f6b51 Enhance getAllHostApplications to support userStatus filtering and include additional host details in response 2025-11-24 17:09:22 +05:30
paritosh18
fbd3b12937 Remove created activity data from response in addActivity handler 2025-11-22 21:10:17 +05:30
paritosh18
1c83cc5910 Add addActivity handler and createActivity method in HostService
- Implemented addActivity handler to create new activities with validation.
- Added createActivity method in HostService to handle activity creation logic.
2025-11-22 20:48:45 +05:30
d21dcacd7b added necessary packages 2025-11-22 20:20:25 +05:30
38d3b4ca6a fixed the file path 2025-11-22 20:15:01 +05:30
d0fd8e6691 Implement validation for required fields in PQQ handlers and enhance file upload logic in submitPqqAns. Added error handling for missing activity and question IDs, and improved S3 file management with delete functionality for existing files. Updated HostService methods for better file handling and header management. 2025-11-22 20:05:43 +05:30
6d48eeb25b Add new endpoint to retrieve the latest PQQ question details and implement corresponding handler. Update HostService to include method for fetching latest question details with active status filtering. 2025-11-22 19:47:04 +05:30
b1623343c8 Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-11-22 19:37:30 +05:30
paritosh18
3fb6961e7c Merge branch 'mayank' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-11-22 19:36:32 +05:30
5e0ba4c8e8 Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-11-22 19:31:40 +05:30
1377e0ba9a made the getbyid pqq question details api 2025-11-22 19:30:16 +05:30
paritosh18
d0b2de3f18 Add new endpoints for activity types and frequencies, and implement email notifications for AM assignments 2025-11-22 19:25:07 +05:30
3b1aac921f Add new handlers for accepting and rejecting host applications, including email notifications. Updated serverless configuration with new function timeouts and added missing handlers. Refactored profile update logic to improve file upload handling and added user detail retrieval methods in the Minglar service. 2025-11-22 19:22:34 +05:30
e7c94a1b19 formatted the files 2025-11-22 12:09:37 +05:30
d351a632bf Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-11-22 12:00:07 +05:30
0d858f5411 made getall document country city and replacing the files on s3 when updating the company details sending mail to am or admin 2025-11-22 11:59:48 +05:30
paritosh18
15c85686c6 Refactor getAllCoadminAndAM to include active user filtering and host count aggregation 2025-11-22 11:33:55 +05:30
976c9fc686 fixed the display order in PQQ getAll api and fixed the get All admin co admin and am api service 2025-11-22 08:00:59 +05:30
4665cb7ca4 fixed the serverless file 2025-11-21 16:23:38 +05:30
1fea4f18f6 storing the assigned on date and time when am is assigned to host 2025-11-21 15:56:40 +05:30
29cc335a07 Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-11-21 15:54:51 +05:30
paritosh18
2ffb9ca242 Add companyName field and filter for profileImage in getAllHostApplications 2025-11-21 15:54:33 +05:30
577471e7f1 Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-11-21 15:39:51 +05:30
e21ffd08f1 made the presigned url middleware 2025-11-21 14:53:53 +05:30
paritosh18
76e2bd542a Refactor MINGLAR constants to use consistent single quotes and expand HOST_SUGGESTION_TITLES with additional fields 2025-11-21 13:32:03 +05:30
926ea67e41 fixed code 2025-11-21 13:31:41 +05:30
ee9c0027de defined the apis in serverless file 2025-11-21 12:28:47 +05:30
8f25bb87e6 made accept host application and reject host application apis 2025-11-21 12:27:23 +05:30
e366bb4a70 Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-11-21 12:11:44 +05:30
2c3b1af754 updating stepper on company details adding 2025-11-21 11:47:29 +05:30
paritosh18
4c17e1b269 Return initial step for non-existent host records instead of throwing an error 2025-11-20 18:37:26 +05:30
78592dafec removed unnecessary variable declaration 2025-11-20 17:45:15 +05:30
5dc0445a61 Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-11-20 17:33:54 +05:30
2ba9b7778b fixed the varchar length and migrated the prisma 2025-11-20 17:33:41 +05:30
paritosh18
3e12419985 Refactor invite teammate handler to use a single service method encapsulating user creation, revenue, and invite details within a transaction 2025-11-20 17:14:23 +05:30
041c28ea99 Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-11-20 17:00:16 +05:30
5f37455290 added varchar 2025-11-20 17:00:01 +05:30
paritosh18
00dd080ee0 Refactor registration handler to use a transaction for user creation and OTP generation; implement OTP storage and user role assignment 2025-11-20 16:57:10 +05:30
paritosh18
f61287a11d update transaction in invite teammates 2025-11-20 16:40:50 +05:30
paritosh18
1a0fe5768c Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-11-20 16:30:41 +05:30
e7234c29ad added some columns and fixed some code issues 2025-11-20 16:26:48 +05:30
6bbcb36b10 Add editAgreementDetails and acceptHostApplication handlers; update serverless.yml and Prisma schema 2025-11-20 15:23:15 +05:30
paritosh18
3a29e70b2b CHange path of of GetSuggestion 2025-11-19 18:47:00 +05:30
paritosh18
a3ac533d96 Merge branch 'mayank' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-11-19 17:25:38 +05:30
paritosh18
5a9d0d6155 GetSuggstion 2025-11-19 17:20:05 +05:30
13ffee5f7e made submit pqq answer for host api 2025-11-19 16:55:54 +05:30
99cbe55a70 made getall pqq question prepopulate 2025-11-19 15:13:30 +05:30
paritosh18
5e061f33a1 Merge branch 'mayank' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-11-19 13:33:54 +05:30
paritosh18
803a11cdda Add suggestion and getsuggestion 2025-11-19 13:33:32 +05:30
58053e061d fixed the category name in seeder 2025-11-19 13:32:57 +05:30
4ea9aa8afa made full excel data seeder for pqq 2025-11-19 13:25:47 +05:30
60e3a0e72e Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-11-19 13:01:11 +05:30
c544c2ac06 added more data in seeder 2025-11-19 13:01:00 +05:30
paritosh18
d65fc684db prisma 2025-11-19 13:00:25 +05:30
5fac8b0156 migration 2025-11-19 12:26:34 +05:30
4664eae896 made prepolulate api in serverless 2025-11-19 11:34:12 +05:30
844ebb4672 made 2 pqq categories seeder 2025-11-18 22:35:51 +05:30
74b1420b8a Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-11-18 20:53:30 +05:30
paritosh18
b1068e7106 Add displayOrder field to DocumentType model in schema.prisma 2025-11-18 20:17:05 +05:30
ee38391d1e add payment details modified and making seeder of pqq 2025-11-18 17:44:51 +05:30
05f97d33e7 Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-11-18 14:37:54 +05:30
b2bfed756d made am assign api 2025-11-18 14:36:32 +05:30
5506b5d5cc Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-11-17 19:06:15 +05:30
paritosh18
94143bd4d0 Merge branch 'mayank' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-11-17 19:06:12 +05:30
e42a98f723 Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-11-17 19:05:44 +05:30
paritosh18
c402c76096 feat: add search functionality to getAllHostApplications 2025-11-17 19:05:39 +05:30
db65abf693 fixed the add company details api 2025-11-17 19:05:26 +05:30
paritosh18
a54f61e8c8 Merge branch 'mayank' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-11-17 17:39:25 +05:30
7ed225a3f0 made get all co admin and am details 2025-11-17 15:28:22 +05:30
paritosh18
d24fe70046 refactor: update getAllHostApplications query to exclude specific roles 2025-11-17 15:08:41 +05:30
paritosh18
cb70b078e5 refactor: remove roleXid filter from getAllHostApplications query 2025-11-17 13:50:12 +05:30
paritosh18
508487c3a3 feat: enhance host application retrieval to filter by accepted Minglar invitations 2025-11-17 13:45:36 +05:30
paritosh18
399c520985 feat: add getAllHostApplication handler and service method for retrieving host applications 2025-11-17 13:33:51 +05:30
008bcf7c79 fixed the invitation status for minglar admin registration 2025-11-17 12:42:25 +05:30
ce4af2ea3b made getAllInvitation Details api 2025-11-17 12:17:06 +05:30
4ee0819051 integrated sending mail from brevo 2025-11-14 19:06:47 +05:30
paritosh18
54c024fc4f refactor: enhance inviteTeammate handler to utilize MinglarService for user management 2025-11-14 18:52:37 +05:30
e9496d3868 made email sending 2025-11-14 17:48:40 +05:30
paritosh18
d0e6629466 Merge branch 'mayank' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-11-14 17:33:13 +05:30
paritosh18
50878e6374 add inviteTeammate function to handle teammate invitations 2025-11-14 17:33:02 +05:30
5d7ce4455e Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-11-14 17:32:37 +05:30
8e91fee8ee made update profile for minglar admin 2025-11-14 17:22:07 +05:30
paritosh18
7e0f5b3162 rename endpoint to retrieve roles for Coadmin and Account_manager 2025-11-14 17:01:09 +05:30
paritosh18
d2045ae0b8 Merge branch 'mayank' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-11-14 16:58:57 +05:30
b2298e0fb1 fixed the middleware functions 2025-11-14 16:58:24 +05:30
paritosh18
f23ff39f2e Merge branch 'mayank' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-11-14 16:33:34 +05:30
paritosh18
d8f08bf564 add prepopulateTeammate function to retrieve roles for Coadmin and Account_manager 2025-11-14 16:33:14 +05:30
paritosh18
f8a405204f Merge branch 'mayank' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-11-14 15:30:47 +05:30
8dda773b90 fixed the condition in the registration of minglar admin 2025-11-14 15:30:42 +05:30
3e64e50385 fixed the condition for minglar login 2025-11-14 15:25:56 +05:30
paritosh18
ecb866c836 Merge branch 'mayank' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-11-14 15:23:08 +05:30
0353982ff4 fixed the file location of stepper 2025-11-14 15:16:53 +05:30
8c48db854e fixed the stepper endpoint 2025-11-14 15:15:13 +05:30
paritosh18
73a1f0b375 Merge branch 'mayank' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-11-14 15:04:44 +05:30
03c2e729a0 Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-11-14 15:04:26 +05:30
43a2a1c2b1 fixed the getbyid host endpoint 2025-11-14 15:04:01 +05:30
paritosh18
096613f1e2 path changes 2025-11-14 15:03:28 +05:30
6b3d2ae7cf added cors 2025-11-14 14:44:04 +05:30
fb9a92984b fixed the schema issues 2025-11-14 14:08:47 +05:30
paritosh18
224e62ef45 Merge branch 'mayank' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-11-13 20:41:13 +05:30
6f75d912eb added statuscode in safehandler 2025-11-13 20:37:07 +05:30
fb044a4535 added new condition for security 2025-11-13 19:45:28 +05:30
paritosh18
ac494455b5 id para change 2025-11-13 19:43:09 +05:30
8d1955f357 refactored serverless file and optimized it 2025-11-13 18:43:09 +05:30
8587016ad5 removed redundant aws package in unwanted functions 2025-11-13 18:10:58 +05:30
8f12151ef1 fixed some logics 2025-11-13 18:01:41 +05:30
477f8d229d Merge branch 'paritosh' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayank 2025-11-13 17:49:20 +05:30
3ac0a591df made add parent company details and upload supporting documents 2025-11-13 17:48:09 +05:30
paritosh18
2ed5435a15 resolvec 2025-11-13 16:04:20 +05:30
paritosh18
ceacdbddcb Merge branch 'mayank' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh 2025-11-13 16:00:18 +05:30
paritosh18
fb72feb20e add api 2025-11-13 15:56:13 +05:30
72f9e26ca6 made add company details api and moved it in the host folder 2025-11-13 15:53:35 +05:30
paritosh18
a14f1388f6 add host and minglar admin APIs for registration, login, and retrieval of host details 2025-11-13 14:59:50 +05:30
8e19bb566d made s3 uploader and apis 2025-11-12 19:59:54 +05:30
c0e58fe1ce made register and login apis for host 2025-11-12 16:03:57 +05:30
127 changed files with 23724 additions and 1642 deletions

8
.gitignore vendored
View File

@@ -44,6 +44,11 @@ lerna-debug.log*
# temp
.tmp
.temp
undefined/
# tsx cache/temp directories
**/tsx-*/
**/temp/tsx-*
# Runtime data
pids
@@ -86,6 +91,9 @@ web_modules/
# Optional npm cache directory
.npm
#package.lock.json
.package-lock.json
# Optional eslint cache
.eslintcache

115
CURL_EXAMPLE.md Normal file
View File

@@ -0,0 +1,115 @@
# CURL Command for Testing addCompanyDetails Lambda
## Prerequisites
1. Replace `YOUR_API_URL` with your actual API Gateway URL
2. Replace `YOUR_AUTH_TOKEN` with a valid JWT token
3. Replace file paths with actual document files on your system
## Form Data Structure
The endpoint expects:
- `companyDetails`: JSON string containing company information
- `documents`: JSON string containing array of document metadata
- File fields: One file field per document (e.g., `panFile`, `gstFile`, etc.)
## CURL Command
```bash
curl -X POST "YOUR_API_URL/minglaradmin/add-company-details" \
-H "x-auth-token: YOUR_AUTH_TOKEN" \
-F "companyDetails={\"companyName\":\"Test Company\",\"hostRefNumber\":\"HOST001\",\"address1\":\"123 Main St\",\"address2\":\"Suite 100\",\"cityXid\":1,\"stateXid\":1,\"countryXid\":1,\"pinCode\":\"12345\",\"isSubsidairy\":false,\"registrationNumber\":\"REG123456\",\"panNumber\":\"ABCDE1234F\",\"gstNumber\":\"27ABCDE1234F1Z5\",\"formationDate\":\"2020-01-01\",\"companyType\":\"Private Limited\",\"currencyXid\":1}" \
-F "documents=[{\"documentTypeXid\":1,\"documentName\":\"pan.pdf\",\"fieldName\":\"panFile\"},{\"documentTypeXid\":2,\"documentName\":\"gst.pdf\",\"fieldName\":\"gstFile\"},{\"documentTypeXid\":3,\"documentName\":\"registration.pdf\",\"fieldName\":\"registrationFile\"},{\"documentTypeXid\":4,\"documentName\":\"aadhaar.pdf\",\"fieldName\":\"aadhaarFile\"}]" \
-F "panFile=@/path/to/pan.pdf" \
-F "gstFile=@/path/to/gst.pdf" \
-F "registrationFile=@/path/to/registration.pdf" \
-F "aadhaarFile=@/path/to/aadhaar.pdf"
```
## Example with Real Values
```bash
curl -X POST "https://abc123.execute-api.ap-south-1.amazonaws.com/minglaradmin/add-company-details" \
-H "x-auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
-F "companyDetails={\"companyName\":\"Acme Corp\",\"hostRefNumber\":\"HOST001\",\"address1\":\"123 Business Park\",\"address2\":\"Building A\",\"cityXid\":1,\"stateXid\":1,\"countryXid\":1,\"pinCode\":\"400001\",\"isSubsidairy\":false,\"registrationNumber\":\"U12345MH2020PTC123456\",\"panNumber\":\"ABCDE1234F\",\"gstNumber\":\"27ABCDE1234F1Z5\",\"formationDate\":\"2020-01-15\",\"companyType\":\"Private Limited\",\"websiteUrl\":\"https://acme.com\",\"currencyXid\":1}" \
-F "documents=[{\"documentTypeXid\":1,\"documentName\":\"pan-certificate.pdf\",\"fieldName\":\"panFile\"},{\"documentTypeXid\":2,\"documentName\":\"gst-certificate.pdf\",\"fieldName\":\"gstFile\"},{\"documentTypeXid\":3,\"documentName\":\"registration-certificate.pdf\",\"fieldName\":\"registrationFile\"},{\"documentTypeXid\":4,\"documentName\":\"aadhaar.pdf\",\"fieldName\":\"aadhaarFile\"}]" \
-F "panFile=@./documents/pan.pdf" \
-F "gstFile=@./documents/gst.pdf" \
-F "registrationFile=@./documents/registration.pdf" \
-F "aadhaarFile=@./documents/aadhaar.pdf"
```
## Postman Setup
1. **Method**: POST
2. **URL**: `YOUR_API_URL/minglaradmin/add-company-details`
3. **Headers**:
- `x-auth-token`: `YOUR_AUTH_TOKEN`
4. **Body**: Select `form-data`
5. **Add Fields**:
- `companyDetails` (Text): JSON string with company details
- `documents` (Text): JSON array string with document metadata
- `panFile` (File): Select PDF file
- `gstFile` (File): Select PDF file
- `registrationFile` (File): Select PDF file
- `aadhaarFile` (File): Select PDF file
## Required Document Types
Based on `REQUIRED_DOC_TYPES`:
- `documentTypeXid: 1` - PAN
- `documentTypeXid: 2` - GST
- `documentTypeXid: 3` - REGISTRATION
- `documentTypeXid: 4` - AADHAAR
## Company Details JSON Structure
```json
{
"companyName": "string (required)",
"hostRefNumber": "string (required)",
"address1": "string (required)",
"address2": "string (optional)",
"cityXid": "number (required)",
"stateXid": "number (required)",
"countryXid": "number (required)",
"pinCode": "string (required, min 4 chars)",
"logoPath": "string (optional)",
"isSubsidairy": "boolean (required)",
"registrationNumber": "string (required)",
"panNumber": "string (required)",
"gstNumber": "string (optional)",
"formationDate": "string (required, ISO date)",
"companyType": "string (required)",
"websiteUrl": "string (optional, must be valid URL)",
"instagramUrl": "string (optional, must be valid URL)",
"facebookUrl": "string (optional, must be valid URL)",
"linkedinUrl": "string (optional, must be valid URL)",
"twitterUrl": "string (optional, must be valid URL)",
"currencyXid": "number (required)"
}
```
## Documents JSON Structure
```json
[
{
"documentTypeXid": 1,
"documentName": "pan.pdf",
"fieldName": "panFile"
},
{
"documentTypeXid": 2,
"documentName": "gst.pdf",
"fieldName": "gstFile"
}
]
```
## Notes
- All 4 required document types (PAN, GST, REGISTRATION, AADHAAR) must be provided
- File field names in the `documents` array must match the form field names
- Files can be PDF, images, or any other document type
- The endpoint supports both `multipart/form-data` and JSON (for backward compatibility)

46
build-prisma-layer.ps1 Normal file
View File

@@ -0,0 +1,46 @@
# build-prisma-layer.ps1
# Script to rebuild the Prisma layer for Lambda deployment
# Usage: .\build-prisma-layer.ps1
$ErrorActionPreference = "Stop"
$projectRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
$layerPath = Join-Path $projectRoot "layers\prisma\nodejs"
$nodeModulesPath = Join-Path $layerPath "node_modules"
Write-Host "🔄 Building Prisma layer for Lambda..." -ForegroundColor Cyan
# Step 1: Regenerate Prisma client
Write-Host "📦 Regenerating Prisma client..." -ForegroundColor Yellow
Push-Location $projectRoot
npx prisma generate
Pop-Location
# Step 2: Clean layer node_modules
Write-Host "🧹 Cleaning layer node_modules..." -ForegroundColor Yellow
if (Test-Path $nodeModulesPath) {
Remove-Item -Recurse -Force $nodeModulesPath
}
# Step 3: Install layer dependencies
Write-Host "📥 Installing layer dependencies..." -ForegroundColor Yellow
Push-Location $layerPath
npm install --production
Pop-Location
# Step 4: Copy generated Prisma client to layer
Write-Host "📋 Copying generated Prisma client to layer..." -ForegroundColor Yellow
$sourcePrisma = Join-Path $projectRoot "node_modules\.prisma\client"
$destPrisma = Join-Path $nodeModulesPath ".prisma\client"
New-Item -ItemType Directory -Force -Path (Split-Path $destPrisma) | Out-Null
Copy-Item -Path $sourcePrisma -Destination $destPrisma -Recurse -Force
# Step 5: Calculate layer size
$layerSize = (Get-ChildItem -Path $layerPath -Recurse -File | Measure-Object -Property Length -Sum).Sum / 1MB
Write-Host "✅ Layer built successfully!" -ForegroundColor Green
Write-Host "📊 Layer size: $([math]::Round($layerSize, 2)) MB" -ForegroundColor Cyan
# List contents
Write-Host "`n📁 Layer contents:" -ForegroundColor Cyan
Get-ChildItem $nodeModulesPath -Directory | ForEach-Object { Write-Host " - $($_.Name)" }

40
build-prisma-layer.sh Normal file
View File

@@ -0,0 +1,40 @@
#!/bin/bash
# build-prisma-layer.sh
# Script to rebuild the Prisma layer for Lambda deployment
# Usage: ./build-prisma-layer.sh
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LAYER_PATH="$SCRIPT_DIR/layers/prisma/nodejs"
echo "🔄 Building Prisma layer for Lambda..."
# Step 1: Regenerate Prisma client
echo "📦 Regenerating Prisma client..."
cd "$SCRIPT_DIR"
npx prisma generate
# Step 2: Clean layer node_modules
echo "🧹 Cleaning layer node_modules..."
rm -rf "$LAYER_PATH/node_modules"
# Step 3: Install layer dependencies
echo "📥 Installing layer dependencies..."
cd "$LAYER_PATH"
npm install --production
# Step 4: Copy generated Prisma client to layer
echo "📋 Copying generated Prisma client to layer..."
mkdir -p "$LAYER_PATH/node_modules/.prisma"
cp -r "$SCRIPT_DIR/node_modules/.prisma/client" "$LAYER_PATH/node_modules/.prisma/client"
# Step 5: Calculate layer size
LAYER_SIZE=$(du -sh "$LAYER_PATH" | cut -f1)
echo "✅ Layer built successfully!"
echo "📊 Layer size: $LAYER_SIZE"
# List contents
echo ""
echo "📁 Layer contents:"
ls -d "$LAYER_PATH/node_modules"/*/ 2>/dev/null | xargs -n 1 basename | sed 's/^/ - /'

228
layers/prisma/nodejs/package-lock.json generated Normal file
View File

@@ -0,0 +1,228 @@
{
"name": "prisma-layer",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "prisma-layer",
"version": "1.0.0",
"dependencies": {
"@prisma/adapter-pg": "^7.0.1",
"@prisma/client": "^7.0.1",
"pg": "^8.13.0"
}
},
"node_modules/@prisma/adapter-pg": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.0.1.tgz",
"integrity": "sha512-01GpPPhLMoDMF4ipgfZz0L87fla/TV/PBQcmHy+9vV1ml6gUoqF8dUIRNI5Yf2YKpOwzQg9sn8C7dYD1Yio9Ug==",
"license": "Apache-2.0",
"dependencies": {
"@prisma/driver-adapter-utils": "7.0.1",
"pg": "^8.16.3",
"postgres-array": "3.0.4"
}
},
"node_modules/@prisma/client": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.0.1.tgz",
"integrity": "sha512-O74T6xcfaGAq5gXwCAvfTLvI6fmC3and2g5yLRMkNjri1K8mSpEgclDNuUWs9xj5AwNEMQ88NeD3asI+sovm1g==",
"license": "Apache-2.0",
"dependencies": {
"@prisma/client-runtime-utils": "7.0.1"
},
"engines": {
"node": "^20.19 || ^22.12 || >=24.0"
},
"peerDependencies": {
"prisma": "*",
"typescript": ">=5.4.0"
},
"peerDependenciesMeta": {
"prisma": {
"optional": true
},
"typescript": {
"optional": true
}
}
},
"node_modules/@prisma/client-runtime-utils": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.0.1.tgz",
"integrity": "sha512-R26BVX9D/iw4toUmZKZf3jniM/9pMGHHdZN5LVP2L7HNiCQKNQQx/9LuMtjepbgRqSqQO3oHN0yzojHLnKTGEw==",
"license": "Apache-2.0"
},
"node_modules/@prisma/debug": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.0.1.tgz",
"integrity": "sha512-5+25XokVeAK2Z2C9W457AFw7Hk032Q3QI3G58KYKXPlpgxy+9FvV1+S1jqfJ2d4Nmq9LP/uACrM6OVhpJMSr8w==",
"license": "Apache-2.0"
},
"node_modules/@prisma/driver-adapter-utils": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.0.1.tgz",
"integrity": "sha512-sBbxm/yysHLLF2iMAB+qcX/nn3WFgsiC4DQNz0uM6BwGSIs8lIvgo0u8nR9nxe5gvFgKiIH8f4z2fgOEMeXc8w==",
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "7.0.1"
}
},
"node_modules/pg": {
"version": "8.16.3",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
"integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==",
"license": "MIT",
"dependencies": {
"pg-connection-string": "^2.9.1",
"pg-pool": "^3.10.1",
"pg-protocol": "^1.10.3",
"pg-types": "2.2.0",
"pgpass": "1.0.5"
},
"engines": {
"node": ">= 16.0.0"
},
"optionalDependencies": {
"pg-cloudflare": "^1.2.7"
},
"peerDependencies": {
"pg-native": ">=3.0.1"
},
"peerDependenciesMeta": {
"pg-native": {
"optional": true
}
}
},
"node_modules/pg-cloudflare": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz",
"integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==",
"license": "MIT",
"optional": true
},
"node_modules/pg-connection-string": {
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz",
"integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==",
"license": "MIT"
},
"node_modules/pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
"license": "ISC",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/pg-pool": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz",
"integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==",
"license": "MIT",
"peerDependencies": {
"pg": ">=8.0"
}
},
"node_modules/pg-protocol": {
"version": "1.10.3",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz",
"integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==",
"license": "MIT"
},
"node_modules/pg-types": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
"license": "MIT",
"dependencies": {
"pg-int8": "1.0.1",
"postgres-array": "~2.0.0",
"postgres-bytea": "~1.0.0",
"postgres-date": "~1.0.4",
"postgres-interval": "^1.1.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/pg-types/node_modules/postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/pgpass": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
"license": "MIT",
"dependencies": {
"split2": "^4.1.0"
}
},
"node_modules/postgres-array": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz",
"integrity": "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==",
"license": "MIT",
"engines": {
"node": ">=12"
}
},
"node_modules/postgres-bytea": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-date": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-interval": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"license": "MIT",
"dependencies": {
"xtend": "^4.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
"license": "ISC",
"engines": {
"node": ">= 10.x"
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"license": "MIT",
"engines": {
"node": ">=0.4"
}
}
}
}

View File

@@ -0,0 +1,10 @@
{
"name": "prisma-layer",
"version": "1.0.0",
"description": "Lambda layer for Prisma 7 with pg driver adapter",
"dependencies": {
"@prisma/client": "^7.0.1",
"@prisma/adapter-pg": "^7.0.1",
"pg": "^8.13.0"
}
}

8284
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -22,9 +22,18 @@
"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",
"build:layer": "powershell -ExecutionPolicy Bypass -File ./build-prisma-layer.ps1",
"build:layer:unix": "chmod +x ./build-prisma-layer.sh && ./build-prisma-layer.sh"
},
"dependencies": {
"@aws-crypto/crc32c": "^5.2.0",
"@aws-crypto/sha256-browser": "^5.2.0",
"@aws-crypto/sha256-js": "^5.2.0",
"@aws-sdk/client-s3": "^3.928.0",
"@aws-sdk/s3-request-presigner": "^3.310.0",
"@aws/lambda-invoke-store": "^0.2.1",
"@nestjs/common": "^10.3.0",
"@nestjs/config": "^3.1.1",
"@nestjs/core": "^10.3.0",
@@ -33,23 +42,35 @@
"@nestjs/platform-express": "^10.3.0",
"@nestjs/swagger": "^7.1.17",
"@nestjs/throttler": "^5.1.1",
"@prisma/client": "^5.8.1",
"@prisma/adapter-pg": "^7.0.1",
"@prisma/client": "^7.0.1",
"@smithy/middleware-stack": "^4.2.5",
"@smithy/protocol-http": "^5.3.5",
"@smithy/types": "^4.9.0",
"@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",
"fast-xml-parser": "^5.3.1",
"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",
"prisma": "^7.0.1",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
"serverless": "4.17.0",
"swagger-ui-express": "^5.0.0",
"yup": "^1.7.1"
"tslib": "^2.8.1",
"uuid": "^13.0.0",
"yup": "^1.7.1",
"zod": "^4.1.12"
},
"devDependencies": {
"@nestjs/cli": "^10.3.0",
@@ -69,14 +90,15 @@
"eslint-plugin-prettier": "^5.1.3",
"jest": "^29.7.0",
"prettier": "^3.2.5",
"prisma": "^5.8.1",
"serverless-esbuild": "^1.55.1",
"serverless-offline": "^14.4.0",
"source-map-support": "^0.5.21",
"supertest": "^6.3.4",
"ts-jest": "^29.1.2",
"ts-loader": "^9.5.1",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"tsx": "^4.20.6",
"typescript": "^5.3.3"
},
"jest": {

12
prisma.config.ts Normal file
View File

@@ -0,0 +1,12 @@
import 'dotenv/config'
import { defineConfig, env } from 'prisma/config'
export default defineConfig({
schema: 'prisma/schema.prisma',
migrations: {
path: 'prisma/migrations',
},
datasource: {
url: env('DATABASE_URL'),
},
})

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"

10
prisma/prisma.ts Normal file
View File

@@ -0,0 +1,10 @@
// prisma.ts
import { PrismaClient } from '@prisma/client';
// The DATABASE_URL environment variable will be automatically used
export const prisma = new PrismaClient();
process.on('SIGINT', async () => {
await prisma.$disconnect();
process.exit(0);
});

File diff suppressed because it is too large Load Diff

1180
prisma/seed.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,27 @@
service: minglarDev
service: minglar
provider:
name: aws
runtime: nodejs20.x
runtime: nodejs22.x
region: ap-south-1
versionFunctions: false
memorySize: 512
# Apply Prisma layer to all functions
# Use the published layer version ARN (works for full deploy and `deploy function`)
layers:
- ${cf:${self:service}-${sls:stage}.PrismaLambdaLayerQualifiedArn}
apiGateway:
binaryMediaTypes:
- '*/*'
minimumCompressionSize: 1024
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}
@@ -18,56 +33,79 @@ provider:
BREVO_SMTP_PASS: ${env:BREVO_SMTP_PASS}
REFRESH_TOKEN_SECRET: ${env:REFRESH_TOKEN_SECRET}
JWT_SECRET: ${env:JWT_SECRET}
JWT_ACCESS_EXPIRATION_MINUTES: ${env:JWT_ACCESS_EXPIRATION_MINUTES}
JWT_REFRESH_EXPIRATION_DAYS: ${env:JWT_REFRESH_EXPIRATION_DAYS}
JWT_RESET_PASSWORD_EXPIRATION_MINUTES: ${env:JWT_RESET_PASSWORD_EXPIRATION_MINUTES}
JWT_VERIFY_EMAIL_EXPIRATION_MINUTES: ${env:JWT_VERIFY_EMAIL_EXPIRATION_MINUTES}
SALT_ROUNDS: ${env:SALT_ROUNDS}
NODE_ENV: ${env:NODE_ENV}
S3_BUCKET_NAME: ${env:S3_BUCKET_NAME}
MINGLAR_ADMIN_NAME: ${env:MINGLAR_ADMIN_NAME}
MINGLAR_ADMIN_EMAIL: ${env:MINGLAR_ADMIN_EMAIL}
httpApi:
cors:
allowedOrigins: ['*']
allowedHeaders:
- Content-Type
- X-Amz-Date
- Authorization
- X-Api-Key
- X-Auth-Token
allowCredentials: false
iam:
role:
statements:
- Effect: Allow
Action:
- s3:PutObject
- s3:GetObject
- s3:DeleteObject
- s3:ListBucket
Resource:
- 'arn:aws:s3:::${env:S3_BUCKET_NAME}'
- 'arn:aws:s3:::${env:S3_BUCKET_NAME}/*'
custom:
serverless-offline:
reloadHandler: true
build:
esbuild:
bundle: true
minify: false
minify: true
sourcemap: false
exclude: ['aws-sdk']
target: node20
platform: node
concurrency: 10
outdir: dist
external:
# These are provided by the Prisma layer
- '@prisma/client'
- '@prisma/adapter-pg'
- '.prisma'
- 'pg'
exclude:
- 'aws-sdk'
# Define layers
layers:
prisma:
path: layers/prisma
name: ${self:service}-prisma-layer-${sls:stage}
description: Prisma 7 client with pg driver adapter (no binary engines)
compatibleRuntimes:
- nodejs22.x
retain: false
package:
individually: true
patterns:
- '!node_modules/**' # exclude all node_modules first
- '!**/*.spec.ts'
- '!**/*.test.ts'
- '!**/*.log'
- 'src/**' # include all source files
- 'common/**' # include common modules
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/**'
- 'prisma/schema.prisma'
- '!node_modules/**'
- '!**/*.test.js'
- '!**/*.spec.js'
- '!**/test/**'
- '!**/__tests__/**'
- '!package-lock.json'
- '!yarn.lock'
- '!README.md'
- '!*.config.js'
- '!.git/**'
- '!.github/**'
# Import function definitions from separate files organized by module
functions:
# 👇 Example Lambda for Host Module
- ${file(./serverless/functions/host.yml)}
- ${file(./serverless/functions/minglaradmin.yml)}
- ${file(./serverless/functions/prepopulate.yml)}
getHosts:
handler: src/modules/host/handlers/host.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
method: get
plugins:
- serverless-offline

764
serverless.yml.backup Normal file
View File

@@ -0,0 +1,764 @@
service: minglarDev
provider:
name: aws
runtime: nodejs22.x
region: ap-south-1
versionFunctions: false
memorySize: 512 # Default memory for all functions (can be overridden per function)
apiGateway:
binaryMediaTypes:
- '*/*'
minimumCompressionSize: 1024
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}
BREVO_API_BASEURL: ${env:BREVO_API_BASEURL}
BREVO_FROM_EMAIL: ${env:BREVO_FROM_EMAIL}
BREVO_SMTP_HOST: ${env:BREVO_SMTP_HOST}
BREVO_SMTP_PORT: ${env:BREVO_SMTP_PORT}
BREVO_SMTP_USER: ${env:BREVO_SMTP_USER}
BREVO_SMTP_PASS: ${env:BREVO_SMTP_PASS}
REFRESH_TOKEN_SECRET: ${env:REFRESH_TOKEN_SECRET}
JWT_SECRET: ${env:JWT_SECRET}
SALT_ROUNDS: ${env:SALT_ROUNDS}
NODE_ENV: ${env:NODE_ENV}
S3_BUCKET_NAME: ${env:S3_BUCKET_NAME}
MINGLAR_ADMIN_NAME: ${env:MINGLAR_ADMIN_NAME}
MINGLAR_ADMIN_EMAIL: ${env:MINGLAR_ADMIN_EMAIL}
iam:
role:
statements:
- Effect: Allow
Action:
- s3:PutObject
- s3:GetObject
- s3:DeleteObject
- s3:ListBucket
Resource:
- 'arn:aws:s3:::${env:S3_BUCKET_NAME}'
- 'arn:aws:s3:::${env:S3_BUCKET_NAME}/*'
custom:
esbuild:
bundle: true
minify: true
sourcemap: false
target: node20
platform: node
concurrency: 5
external:
- '@prisma/client'
- '.prisma'
exclude:
- 'aws-sdk'
package:
individually: true
patterns:
- '!node_modules/**'
- '!**/*.test.js'
- '!**/*.spec.js'
- '!**/test/**'
- '!**/__tests__/**'
- '!package-lock.json'
- '!yarn.lock'
- '!README.md'
- '!*.config.js'
- '!.git/**'
- '!.github/**'
functions:
getHosts:
handler: src/modules/host/handlers/host.handler
memorySize: 384 # Lower memory for simple GET operations
package:
patterns:
- 'src/modules/host/handlers/host.*'
- 'src/modules/host/services/**'
- 'src/common/**'
- 'common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /host
method: get
verifyOtp:
handler: src/modules/host/handlers/verifyOtp.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/verifyOtp.*'
- 'src/modules/host/services/**'
- 'src/common/**'
- 'common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /host/verify-otp
method: post
loginForHost:
handler: src/modules/host/handlers/loginForHost.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/loginForHost.*'
- 'src/modules/host/services/**'
- 'src/common/**'
- 'common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /host/login
method: post
registrationOfHost:
handler: src/modules/host/handlers/registration.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/registration.*'
- 'src/modules/host/services/**'
- 'src/common/**'
- 'common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /host/registration
method: post
createPasswordForHost:
handler: src/modules/host/handlers/createPassword.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/createPassword.*'
- 'src/modules/host/services/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /host/create-password
method: post
addPaymentDetailsForHost:
handler: src/modules/host/handlers/addPaymentDetails.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/addPaymentDetails.*'
- 'src/modules/host/services/**'
- 'src/common/**'
- 'common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /host/add-payment-details
method: post
addActivity:
handler: src/modules/host/handlers/addActivity.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/addActivity.*'
- 'src/modules/host/services/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /host/add-activity
method: post
getHostById:
handler: src/modules/host/handlers/getbyidhandler.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/getbyidhandler.*'
- 'src/modules/host/services/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /host/getById
method: get
getPQQQuestionDetailsById:
handler: src/modules/host/handlers/getByIdPQQ.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/getByIdPQQ.*'
- 'src/modules/host/services/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /host/get-pqq-question-details
method: get
getLatestPQQQuestionDetails:
handler: src/modules/host/handlers/getLatestQuestionDetailsPQQ.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/getLatestQuestionDetailsPQQ.*'
- 'src/modules/host/services/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /host/get-latest-pqq-question-details
method: get
getActivityTypes:
handler: src/modules/host/handlers/getActivity.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/getActivity.*'
- 'src/modules/host/services/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /host/get-activity
method: get
acceptMinglarAgreement:
handler: src/modules/host/handlers/acceptAgreement.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/acceptAgreement.*'
- 'src/modules/host/services/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /host/accept-agreement
method: patch
getStepperInfo:
handler: src/modules/host/handlers/getStepper.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/getStepper.*'
- 'src/common/utils/handlers/safeHandler.*'
- 'src/common/database/**'
- 'src/modules/host/services/**'
- 'common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /stepper
method: get
getSuggestion:
handler: src/modules/minglaradmin/handlers/getSuggestion.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /minglaradmin/get-suggestion
method: get
minglarRegistration:
handler: src/modules/minglaradmin/handlers/registration.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /minglaradmin/registration
method: post
minglarLoginForAdmin:
handler: src/modules/minglaradmin/handlers/loginForMinglar.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /minglaradmin/login
method: post
minglarCreatePassword:
handler: src/modules/minglaradmin/handlers/createPassword.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /minglaradmin/create-password
method: post
# Functions using AWS SDK - KEEP AS IS with higher memory
updateMinglarProfile:
handler: src/modules/minglaradmin/handlers/updateProfile.handler
memorySize: 512 # Higher memory for AWS SDK operations
timeout: 30
package:
patterns:
- 'src/modules/minglaradmin/handlers/updateProfile.*'
- 'src/modules/minglaradmin/services/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
- 'node_modules/@aws-sdk/**'
- 'node_modules/@smithy/**'
- 'node_modules/tslib/**'
- 'node_modules/fast-xml-parser/**'
events:
- httpApi:
path: /minglaradmin/update-profile
method: patch
prepopulateTeammate:
handler: src/modules/minglaradmin/handlers/prepopulateTeammate.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /minglaradmin/prepopulate-Roles
method: get
inviteTeammate:
handler: src/modules/minglaradmin/handlers/inviteTeammate.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /minglaradmin/invite-teammate
method: post
getAllHostApplication:
handler: src/modules/minglaradmin/handlers/getAllHostApplication.handler
memorySize: 512 # Higher memory for data-intensive operations
package:
patterns:
- 'src/modules/minglaradmin/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /minglaradmin/get-all-host-applications-am
method: get
getAllOnboardingHostApplications:
handler: src/modules/minglaradmin/handlers/getAllOnboardingHosts.handler
memorySize: 512 # Higher memory for data-intensive operations
package:
patterns:
- 'src/modules/minglaradmin/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /minglaradmin/get-all-host-applications-admin
method: get
getAllOnboardingHostApplications_New:
handler: src/modules/minglaradmin/handlers/getOnboardingNewApplications.handler
memorySize: 512 # Higher memory for data-intensive operations
package:
patterns:
- 'src/modules/minglaradmin/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /minglaradmin/get-all-host-applications-admin-new
method: get
getAllInvitationDetails:
handler: src/modules/minglaradmin/handlers/getAllInvitationDetails.handler
memorySize: 512
package:
patterns:
- 'src/modules/minglaradmin/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /minglaradmin/get-all-invitation-details
method: get
addSuggestion:
handler: src/modules/minglaradmin/handlers/addSuggestion.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /minglaradmin/add-suggestion
method: post
getAllCoadminAndAMDetails:
handler: src/modules/minglaradmin/handlers/getAllCoadminAndAM.handler
memorySize: 512
package:
patterns:
- 'src/modules/minglaradmin/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /minglaradmin/get-all-coadmin-and-am-details
method: get
getAllInvitedCoadminAndAMDetails:
handler: src/modules/minglaradmin/handlers/getAllInvitedCoadminAndAM.handler
memorySize: 512
package:
patterns:
- 'src/modules/minglaradmin/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /minglaradmin/get-all-invited-coadmin-and-am
method: get
getAllBankAndCurrencyDetails:
handler: src/modules/prepopulate/handlers/getAllBankDetails.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /prepopulate/get-all-bank-currency-details
method: get
getCityByState:
handler: src/modules/prepopulate/handlers/getCityByState.handler
memorySize: 384
package:
patterns:
- 'src/modules/prepopulate/handlers/getCityByState.*'
- 'src/modules/prepopulate/services/**'
- 'src/common/**'
- 'common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /prepopulate/get-city-by-state
method: get
getBranchByBankXid:
handler: src/modules/prepopulate/handlers/getBranchByBank.handler
memorySize: 384
package:
patterns:
- 'src/modules/prepopulate/handlers/getBranchByBank.*'
- 'src/modules/prepopulate/services/**'
- 'src/common/**'
- 'common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /prepopulate/get-branch-by-bank
method: get
getAllDocumentCountryStateCityDetails:
handler: src/modules/prepopulate/handlers/getAllDocTypeWithCountryState.handler
memorySize: 512
package:
patterns:
- 'src/modules/minglaradmin/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /prepopulate/get-all-doc-country
method: get
getAllPqqQuesAns:
handler: src/modules/prepopulate/handlers/getAllPQQQuesWithAns.handler
memorySize: 512
package:
patterns:
- 'src/modules/minglaradmin/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /prepopulate/get-all-pqq-ques-ans
method: get
getFrequenciesOfActivity:
handler: src/modules/prepopulate/handlers/getAllFrequencies.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /prepopulate/get-all-Frequencies
method: get
assignAMToHost:
handler: src/modules/minglaradmin/handlers/assignAM.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /minglaradmin/assign-am-to-host
method: patch
editAgreementDetails:
handler: src/modules/minglaradmin/handlers/editAgreementDetails.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /minglaradmin/edit-agreement-details
method: patch
acceptHostApplication:
handler: src/modules/minglaradmin/handlers/acceptHostApplication.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /minglaradmin/accept-host-application
method: patch
acceptHostApplicationMinglar:
handler: src/modules/minglaradmin/handlers/acceptHostAppMinglar.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /minglaradmin/accept-host-application-minglar
method: patch
rejectHostApplication:
handler: src/modules/minglaradmin/handlers/rejectHostApplication.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /minglaradmin/reject-host-application
method: patch
rejectHostApplicationAM:
handler: src/modules/minglaradmin/handlers/rejectHostApplicationAM.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
events:
- httpApi:
path: /minglaradmin/reject-host-application-am
method: patch
# Functions using AWS SDK and S3 - KEEP AS IS with higher memory
addCompanyDetails:
handler: src/modules/host/handlers/addCompanyDetails.handler
memorySize: 512
timeout: 30
package:
patterns:
- 'src/modules/host/handlers/addCompanyDetails.*'
- 'src/modules/host/services/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
# Only include specific AWS SDK modules needed for S3
- 'node_modules/@aws-sdk/client-s3/**'
- 'node_modules/@aws-sdk/s3-request-presigner/**'
- 'node_modules/@aws-sdk/types/**'
- 'node_modules/@aws-sdk/middleware-logger/**'
- 'node_modules/@aws-sdk/util-utf8-node/**'
- 'node_modules/@aws-sdk/util-utf8-browser/**'
- 'node_modules/@smithy/**'
- 'node_modules/tslib/**'
# Remove these large/unnecessary packages:
- 'node_modules/fast-xml-parser/**' # Remove if not used
- 'node_modules/lambda-multipart-parser/**' # You're using busboy directly
- 'node_modules/busboy/**'
# Remove these AWS utility packages (included in main SDK):
- 'node_modules/@aws-crypto/**'
# - 'node_modules/uuid/**' # AWS SDK includes its own
# - 'node_modules/@aws/util-uri-escape/**'
# - 'node_modules/@aws/util-middleware/**'
- 'node_modules/@aws/smithy-client/**'
# - 'node_modules/@aws/lambda-invoke-store/**'
events:
- httpApi:
path: /host/add-company-details
method: patch
submitPqqAnswer:
handler: src/modules/host/handlers/submitPqqAns.handler
memorySize: 512
timeout: 30
package:
patterns:
- 'src/modules/host/handlers/submitPqqAns.*'
- 'src/modules/host/services/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
- 'node_modules/@aws-sdk/**'
- 'node_modules/@smithy/**'
- 'node_modules/tslib/**'
- 'node_modules/fast-xml-parser/**'
- 'node_modules/lambda-multipart-parser/**'
- 'node_modules/busboy/**'
- 'node_modules/@aws-crypto/**'
- 'node_modules/uuid/**'
- 'node_modules/@aws/util-uri-escape/**'
- 'node_modules/@aws/util-middleware/**'
- 'node_modules/@aws/smithy-client/**'
- 'node_modules/@aws/lambda-invoke-store/**'
events:
- httpApi:
path: /host/submit-pqq-ans
method: patch
submitFinalPqqAnswer:
handler: src/modules/host/handlers/getPQQScore.handler
memorySize: 512
timeout: 30
package:
patterns:
- 'src/modules/host/handlers/getPQQScore.*'
- 'src/modules/host/services/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-openssl-3.0.x.so.node'
- 'node_modules/@aws-sdk/**'
- 'node_modules/@smithy/**'
- 'node_modules/tslib/**'
- 'node_modules/fast-xml-parser/**'
- 'node_modules/lambda-multipart-parser/**'
- 'node_modules/busboy/**'
- 'node_modules/@aws-crypto/**'
- 'node_modules/uuid/**'
- 'node_modules/@aws/util-uri-escape/**'
- 'node_modules/@aws/util-middleware/**'
- 'node_modules/@aws/smithy-client/**'
- 'node_modules/@aws/lambda-invoke-store/**'
events:
- httpApi:
path: /host/submit-final-pqq-ans
method: patch
addPQQSuggestion:
handler: src/modules/minglar/handlers/addPQQSuggestion.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/handlers/addPQQSuggestion.*'
- 'src/modules/minglaradmin/services/**'
- 'src/common/**'
- 'node_modules/@prisma/client/**'
- 'node_modules/.prisma/**'
events:
- httpApi:
path: /minglar/add-Pqq-suggestion
method: post

View File

@@ -0,0 +1,680 @@
# Host Module Functions
# All authentication and host management endpoints
getHosts:
handler: src/modules/host/handlers/host.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/host.*'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host
method: get
verifyOTP:
handler: src/modules/host/handlers/Host_Admin/onboarding/verifyOTP.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/verifyOtp.*'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Host_Admin/onboarding/verify-otp
method: post
login:
handler: src/modules/host/handlers/Host_Admin/onboarding/login.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/loginForHost.*'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Host_Admin/onboarding/login
method: post
signUp:
handler: src/modules/host/handlers/Host_Admin/onboarding/signUp.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/registration.*'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Host_Admin/onboarding/registration
method: post
createPassword:
handler: src/modules/host/handlers/Host_Admin/onboarding/createPassword.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/createPassword.*'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Host_Admin/onboarding/create-password
method: post
updateBankDetails:
handler: src/modules/host/handlers/Host_Admin/onboarding/updateBankDetails.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/addPaymentDetails.*'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Host_Admin/onboarding/add-payment-details
method: post
saveActivity_ForPQQ:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/saveActivity_ForPQQ.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/addActivity.*'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Activity_Hub/OnBoarding/add-activity
method: post
getHostById:
handler: src/modules/host/handlers/getbyidhandler.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/getbyidhandler.*'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/getById
method: get
getPQQ_ByQuestionId:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/getPQQ_ByQuestionId.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/getByIdPQQ.*'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Activity_Hub/OnBoarding/get-pqq-question-details
method: get
getPQQ_LastUpdatedQuestion:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/getPQQ_LastUpdatedQuestion.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/getLatestQuestionDetailsPQQ.*'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Activity_Hub/OnBoarding/get-latest-pqq-question-details
method: get
getAllActivityType:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/getAllActivityType.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/getActivityType.*'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Activity_Hub/OnBoarding/get-activity-type
method: get
showSuggestion:
handler: src/modules/host/handlers/Host_Admin/onboarding/showSuggestion.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/Host_Admin/onboarding/showSuggestion.handler.*'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/get-suggestion
method: get
getAllHostActivity:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/getAllHostActivity.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/Activity_Hub/OnBoarding/getAllHostActivity.*'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Activity_Hub/OnBoarding/get-all-host-activity
method: get
acceptAggrement:
handler: src/modules/host/handlers/Host_Admin/onboarding/acceptAggrement.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/acceptAgreement.*'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Host_Admin/onboarding/accept-agreement
method: patch
getStepperInfo:
handler: src/modules/host/handlers/getStepper.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/getStepper.*'
- 'src/common/utils/handlers/safeHandler.*'
- 'src/common/database/**'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /stepper
method: get
# Functions with S3/AWS SDK dependencies
submitCompanyDetails:
handler: src/modules/host/handlers/Host_Admin/onboarding/submitCompanyDetails.handler
memorySize: 1024
timeout: 30
package:
patterns:
- 'src/modules/host/handlers/addCompanyDetails.*'
- 'src/modules/host/services/**'
- 'src/common/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Host_Admin/onboarding/add-company-details
method: patch
submitPQQ_Answer:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/submitPQQ_Answer.handler
memorySize: 1024
package:
patterns:
- 'src/modules/host/handlers/submitPqqAns.*'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Activity_Hub/OnBoarding/submit-pqq-answer
method: patch
updatePQQ_LastAnswer:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/getPQQScore.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/submitPqqAns.*'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Activity_Hub/OnBoarding/submit-final-pqq-answer
method: post
getAllPQQwithSubmittedAns:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/getAllPQQwithSubmittedAns.handler
memorySize: 512
package:
patterns:
- 'src/modules/prepopulate/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Activity_Hub/OnBoarding/get-all-pqq-ques-submited-ans
method: get
updateSuggestionAsReviewed:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/updateSuggestionAsReviewed.handler
memorySize: 512
package:
patterns:
- 'src/modules/host/handlers/Activity_Hub/OnBoarding/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Activity_Hub/OnBoarding/update-suggestion-reviewed
method: patch
resendOTPmail:
handler: src/modules/host/handlers/resendOtp.handler
memorySize: 512
package:
patterns:
- 'src/modules/host/handlers/resendOtp/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /resend-otp
method: post

View File

@@ -0,0 +1,380 @@
# Minglar Admin Module Functions
# Admin dashboard and management endpoints
minglarRegistration:
handler: src/modules/minglaradmin/handlers/registration.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/registration
method: post
minglarLoginForAdmin:
handler: src/modules/minglaradmin/handlers/loginForMinglar.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/login
method: post
minglarCreatePassword:
handler: src/modules/minglaradmin/handlers/createPassword.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/create-password
method: post
updateMinglarProfile:
handler: src/modules/minglaradmin/handlers/updateProfile.handler
memorySize: 1024
timeout: 30
package:
patterns:
- 'src/modules/minglaradmin/handlers/updateProfile.*'
- 'src/modules/minglaradmin/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/update-profile
method: patch
prepopulateRole:
handler: src/modules/minglaradmin/handlers/prepopulateRole.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/prepopulate-Roles
method: get
getHostDetailsById:
handler: src/modules/minglaradmin/handlers/hosthub/hosts/getByIdHostDetails.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/handlers/hosthub/hosts/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/hosts/get-host-details/{host_xid}
method: get
inviteTeammate:
handler: src/modules/minglaradmin/handlers/settings/teammates/inviteTeammate.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/handlers/settings/teammates/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/settings/teammates/invite-teammate
method: post
getAllHostApplication:
handler: src/modules/minglaradmin/handlers/hosthub/hosts/getAllHostApplicationForAM.handler
memorySize: 512
package:
patterns:
- 'src/modules/minglaradmin/handlers/hosthub/hosts/**'
- 'src/modules/minglaradmin/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/hosts/get-all-host-applications-am
method: get
getAllHostActivityForAdmin:
handler: src/modules/minglaradmin/handlers/hosthub/onboarding/getAllActivityOfHost.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/handlers/hosthub/onboarding/getAllActivityOfHost.handler.*'
- 'src/modules/minglaradmin/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/get-all-activity-of-host/{id}
method: get
getAllOnboardingHostApplications:
handler: src/modules/minglaradmin/handlers/hosthub/onboarding/getAllOnboardingHosts.handler
memorySize: 512
package:
patterns:
- 'src/modules/minglaradmin/handlers/hosthub/onboarding/**'
- 'src/modules/minglaradmin/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/onboarding/get-all-host-applications-admin
method: get
getAllOnboardingHostApplications_New:
handler: src/modules/minglaradmin/handlers/hosthub/onboarding/getOnboardingNewApplications.handler
memorySize: 512
package:
patterns:
- 'src/modules/minglaradmin/handlers/hosthub/onboarding/**'
- 'src/modules/minglaradmin/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/onboarding/get-all-host-applications-admin-new
method: get
getAllInvitationDetails:
handler: src/modules/minglaradmin/handlers/settings/teammates/getAllInvitationDetails.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/handlers/settings/teammates**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/settings/teammates/get-all-invitation-details
method: get
addSuggestion:
handler: src/modules/minglaradmin/handlers/hosthub/hosts/addSuggestion.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/handlers/hosthub/hosts/**'
- 'src/modules/minglaradmin/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/hosts/add-suggestion
method: post
getAllCoadminAndAMDetails:
handler: src/modules/minglaradmin/handlers/settings/teammates/getAllCoadminAndAM.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/handlers/settings/teammates/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/settings/teammates/get-all-coadmin-am
method: get
getAllInvitedCoadminAndAMDetails:
handler: src/modules/minglaradmin/handlers/settings/teammates/getAllInvitedCoadminAndAM.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/handlers/settings/teammates/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/settings/teammates/get-all-invited-coadmin-am
method: get
getAmDetailsbyId:
handler: src/modules/minglaradmin/handlers/getAmDetail_ById.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/handlers/settings/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/settings/teammates/get-am-details-by-id/{amXid}
method: get
assignAMToHost:
handler: src/modules/minglaradmin/handlers/hosthub/onboarding/assignAM.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/handlers/hosthub/onboarding/**'
- 'src/modules/minglaradmin/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/onboarding/assign-am
method: patch
editAgreementDetails:
handler: src/modules/minglaradmin/handlers/hosthub/onboarding/editAgreementDetails.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/handlers/hosthub/onboarding/**'
- 'src/modules/minglaradmin/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/onboarding/edit-agreement
method: patch
acceptHostApplication:
handler: src/modules/minglaradmin/handlers/hosthub/hosts/acceptHostApplication.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/handlers/hosthub/hosts/**'
- 'src/modules/minglaradmin/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/hosts/accept-host-application
method: patch
RejectPQQByAM:
handler: src/modules/minglaradmin/handlers/hosthub/hosts/rejectPQQbyAM.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/handlers/hosthub/hosts/**'
- 'src/modules/minglaradmin/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/hosts/reject-pqq-by-am
method: patch
acceptHostApplicationMinglar:
handler: src/modules/minglaradmin/handlers/hosthub/onboarding/acceptHostAppMinglar.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/handlers/hosthub/onboarding/**'
- 'src/modules/minglaradmin/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/onboarding/accept-host-application-minglar
method: patch
rejectHostApplication:
handler: src/modules/minglaradmin/handlers/hosthub/onboarding/rejectHostApplication.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/handlers/hosthub/onboarding/**'
- 'src/modules/minglaradmin/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/onboarding/reject-host-application
method: post
rejectHostApplicationAM:
handler: src/modules/minglaradmin/handlers/hosthub/hosts/rejectHostApplicationAM.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/handlers/hosthub/hosts/**'
- 'src/modules/minglaradmin/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/hosts/reject-host-application-am
method: post
addPQQSuggestion:
handler: src/modules/minglaradmin/handlers/hosthub/hosts/addPQQSuggestion.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/handlers/hosthub/hosts/**'
- 'src/modules/minglaradmin/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/hosts/add-Pqq-suggestion
method: post

View File

@@ -0,0 +1,94 @@
# Prepopulate Module Functions
# Reference data and lookup endpoints
getAllBankAndCurrencyDetails:
handler: src/modules/prepopulate/handlers/getAllBankDetails.handler
memorySize: 384
package:
patterns:
- 'src/modules/prepopulate/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /prepopulate/get-all-bank-currency-details
method: get
getCityByState:
handler: src/modules/prepopulate/handlers/getCityByState.handler
memorySize: 384
package:
patterns:
- 'src/modules/prepopulate/handlers/getCityByState.*'
- 'src/modules/prepopulate/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /prepopulate/get-city-by-state
method: get
getBranchByBankXid:
handler: src/modules/prepopulate/handlers/getBranchByBank.handler
memorySize: 384
package:
patterns:
- 'src/modules/prepopulate/handlers/getBranchByBank.*'
- 'src/modules/prepopulate/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /prepopulate/get-branch-by-bank
method: get
getAllDocumentCountryStateCityDetails:
handler: src/modules/prepopulate/handlers/getAllDocTypeWithCountryState.handler
memorySize: 512
package:
patterns:
- 'src/modules/prepopulate/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /prepopulate/get-all-doc-country
method: get
getAllPqqQuesAns:
handler: src/modules/prepopulate/handlers/getAllPQQQuesWithAns.handler
memorySize: 512
package:
patterns:
- 'src/modules/prepopulate/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /prepopulate/get-all-pqq-ques-ans
method: get
getFrequenciesOfActivity:
handler: src/modules/prepopulate/handlers/getAllFrequencies.handler
memorySize: 384
package:
patterns:
- 'src/modules/prepopulate/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /prepopulate/get-all-Frequencies
method: get

View File

@@ -0,0 +1,18 @@
# AWS S3 SDK packaging patterns (merged from aws-s3.yml and aws-s3-full.yml)
# Comprehensive list of AWS S3 dependencies for Lambda packaging
pattern1: 'node_modules/@aws-sdk/client-s3/**'
pattern2: 'node_modules/@aws-sdk/s3-request-presigner/**'
pattern3: 'node_modules/@aws-sdk/types/**'
pattern4: 'node_modules/@aws-sdk/middleware-logger/**'
pattern5: 'node_modules/@aws-sdk/**'
pattern6: 'node_modules/@smithy/**'
pattern7: 'node_modules/tslib/**'
pattern8: 'node_modules/uuid/**'
pattern9: 'node_modules/@aws-crypto/**'
pattern10: 'node_modules/@aws/smithy-client/**'
pattern11: 'node_modules/@aws/util-uri-escape/**'
pattern12: 'node_modules/@aws/util-middleware/**'
pattern13: 'node_modules/@aws/lambda-invoke-store/**'
pattern14: 'node_modules/busboy/**'
pattern15: 'node_modules/lambda-multipart-parser/**'
pattern16: 'node_modules/fast-xml-parser/**'

View File

@@ -0,0 +1,8 @@
# Base packaging patterns shared across all functions
# Note: Prisma 7 uses driver adapters (no binary engines) - everything is in the layer
pattern1: 'src/common/**'
pattern2: 'common/**'
# Prisma packages are now provided by the layer, no need to include in function package
pattern3: '!node_modules/@prisma/**'
pattern4: '!node_modules/.prisma/**'
pattern5: '!node_modules/pg/**'

View File

@@ -0,0 +1,11 @@
import { PrismaClient } from '@prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
export const prisma = new PrismaClient({
adapter,
log: process.env.NODE_ENV === 'dev' ? ['query', 'info', 'warn', 'error'] : ['error'],
});

View File

@@ -1,15 +1,13 @@
import { Injectable, OnModuleInit, OnModuleDestroy, INestApplication } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
constructor() {
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
super({
datasources: {
db: {
url: process.env.DATABASE_URL,
},
},
adapter,
log: process.env.NODE_ENV === 'dev' ? ['query', 'info', 'warn', 'error'] : ['error'],
});
}
@@ -27,5 +25,4 @@ export class PrismaService extends PrismaClient implements OnModuleInit, OnModul
await app.close();
});
}
}
}

View File

@@ -0,0 +1,46 @@
import axios, { AxiosInstance } from 'axios';
import config from '../../config/config';
interface EmailRecipient {
email: string;
name?: string;
}
interface EmailOptions {
recipients: EmailRecipient[];
subject: string;
htmlContent: string;
}
class BrevoService {
private readonly instance: AxiosInstance;
constructor() {
this.instance = axios.create({
baseURL: config.email.BrevobaseURL,
headers: {
'api-key': config.email.api_key,
'Content-Type': 'application/json',
},
});
}
async sendEmail(options: EmailOptions): Promise<{ messageId: string }> {
const response = await this.instance.post('/smtp/email', {
sender: {
name: 'Minglar',
email: 'minglar.admin@minglargroup.com',
},
to: options.recipients,
subject: options.subject,
htmlContent: options.htmlContent,
replyTo: {
email: 'minglar.admin@minglargroup.com',
},
});
return response.data;
}
}
export const brevoService = new BrevoService();

View File

@@ -0,0 +1,18 @@
// common/utils/awsPresign.ts
import config from "@/config/config";
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const s3 = new S3Client({
region: config.aws.region, // e.g. ap-south-1
});
export const getPresignedUrl = async (bucket: string, key: string) => {
const command = new GetObjectCommand({
Bucket: bucket,
Key: key,
});
// URL valid for 1 hour
return await getSignedUrl(s3, command, { expiresIn: 3600 });
};

View File

@@ -1,14 +1,14 @@
import jwt from 'jsonwebtoken';
import httpStatus from 'http-status';
import { Request, Response, NextFunction } from 'express';
import { PrismaClient } from '@prisma/client';
import ApiError from '../../utils/helper/ApiError';
import config from '../../../config/config';
const prisma = new PrismaClient();
import { ROLE } from '@/common/utils/constants/common.constant';
import { prisma } from '../../database/prisma.client';
interface DecodedToken {
id: number;
id?: number;
sub?: string | number;
role?: string;
iat: number;
exp: number;
@@ -26,7 +26,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 !== ROLE.HOST) {
throw new ApiError(httpStatus.FORBIDDEN, 'Access denied.');
}
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 +87,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 +112,4 @@ const auth =
.catch((err) => next(err));
};
export default auth;
export default authForHost;

View File

@@ -0,0 +1,118 @@
import jwt from 'jsonwebtoken';
import httpStatus from 'http-status';
import { Request, Response, NextFunction } from 'express';
import ApiError from '../../utils/helper/ApiError';
import config from '../../../config/config';
import { ROLE } from '@/common/utils/constants/common.constant';
import { prisma } from '../../database/prisma.client';
interface DecodedToken {
id?: number;
sub?: string | number;
role?: string;
iat: number;
exp: number;
}
interface UserPayload {
id: string;
role?: string;
}
declare module 'express-serve-static-core' {
interface Request {
user?: UserPayload;
}
}
/**
* Core authentication function - verifies JWT and validates Host user
* Can be used by both Express middleware and Lambda handlers
*/
export async function verifyMinglarAdminHostToken(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 Minglar Roles (role_xid = 1, 2, 3)
if (![ROLE.MINGLAR_ADMIN, ROLE.CO_ADMIN, ROLE.ACCOUNT_MANAGER, ROLE.HOST].includes(user.roleXid)) {
throw new ApiError(httpStatus.FORBIDDEN, 'Access denied.');
}
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 = 1)
*/
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 verifyMinglarAdminHostToken(token);
// Attach user to request
req.user = { id: userInfo.id.toString(), role: userInfo.role };
resolve();
} catch (error) {
return reject(error as Error);
}
};
/**
* Express middleware — use as `auth()` in routes
*/
const authForHost =
() =>
async (req: Request, res: Response, next: NextFunction) => {
return new Promise((resolve, reject) => {
verifyCallback(req, resolve, reject);
})
.then(() => next())
.catch((err) => next(err));
};
export default authForHost;

View File

@@ -1,14 +1,14 @@
import jwt from 'jsonwebtoken';
import httpStatus from 'http-status';
import { Request, Response, NextFunction } from 'express';
import { PrismaClient } from '@prisma/client';
import ApiError from '../../utils/helper/ApiError';
import config from '../../../config/config';
const prisma = new PrismaClient();
import { ROLE } from '@/common/utils/constants/common.constant';
import { prisma } from '../../database/prisma.client';
interface DecodedToken {
id: number;
id?: number;
sub?: string | number;
role?: string;
iat: number;
exp: number;
@@ -26,7 +26,60 @@ 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 verifyMinglarAdminToken(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 Minglar Roles (role_xid = 1, 2, 3)
if (![ROLE.MINGLAR_ADMIN, ROLE.CO_ADMIN, ROLE.ACCOUNT_MANAGER].includes(user.roleXid)) {
throw new ApiError(httpStatus.FORBIDDEN, 'Access denied.');
}
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 = 1)
*/
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 Admin role (role_xid = 2)
if (user.roleXid !== 2) {
return reject(
new ApiError(httpStatus.FORBIDDEN, 'Access restricted to host users only')
);
}
const userInfo = await verifyMinglarAdminToken(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

@@ -0,0 +1,116 @@
import jwt from 'jsonwebtoken';
import httpStatus from 'http-status';
import { Request, Response, NextFunction } from 'express';
import ApiError from '../../utils/helper/ApiError';
import config from '../../../config/config';
import { ROLE } from '@/common/utils/constants/common.constant';
import { prisma } from '../../database/prisma.client';
interface DecodedToken {
id?: number;
sub?: string | number;
role?: string;
iat: number;
exp: number;
}
interface UserPayload {
id: string;
role?: string;
}
declare module 'express-serve-static-core' {
interface Request {
user?: UserPayload;
}
}
/**
* Core authentication function - verifies JWT and validates Host user
* Can be used by both Express middleware and Lambda handlers
*/
export async function verifyOnlyMinglarAdminToken(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 Minglar Roles (role_xid = 1)
if (user.roleXid !== ROLE.MINGLAR_ADMIN) {
throw new ApiError(httpStatus.FORBIDDEN, 'Access denied.');
}
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 = 1)
*/
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 verifyOnlyMinglarAdminToken(token);
// Attach user to request
req.user = { id: userInfo.id.toString(), role: userInfo.role };
resolve();
} catch (error) {
return reject(error as Error);
}
};
/**
* Express middleware — use as `auth()` in routes
*/
const authForHost =
() =>
async (req: Request, res: Response, next: NextFunction) => {
return new Promise((resolve, reject) => {
verifyCallback(req, resolve, reject);
})
.then(() => next())
.catch((err) => next(err));
};
export default authForHost;

View File

@@ -1,14 +1,15 @@
import jwt from 'jsonwebtoken';
import httpStatus from 'http-status';
import { Request, Response, NextFunction } from 'express';
import { PrismaClient } from '@prisma/client';
import ApiError from '../../utils/helper/ApiError';
import config from '../../../config/config';
import { ROLE } from '@/common/utils/constants/common.constant';
const prisma = new PrismaClient();
import { prisma } from '../../database/prisma.client';
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 verifyUserToken(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 = 6)
if (user.roleXid !== ROLE.USER) {
throw new ApiError(httpStatus.FORBIDDEN, 'Access denied.');
}
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 User role (role_xid = 1)
if (user.roleXid !== 1) {
return reject(
new ApiError(httpStatus.FORBIDDEN, 'Access restricted to host users only')
);
}
const userInfo = await verifyUserToken(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

@@ -0,0 +1,24 @@
export const ROLE = {
MINGLAR_ADMIN: 1,
CO_ADMIN: 2,
ACCOUNT_MANAGER: 3,
HOST: 4,
OPERATOR: 5,
USER: 6
}
export const ROLE_NAME = {
MINGLAR_ADMIN: 'Minglar Admin',
CO_ADMIN: 'Co-admin',
ACCOUNT_MANAGER: 'Account manager',
HOST: 'Host',
OPERATOR: 'Operator',
USER: 'User'
}
export const USER_STATUS = {
INVITED: "Invited",
ACTIVE: "Active",
DE_ACTIVATED: "De-activated",
REJECTED: "Rejected"
}

View File

@@ -0,0 +1,72 @@
export const HOST_STATUS_INTERNAL = {
HOST_SUBMITTED: "Host Submitted",
HOST_TO_UPDATE: "Host To Update",
REJECTED: "Rejected",
APPROVED: "Approved",
DRAFT: "Draft",
}
export const HOST_STATUS_DISPLAY = {
DRAFT: "Draft",
UNDER_REVIEW: "Under Review",
ENHANCING: "Enhancing",
REJECTED: "Rejected",
APPROVED: "Approved",
}
export const STEPPER = {
NOT_SUBMITTED: 1,
UNDER_REVIEW: 2,
COMPANY_DETAILS_APPROVED: 3,
BANK_DETAILS_UPDATED: 4,
AGREEMENT_ACCEPTED: 5,
REJECTED: 6
}
export const LAST_QUESTION_ID = {
Q_ID: 55
}
export const ACTIVITY_INTERNAL_STATUS = {
DRAFT_PQ: 'Draft - PQ',
APPROVED: 'Approved',
REJECTED: 'Rejected',
DRAFT: 'Draft',
UNDER_REVIEW: 'Under-Review',
PQQ_FAILED: 'PQQ Failed',
PQQ_TO_UPDATE: 'PQ To Update',
PQQ_SUBMITTED: 'PQ Submitted'
}
export const ACTIVITY_DISPLAY_STATUS = {
DRAFT_PQ: 'Draft - PQ',
APPROVED: 'Approved',
REJECTED: 'Rejected',
DRAFT: 'Draft',
UNDER_REVIEW: 'Under-Review',
PQQ_FAILED: 'PQQ Failed',
ENHANCING: 'Enchancing',
PQ_IN_REVIEW: 'PQ In Review'
}
export const ACTIVITY_AM_INTERNAL_STATUS = {
DRAFT_PQ: 'Draft - PQ',
APPROVED: 'Approved',
REJECTED: 'Rejected',
DRAFT: 'Draft',
UNDER_REVIEW: 'Under-Review',
PQQ_FAILED: 'PQQ Failed',
PQQ_REJECTED: 'PQ Rejected',
PQQ_TO_REVIEW: 'PQ To Review'
}
export const ACTIVITY_AM_DISPLAY_STATUS = {
DRAFT_PQ: 'Draft - PQ',
APPROVED: 'Approved',
REJECTED: 'Rejected',
DRAFT: 'Draft',
UNDER_REVIEW: 'Under-Review',
PQQ_FAILED: 'PQQ Failed',
ENHANCING: 'Enchancing',
NEW: 'New'
}

View File

@@ -0,0 +1,47 @@
export const MINGLAR_STATUS_INTERNAL = {
ADMIN_TO_REVIEW: 'Admin To Review',
ADMIN_REJECTED: 'Admin Rejected',
AM_NOT_ASSIGNED: 'AM Not Assigned',
AM_TO_REVIEW: 'AM To Review',
AM_REJECTED: 'AM Rejected',
AM_APPROVED: 'AM Approved',
DRAFT: 'Draft',
};
export const MINGLAR_STATUS_DISPLAY = {
NEW: 'New',
AM_NOT_ASSIGNED: 'AM Not Assigned',
TO_REVIEW: 'To Review',
ENHANCING: 'Enhancing',
APPROVED: 'Approved',
REJECTED: 'Rejected',
DRAFT: 'Draft'
};
export const MINGLAR_INVITATION_STATUS = {
PENDING: 'Pending',
ACCEPTED: 'Accepted',
REJECTED: 'Rejected',
INVITED: 'Invited',
};
export const HOST_SUGGESTION_TITLES = {
COMPANY_DETAILS: 'Complete Details',
COMPANY_DOCUMENTATION: 'Company documentataion',
COMPANY_SOCIAL_PROOF: 'Social Proof',
ACTIVITY_INFORMATION: 'Activity Information',
ACTIVITY_LOCATION: 'Activity Location',
PICKUP_DROP_LOCATION: 'Pickup-Drop Location',
NUMBER_OF_PEOPLE: 'Number of People',
INCLUSION: 'Inclusion',
TAX_SETUP: 'Tax Setup',
ENERGY_LEVEL: 'Energy Level',
ELIGIBILITY_CRITERIA: 'Eligibility Criteria',
AMENITIES: 'Amenities',
EXLUSIVE_NOTES: 'Exclusive Notes',
CANCELLATION_POLICY: 'Cancellation Policy',
DOs_AND_DONTs: 'Dos and Donts',
TIPS_FOR_USERS: 'Tips for Users',
SUSTAINABILITY: 'Sustainability',
TERMS_AND_CONDITION_FOR_USER: 'Terms and Conditions for User'
};

View File

@@ -2,22 +2,68 @@
import { APIGatewayProxyEvent, Context, APIGatewayProxyResult } from 'aws-lambda';
import ApiError from '../helper/ApiError';
const stage = process.env.STAGE ?? 'dev';
export const safeHandler = (
handler: (event: APIGatewayProxyEvent, context?: Context) => Promise<APIGatewayProxyResult | undefined>
handler: (event: APIGatewayProxyEvent, context?: Context) => Promise<APIGatewayProxyResult | any>
): ((event: APIGatewayProxyEvent, context: Context) => Promise<APIGatewayProxyResult>) => {
return async (event, context) => {
try {
const result = await handler(event, context);
return (
result ?? {
// If handler returned null/undefined → return 204 response
if (!result) {
return {
statusCode: 204,
body: '',
}
);
body: JSON.stringify({
success: true,
statusCode: 204,
message: 'No content',
data: null,
}),
};
}
// If handler returned a structured Lambda response
if (result.statusCode && result.body) {
return {
statusCode: result.statusCode,
headers: result.headers || {},
body: injectStatusCodeIntoBody(result.body, result.statusCode),
};
}
// If handler returned plain data (not wrapped)
return {
statusCode: 200,
body: JSON.stringify({
success: true,
message: 'OK',
statusCode: 200,
data: result,
}),
};
} catch (error: any) {
console.error('Error occurred:', error);
console.error('Error occurred:', error);
// Convert Prisma errors to ApiError automatically
if (ApiError.isPrismaError(error)) {
const apiError = ApiError.fromPrismaError(error);
return {
statusCode: apiError.statusCode,
body: JSON.stringify({
success: false,
message: apiError.message,
statusCode: apiError.statusCode,
data: null,
error: {
code: apiError.code || apiError.statusCode,
description: apiError.message,
statusCode: apiError.statusCode,
...(apiError.meta && { meta: apiError.meta }),
...(process.env.STAGE !== 'prod' && { debug: apiError.stack }),
},
}),
};
}
if (error instanceof ApiError) {
return {
@@ -25,31 +71,53 @@ export const safeHandler = (
body: JSON.stringify({
success: false,
message: error.message,
statusCode: error.statusCode,
data: null,
error: {
code: error.statusCode,
code: error.code || error.statusCode,
description: error.message,
statusCode: error.statusCode,
...(process.env.STAGE !== 'prod' && { debug: error.stack ?? error.message }),
...(error.meta && { meta: error.meta }),
...(process.env.STAGE !== 'prod' && { debug: error.stack }),
},
}),
};
}
// Internal Server Error fallback
return {
statusCode: 500,
body: JSON.stringify({
success: false,
message: 'Internal server error',
statusCode: 500,
data: null,
error: {
code: 500,
description: 'Internal server error',
statusCode: 500,
...(process.env.STAGE !== 'prod' && { debug: error.stack ?? error.message }),
...(process.env.STAGE !== 'prod' && { debug: error.stack }),
},
}),
};
}
};
};
// Utility: safely inject statusCode into the JSON response body
function injectStatusCodeIntoBody(body: string, statusCode: number): string {
try {
const json = JSON.parse(body);
json.statusCode = statusCode;
return JSON.stringify(json);
} catch {
// If body is not JSON, wrap it
return JSON.stringify({
success: true,
statusCode,
message: body,
data: null,
});
}
}

View File

@@ -1,3 +1,75 @@
import { Prisma } from '@prisma/client';
/**
* Prisma error code mappings to user-friendly messages and HTTP status codes
*/
const PRISMA_ERROR_CODES: Record<string, { statusCode: number; message: string }> = {
// Common errors (P1xxx)
P1000: { statusCode: 500, message: 'Authentication failed against database server' },
P1001: { statusCode: 503, message: 'Database server is not reachable' },
P1002: { statusCode: 504, message: 'Database server timed out' },
P1003: { statusCode: 500, message: 'Database does not exist' },
P1008: { statusCode: 504, message: 'Database operation timed out' },
P1009: { statusCode: 409, message: 'Database already exists' },
P1010: { statusCode: 403, message: 'User was denied access to the database' },
P1011: { statusCode: 500, message: 'Error opening a TLS connection' },
P1012: { statusCode: 500, message: 'Schema validation error' },
P1013: { statusCode: 400, message: 'Invalid database connection string' },
P1014: { statusCode: 500, message: 'Underlying model does not exist' },
P1015: { statusCode: 500, message: 'Database schema uses unsupported features' },
P1016: { statusCode: 400, message: 'Raw query has incorrect number of parameters' },
P1017: { statusCode: 503, message: 'Database server has closed the connection' },
// Prisma Client Query Engine errors (P2xxx)
P2000: { statusCode: 400, message: 'Value too long for column' },
P2001: { statusCode: 404, message: 'Record not found' },
P2002: { statusCode: 409, message: 'Unique constraint violation' },
P2003: { statusCode: 409, message: 'Foreign key constraint violation' },
P2004: { statusCode: 400, message: 'Database constraint violation' },
P2005: { statusCode: 400, message: 'Invalid value stored in database' },
P2006: { statusCode: 400, message: 'Invalid value provided' },
P2007: { statusCode: 400, message: 'Data validation error' },
P2008: { statusCode: 400, message: 'Failed to parse the query' },
P2009: { statusCode: 400, message: 'Failed to validate the query' },
P2010: { statusCode: 500, message: 'Raw query failed' },
P2011: { statusCode: 400, message: 'Null constraint violation' },
P2012: { statusCode: 400, message: 'Missing required value' },
P2013: { statusCode: 400, message: 'Missing required argument' },
P2014: { statusCode: 409, message: 'Required relation violation' },
P2015: { statusCode: 404, message: 'Related record not found' },
P2016: { statusCode: 400, message: 'Query interpretation error' },
P2017: { statusCode: 400, message: 'Records for relation not connected' },
P2018: { statusCode: 404, message: 'Required connected records not found' },
P2019: { statusCode: 400, message: 'Input error' },
P2020: { statusCode: 400, message: 'Value out of range' },
P2021: { statusCode: 500, message: 'Table does not exist' },
P2022: { statusCode: 500, message: 'Column does not exist' },
P2023: { statusCode: 500, message: 'Inconsistent column data' },
P2024: { statusCode: 503, message: 'Connection pool timeout' },
P2025: { statusCode: 404, message: 'Record not found' },
P2026: { statusCode: 400, message: 'Unsupported database feature used in query' },
P2027: { statusCode: 500, message: 'Multiple database errors occurred' },
P2028: { statusCode: 500, message: 'Transaction API error' },
P2029: { statusCode: 400, message: 'Query parameter limit exceeded' },
P2030: { statusCode: 400, message: 'Fulltext index not found' },
P2031: { statusCode: 500, message: 'MongoDB requires replica set' },
P2033: { statusCode: 400, message: 'Number does not fit in 64 bit signed integer' },
P2034: { statusCode: 409, message: 'Transaction failed due to write conflict or deadlock' },
P2035: { statusCode: 500, message: 'Database assertion violation' },
P2036: { statusCode: 500, message: 'External connector error' },
P2037: { statusCode: 503, message: 'Too many database connections opened' },
};
interface PrismaErrorMeta {
target?: string[];
field_name?: string;
model_name?: string;
argument_name?: string;
constraint?: string;
cause?: string;
[key: string]: unknown;
}
class ApiError<T = unknown> extends Error {
statusCode: number;
data: T | null;
@@ -6,13 +78,17 @@ class ApiError<T = unknown> extends Error {
errors: Array<Error>;
isOperational: boolean;
stack?: string;
code?: string;
meta?: PrismaErrorMeta;
constructor(
statusCode: number,
message: string = 'Something went wrong',
errors: Array<Error> = [],
isOperational: boolean = true,
stack?: string
stack?: string,
code?: string,
meta?: PrismaErrorMeta
) {
super(message);
this.statusCode = statusCode;
@@ -21,6 +97,8 @@ class ApiError<T = unknown> extends Error {
this.success = false;
this.errors = errors;
this.isOperational = isOperational;
this.code = code;
this.meta = meta;
if (stack) {
this.stack = stack;
@@ -28,6 +106,159 @@ class ApiError<T = unknown> extends Error {
Error.captureStackTrace(this, this.constructor);
}
}
/**
* Creates an ApiError from a Prisma error
* Handles all Prisma error types: PrismaClientKnownRequestError, PrismaClientUnknownRequestError,
* PrismaClientRustPanicError, PrismaClientInitializationError, PrismaClientValidationError
*/
static fromPrismaError(error: unknown): ApiError {
// Handle PrismaClientKnownRequestError
if (error instanceof Prisma.PrismaClientKnownRequestError) {
const errorInfo = PRISMA_ERROR_CODES[error.code] || {
statusCode: 500,
message: 'Database operation failed',
};
let message = errorInfo.message;
const meta = error.meta as PrismaErrorMeta | undefined;
// Provide more specific messages based on error code and meta
switch (error.code) {
case 'P2002': {
const target = meta?.target;
if (target && Array.isArray(target)) {
message = `Unique constraint violation on field(s): ${target.join(', ')}`;
}
break;
}
case 'P2003': {
const fieldName = meta?.field_name;
if (fieldName) {
message = `Foreign key constraint failed on field: ${fieldName}`;
}
break;
}
case 'P2025': {
const cause = meta?.cause;
if (cause) {
message = `Record not found: ${cause}`;
}
break;
}
case 'P2011': {
const constraint = meta?.constraint;
if (constraint) {
message = `Null constraint violation on: ${constraint}`;
}
break;
}
case 'P2014': {
const modelName = meta?.model_name;
if (modelName) {
message = `Required relation violation on model: ${modelName}`;
}
break;
}
}
return new ApiError(
errorInfo.statusCode,
message,
[error],
true,
error.stack,
error.code,
meta
);
}
// Handle PrismaClientUnknownRequestError
if (error instanceof Prisma.PrismaClientUnknownRequestError) {
return new ApiError(
500,
'An unknown database error occurred',
[error],
true,
error.stack,
'UNKNOWN_REQUEST_ERROR'
);
}
// Handle PrismaClientRustPanicError
if (error instanceof Prisma.PrismaClientRustPanicError) {
return new ApiError(
500,
'A critical database error occurred. Please try again later.',
[error],
false,
error.stack,
'RUST_PANIC_ERROR'
);
}
// Handle PrismaClientInitializationError
if (error instanceof Prisma.PrismaClientInitializationError) {
const errorInfo = error.errorCode
? PRISMA_ERROR_CODES[error.errorCode] || { statusCode: 500, message: 'Database initialization failed' }
: { statusCode: 500, message: 'Database initialization failed' };
return new ApiError(
errorInfo.statusCode,
errorInfo.message,
[error],
false,
error.stack,
error.errorCode || 'INITIALIZATION_ERROR'
);
}
// Handle PrismaClientValidationError
if (error instanceof Prisma.PrismaClientValidationError) {
return new ApiError(
400,
'Invalid data provided for database operation',
[error],
true,
error.stack,
'VALIDATION_ERROR'
);
}
// Fallback for any other error
if (error instanceof Error) {
return new ApiError(500, error.message, [error], true, error.stack);
}
return new ApiError(500, 'An unexpected error occurred');
}
/**
* Check if an error is a Prisma error
*/
static isPrismaError(error: unknown): boolean {
return (
error instanceof Prisma.PrismaClientKnownRequestError ||
error instanceof Prisma.PrismaClientUnknownRequestError ||
error instanceof Prisma.PrismaClientRustPanicError ||
error instanceof Prisma.PrismaClientInitializationError ||
error instanceof Prisma.PrismaClientValidationError
);
}
/**
* Get a user-friendly message for a Prisma error code
*/
static getPrismaErrorMessage(code: string): string {
return PRISMA_ERROR_CODES[code]?.message || 'Database operation failed';
}
/**
* Get HTTP status code for a Prisma error code
*/
static getPrismaErrorStatusCode(code: string): number {
return PRISMA_ERROR_CODES[code]?.statusCode || 500;
}
}
export default ApiError;

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,126 @@
import ApiError from './ApiError';
interface ParsedFormData {
fields: Record<string, string>;
files: Array<{
fieldName: string;
fileName: string;
contentType: string;
data: Buffer;
}>;
}
/**
* Parse multipart/form-data from Lambda event
* Supports both base64 encoded and binary body
*/
export function parseMultipartFormData(
eventBody: string | null,
contentType: string | undefined,
isBase64Encoded: boolean = false
): ParsedFormData {
if (!eventBody) {
throw new ApiError(400, 'Request body is required');
}
if (!contentType || !contentType.includes('multipart/form-data')) {
throw new ApiError(400, 'Content-Type must be multipart/form-data');
}
// Extract boundary from Content-Type header
const boundaryMatch = contentType.match(/boundary=([^;]+)/);
if (!boundaryMatch) {
throw new ApiError(400, 'Invalid multipart boundary');
}
const boundary = boundaryMatch[1].trim();
// Decode base64 body if needed (API Gateway sends base64 encoded for binary media types)
let bodyBuffer: Buffer;
try {
if (isBase64Encoded) {
bodyBuffer = Buffer.from(eventBody, 'base64');
} else {
// Try to detect if it's base64
if (eventBody.match(/^[A-Za-z0-9+/=]+$/)) {
bodyBuffer = Buffer.from(eventBody, 'base64');
} else {
bodyBuffer = Buffer.from(eventBody, 'binary');
}
}
} catch (error) {
throw new ApiError(400, 'Invalid request body encoding');
}
// Split by boundary
const parts = bodyBuffer.toString('binary').split(`--${boundary}`);
const fields: Record<string, string> = {};
const files: ParsedFormData['files'] = [];
for (const part of parts) {
if (!part || part.trim() === '' || part.trim() === '--') {
continue;
}
// Split headers and body
const [headers, ...bodyParts] = part.split('\r\n\r\n');
if (!headers || bodyParts.length === 0) {
continue;
}
const body = bodyParts.join('\r\n\r\n').trim();
if (!body) {
continue;
}
// Parse Content-Disposition header
const contentDispositionMatch = headers.match(/Content-Disposition:\s*form-data;\s*name="([^"]+)"/);
if (!contentDispositionMatch) {
continue;
}
const fieldName = contentDispositionMatch[1];
// Check if it's a file
const filenameMatch = headers.match(/filename="([^"]+)"/);
const contentTypeMatch = headers.match(/Content-Type:\s*([^\r\n]+)/);
if (filenameMatch) {
// It's a file
const fileName = filenameMatch[1];
const fileContentType = contentTypeMatch ? contentTypeMatch[1].trim() : 'application/octet-stream';
// Convert body to buffer (remove trailing boundary markers)
const fileData = Buffer.from(body.replace(/\r\n--$/, ''), 'binary');
files.push({
fieldName,
fileName,
contentType: fileContentType,
data: fileData,
});
} else {
// It's a regular field
fields[fieldName] = body.replace(/\r\n--$/, '').trim();
}
}
return { fields, files };
}
/**
* Parse JSON field from form data
*/
export function parseJsonField(fields: Record<string, string>, fieldName: string): any {
const value = fields[fieldName];
if (!value) {
return null;
}
try {
return JSON.parse(value);
} catch (error) {
throw new ApiError(400, `Invalid JSON in field: ${fieldName}`);
}
}

View File

@@ -0,0 +1,63 @@
import * as bcrypt from "bcryptjs";
import { OtpGenerator, OtpGeneratorSixDigit } from "./OtpGenerator";
import { encryptUserId } from "./CodeGenerator";
export interface OtpResult {
otp: string;
hashedOtp: string;
expiry: Date;
encryptedId: string;
}
export async function resendOtpHelper(
prisma: any,
userId: number,
email: string,
emailPurpose: "Register" | "Login" | "ForgotPassword",
otpLength: 4 | 6 = 4,
expiryMinutes: number = 5
): Promise<OtpResult> {
// 1⃣ Deactivate previous OTPs
await prisma.userOtp.updateMany({
where: {
userXid: userId,
otpType: emailPurpose,
isActive: true,
},
data: {
isActive: false,
isVerified: true,
},
});
// 2⃣ Generate new OTP
const otp =
otpLength === 6
? OtpGeneratorSixDigit.generateOtp()
: OtpGenerator.generateOtp();
const hashedOtp = await bcrypt.hash(otp, 10);
const expiry = new Date(Date.now() + expiryMinutes * 60000);
const encryptedId = encryptUserId(userId.toString());
// 3⃣ Insert new OTP into table
await prisma.userOtp.create({
data: {
userXid: userId,
otpType: emailPurpose,
otpCode: hashedOtp,
expiresOn: expiry,
isVerified: false,
isActive: true,
},
});
// 4⃣ Return new OTP (email will use this)
return {
otp,
hashedOtp,
expiry,
encryptedId,
};
}

View File

@@ -0,0 +1,191 @@
import dotenv from 'dotenv';
import path from 'path';
import * as yup from 'yup';
dotenv.config({ path: path.join(__dirname, '../../.env') });
const envVarsSchema = yup
.object()
.shape({
NODE_ENV: yup
.string()
.oneOf(['production', 'development', 'test'])
.required(),
PORT: yup.number().default(3000),
// FRONTEND_URL: yup.string().required('Frontend URL is required'),
//JWT
JWT_SECRET: yup.string().required('JWT secret key is required'),
JWT_ACCESS_EXPIRATION_MINUTES: yup
.number()
.default(30)
.required('minutes after which access tokens expire'),
JWT_REFRESH_EXPIRATION_DAYS: yup
.number()
.default(30)
.required('days after which refresh tokens expire'),
JWT_RESET_PASSWORD_EXPIRATION_MINUTES: yup
.number()
.default(10)
.required('minutes after which reset password token expires'),
JWT_VERIFY_EMAIL_EXPIRATION_MINUTES: yup
.number()
.default(10)
.required('minutes after which verify email token expires'),
AWS_REGION: yup.string().required('AWS region is required'),
S3_BUCKET_NAME: yup.string().required('S3 bucket name is required'),
//SMTP and BREVO
// BREVO_SMTP_HOST: yup
// .string()
// .nullable()
// .required('server that will send the emails'),
// BREVO_SMTP_PORT: yup
// .number()
// .nullable()
// .required('port to connect to the email server'),
// BREVO_SMTP_USER: yup
// .string()
// .nullable()
// .required('username for email server'),
// BREVO_SMTP_PASS: yup
// .string()
// .nullable()
// .required('password for email server'),
// BREVO_FROM_EMAIL: yup
// .string()
// .nullable()
// .required('the from field in the emails sent by the app'),
// BREVO_EMAIL_API_KEY: yup
// .string()
// .nullable()
// .required('the from field in the emails sent by the app api key'),
// BREVO_API_BASEURL: yup.string().required('Brevo base URL is required'),
// //one signal
// ONESIGNAL_APPID: yup.string().required('One signal app id is required'),
// ONESIGNAL_REST_APIKEY: yup
// .string()
// .required('One signal api key is required'),
//branch IO
// BRANCH_IO_KEY: yup.string().required('Branch IO key is required'),
// DataBase
DB_USERNAME: yup.string().required('DB Username is required'),
DB_PASSWORD: yup.string().required('DB Password is required'),
DB_DATABASE_NAME: yup.string().required('Database name is required'),
DB_HOSTNAME: yup
.string()
.default('127.0.0.1')
.required('DB Hostname is required'),
DB_PORT: yup.number().default(3306).required('DB Port is required'),
//OTP Bypass
BYPASS_OTP: yup.boolean().default(false).required('Bypass OTP is required'),
})
.noUnknown(true);
// Validate and prepare the configuration
function getConfig() {
try {
// Validate the environment variables
const envVars = envVarsSchema.validateSync(process.env, {
abortEarly: false, // Validate all fields before throwing errors
stripUnknown: true, // Remove fields not in the schema
});
// Return the validated configuration
return {
env: envVars.NODE_ENV,
port: envVars.PORT,
jwt: {
secret: envVars.JWT_SECRET,
accessExpirationMinutes: envVars.JWT_ACCESS_EXPIRATION_MINUTES,
refreshExpirationDays: envVars.JWT_REFRESH_EXPIRATION_DAYS,
resetPasswordExpirationMinutes:
envVars.JWT_RESET_PASSWORD_EXPIRATION_MINUTES,
verifyEmailExpirationMinutes:
envVars.JWT_VERIFY_EMAIL_EXPIRATION_MINUTES,
},
database: {
development: {
host: envVars.DB_HOSTNAME,
port: envVars.DB_PORT,
username: envVars.DB_USERNAME,
password: envVars.DB_PASSWORD,
database: envVars.DB_DATABASE_NAME,
logging: false,
},
test: {
host: envVars.DB_HOSTNAME,
port: envVars.DB_PORT,
username: envVars.DB_USERNAME,
password: envVars.DB_PASSWORD,
database: envVars.DB_DATABASE_NAME,
logging: false,
socketPath: '/var/run/mysqld/mysqld.sock',
},
production: {
host: envVars.DB_HOSTNAME,
port: envVars.DB_PORT,
username: envVars.DB_USERNAME,
password: envVars.DB_PASSWORD,
database: envVars.DB_DATABASE_NAME,
logging: false,
socketPath: '/var/run/mysqld/mysqld.sock',
},
},
aws: {
region: envVars.AWS_REGION,
bucketName: envVars.S3_BUCKET_NAME,
},
byPassOTP: envVars.BYPASS_OTP,
// BaseURL: envVars.BASEURL,
// FRONTEND_URL: envVars.FRONTEND_URL,
// email: {
// smtp: {
// host: envVars?.BREVO_SMTP_HOST,
// port: envVars?.BREVO_SMTP_PORT,
// secure: envVars?.BREVO_SMTP_PORT == 465, // true for 465, false for other ports
// auth: {
// user: envVars?.BREVO_SMTP_USER,
// pass: envVars?.BREVO_SMTP_PASS,
// },
// },
// from: envVars?.BREVO_FROM_EMAIL,
// api_key: envVars?.BREVO_EMAIL_API_KEY,
// BrevobaseURL: envVars?.BREVO_API_BASEURL,
// },
// oneSignal: {
// appID: envVars.ONESIGNAL_APPID,
// restApiKey: envVars.ONESIGNAL_REST_APIKEY,
// },
// branchIO: {
// branchIOKey: envVars.BRANCH_IO_KEY,
// },
};
} catch (error: unknown) {
if (error instanceof yup.ValidationError) {
console.error('Validation Errors:', error.errors.join(', '));
} else {
console.error('Unexpected error during configuration validation:', error);
}
console.error(
'Server shut down due to incomplete environment variable configuration.'
);
process.exit(1); // Exit with error code 1
}
}
/**
* Created By : Angad Chauhan
* Created at : 31/1/25
* Use : For google login .env file global variable
*/
// export const googleConfig = {
// clientID: process.env.GOOGLE_CLIENT_ID!,
// clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
// callbackURL: process.env.GOOGLE_CALLBACK_URL!,
// };
// Validate and export configuration only if validation succeeds
const config = getConfig();
export default config;

View File

@@ -0,0 +1,62 @@
import * as bcrypt from "bcryptjs";
import { OtpGenerator, OtpGeneratorSixDigit } from "./OtpGenerator";
import { encryptUserId } from "./CodeGenerator";
export interface OtpResult {
otp: string;
hashedOtp: string;
expiry: Date;
encryptedId: string;
}
/**
* Generate OTP using Prisma instance passed from Lambda
*/
export async function generateOtpHelper(
prisma: any, // ⭐ Inject prisma
userId: number,
email: string,
emailPurpose: "Register" | "Login" | "ForgotPassword",
otpLength: 4 | 6 = 4,
expiryMinutes: number = 5
): Promise<OtpResult> {
const otp =
otpLength === 6
? OtpGeneratorSixDigit.generateOtp()
: OtpGenerator.generateOtp();
const hashedOtp = await bcrypt.hash(otp, 10);
const expiry = new Date(Date.now() + expiryMinutes * 60000);
const encryptedId = encryptUserId(userId.toString());
// Delete previous active OTPs
await prisma.userOtp.deleteMany({
where: {
userXid: userId,
otpType: emailPurpose,
isActive: true,
},
});
// Create new OTP entry
await prisma.userOtp.create({
data: {
userXid: userId,
otpType: emailPurpose,
otpCode: hashedOtp,
expiresOn: expiry,
isVerified: false,
isActive: true,
},
});
return {
otp,
hashedOtp,
expiry,
encryptedId,
};
}

View File

@@ -0,0 +1,66 @@
// common/utils/pagination/pagination.service.ts
import { PaginationOptions, PaginationParams, PaginatedResponse } from './pagination.types';
export class PaginationService {
private readonly DEFAULT_PAGE = 1;
private readonly DEFAULT_LIMIT = 10;
private readonly MAX_LIMIT = 100;
/**
* Parse and validate pagination parameters
*/
parsePaginationParams(params: PaginationParams): PaginationOptions {
let page = Number(params.page) || this.DEFAULT_PAGE;
let limit = Number(params.limit) || this.DEFAULT_LIMIT;
// Validate and constrain values
page = Math.max(1, page);
limit = Math.max(1, Math.min(limit, this.MAX_LIMIT));
const skip = (page - 1) * limit;
return {
page,
limit,
skip,
};
}
/**
* Create paginated response
*/
createPaginatedResponse<T>(
data: T[],
totalCount: number,
paginationOptions: PaginationOptions,
): PaginatedResponse<T> {
const { page, limit } = paginationOptions;
const totalPages = Math.ceil(totalCount / limit);
return {
data,
pagination: {
currentPage: page,
pageSize: limit,
totalCount,
totalPages,
hasNext: page < totalPages,
hasPrevious: page > 1,
},
};
}
/**
* Extract pagination params from API Gateway event
*/
getPaginationFromEvent(event: any): PaginationParams {
const queryParams = event.queryStringParameters || {};
return {
page: queryParams.page,
limit: queryParams.limit,
};
}
}
export const paginationService = new PaginationService();

View File

@@ -0,0 +1,23 @@
// common/utils/pagination/pagination.types.ts
export interface PaginationOptions {
page: number;
limit: number;
skip: number;
}
export interface PaginatedResponse<T> {
data: T[];
pagination: {
currentPage: number;
pageSize: number;
totalCount: number;
totalPages: number;
hasNext: boolean;
hasPrevious: boolean;
};
}
export interface PaginationParams {
page?: string | number;
limit?: string | number;
}

View File

@@ -0,0 +1,140 @@
/**
* Host Activity Validation Schemas
* Production-ready Zod validations for host activity management
*/
import { z } from 'zod';
import { idSchema, optionalIdSchema, searchQuerySchema, paginationSchema } from '../validation.utils';
// ============================================
// CREATE ACTIVITY (FOR PQQ)
// ============================================
/**
* Create activity schema
*/
export const createActivitySchema = z.object({
activityTypeXid: idSchema.describe('Activity type ID'),
frequenciesXid: optionalIdSchema.describe('Frequency ID'),
});
export type CreateActivityInput = z.infer<typeof createActivitySchema>;
// ============================================
// GET ACTIVITY TYPE
// ============================================
/**
* Get all activity types query params
*/
export const getActivityTypeQuerySchema = z.object({
interestXid: z.coerce
.number()
.int('Interest ID must be an integer')
.positive('Interest ID must be positive')
.optional(),
});
export type GetActivityTypeQuery = z.infer<typeof getActivityTypeQuerySchema>;
// ============================================
// GET PQQ BY QUESTION ID
// ============================================
/**
* Get PQQ by question ID query params
*/
export const getPqqByQuestionIdQuerySchema = z.object({
question_xid: z.coerce
.number()
.int('Question ID must be an integer')
.positive('Question ID must be positive'),
activity_xid: z.coerce
.number()
.int('Activity ID must be an integer')
.positive('Activity ID must be positive'),
});
export type GetPqqByQuestionIdQuery = z.infer<typeof getPqqByQuestionIdQuerySchema>;
// ============================================
// SUBMIT PQQ ANSWER
// ============================================
/**
* Submit PQQ answer schema
*/
export const submitPqqAnswerSchema = z.object({
activityXid: idSchema.describe('Activity ID'),
questionXid: idSchema.describe('Question ID'),
answerXid: idSchema.describe('Answer ID'),
// For file uploads, these are handled separately
documentPath: z.string().max(500).optional(),
remarks: z.string().max(500, 'Remarks cannot exceed 500 characters').optional(),
});
export type SubmitPqqAnswerInput = z.infer<typeof submitPqqAnswerSchema>;
// ============================================
// UPDATE SUGGESTION AS REVIEWED
// ============================================
/**
* Update suggestion as reviewed schema
*/
export const updateSuggestionReviewedSchema = z.object({
activityPqqHeaderXid: idSchema.describe('Activity PQQ Header ID'),
activityPQQSuggestionId: idSchema.optional().describe('Activity PQQ Suggestion ID'),
});
export type UpdateSuggestionReviewedInput = z.infer<typeof updateSuggestionReviewedSchema>;
// ============================================
// GET ALL HOST ACTIVITY
// ============================================
/**
* Get all host activities query params
*/
export const getAllHostActivityQuerySchema = z.object({
hostXid: z.coerce
.number()
.int('Host ID must be an integer')
.positive('Host ID must be positive')
.optional(),
status: z
.enum(['pending', 'approved', 'rejected', 'draft'])
.optional(),
...paginationSchema.shape,
});
export type GetAllHostActivityQuery = z.infer<typeof getAllHostActivityQuerySchema>;
// ============================================
// GET LATEST QUESTION (activity_xid query param)
// ============================================
/**
* Get latest PQQ question query params
*/
export const getLatestPqqQuestionQuerySchema = z.object({
activity_xid: z.coerce
.number()
.int('Activity ID must be an integer')
.positive('Activity ID must be positive'),
});
export type GetLatestPqqQuestionQuery = z.infer<typeof getLatestPqqQuestionQuerySchema>;
// ============================================
// SEARCH QUERY (optional)
// ============================================
/**
* Optional search query params
*/
export const optionalSearchQuerySchema = z.object({
search: searchQuerySchema,
q: searchQuerySchema,
});
export type OptionalSearchQuery = z.infer<typeof optionalSearchQuerySchema>;

View File

@@ -0,0 +1,27 @@
/**
* Host Bank Details Validation Schema
* Production-ready Zod validation for payment/bank details
*/
import { z } from 'zod';
import { idSchema, ifscCodeSchema, accountNumberSchema } from '../validation.utils';
export const hostBankDetailsSchema = z.object({
accountNumber: accountNumberSchema,
accountHolderName: z
.string()
.min(2, 'Account holder name must be at least 2 characters')
.max(100, 'Account holder name cannot exceed 100 characters'),
ifscCode: ifscCodeSchema,
bankXid: idSchema.describe('Bank ID'),
hostXid: idSchema.describe('Host ID'),
bankBranchXid: idSchema.describe('Bank branch ID'),
currencyXid: idSchema.describe('Currency ID'),
});
export type HostBankDetailsInput = z.infer<typeof hostBankDetailsSchema>;

View File

@@ -0,0 +1,139 @@
/**
* Host Onboarding Validation Schemas
* Production-ready Zod validations for host registration and authentication
* Compatible with Zod v4
*/
import { z } from 'zod';
import {
emailSchema,
simplePasswordSchema,
otpSchema,
nameSchema,
optionalNameSchema,
mobileNumberSchema,
isdCodeSchema,
} from '../validation.utils';
// ============================================
// SIGNUP / REGISTRATION
// ============================================
/**
* Host registration/signup schema
*/
export const hostSignUpSchema = z.object({
email: emailSchema,
});
export type HostSignUpInput = z.infer<typeof hostSignUpSchema>;
// ============================================
// OTP VERIFICATION
// ============================================
/**
* OTP verification schema
*/
export const verifyOtpSchema = z.object({
otp: otpSchema,
});
export type VerifyOtpInput = z.infer<typeof verifyOtpSchema>;
/**
* OTP verification with email schema (for verifyOTP handler)
*/
export const verifyOtpWithEmailSchema = z.object({
email: emailSchema,
otp: otpSchema,
});
export type VerifyOtpWithEmailInput = z.infer<typeof verifyOtpWithEmailSchema>;
// ============================================
// LOGIN
// ============================================
/**
* Host login schema
*/
export const hostLoginSchema = z.object({
emailAddress: emailSchema,
userPassword: z
.string()
.min(1, 'Password is required'),
});
export type HostLoginInput = z.infer<typeof hostLoginSchema>;
// ============================================
// CREATE PASSWORD
// ============================================
/**
* Create password schema with confirmation matching
*/
export const createPasswordSchema = z
.object({
password: simplePasswordSchema,
confirmPassword: z.string().min(1, 'Confirm password is required'),
})
.refine((data) => data.password === data.confirmPassword, {
message: 'Password and confirm password do not match',
path: ['confirmPassword'],
});
export type CreatePasswordInput = z.infer<typeof createPasswordSchema>;
// ============================================
// RESEND OTP
// ============================================
/**
* Resend OTP schema
*/
export const resendOtpSchema = z.object({
email: emailSchema,
purpose: z
.enum(['Register', 'Login', 'ForgotPassword'])
.optional()
.default('Register'),
});
export type ResendOtpInput = z.infer<typeof resendOtpSchema>;
// ============================================
// UPDATE PROFILE
// ============================================
/**
* Host profile update schema
*/
export const updateHostProfileSchema = z.object({
firstName: optionalNameSchema,
lastName: optionalNameSchema,
mobileNumber: mobileNumberSchema,
isdCode: isdCodeSchema,
dateOfBirth: z
.string()
.refine((val) => !val || !isNaN(Date.parse(val)), 'Invalid date format')
.optional(),
profileImage: z.string().max(500, 'Profile image path cannot exceed 500 characters').optional(),
});
export type UpdateHostProfileInput = z.infer<typeof updateHostProfileSchema>;
// ============================================
// ACCEPT AGREEMENT
// ============================================
/**
* Accept agreement schema (just confirmation)
*/
export const acceptAgreementSchema = z.object({
agreementAccepted: z.literal(true, {
message: 'Agreement must be accepted',
}),
});
export type AcceptAgreementInput = z.infer<typeof acceptAgreementSchema>;

View File

@@ -0,0 +1,135 @@
import { z } from "zod";
export const parentCompanySchema = z.object({
companyName: z.string()
.min(1, "Parent company name is required")
.max(100, "Parent company name cannot exceed 100 characters"),
address1: z.string()
.min(1, "Address1 is required")
.max(150, "Address1 cannot exceed 150 characters"),
address2: z.string()
.max(150, "Address2 cannot exceed 150 characters")
.optional(),
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")
.max(30, "Pincode cannot exceed 30 characters"),
logoPath: z.string()
.max(400, "Logo path cannot exceed 400 characters")
.optional(),
isSubsidairy: z.boolean().optional(),
registrationNumber: z.string()
.max(30, "Registration number cannot exceed 30 characters")
.optional(),
panNumber: z.string()
.max(30, "PAN number cannot exceed 30 characters")
.optional(),
gstNumber: z.string()
.max(30, "GST number cannot exceed 30 characters")
.optional(),
formationDate: z.string()
.optional()
.refine((val) => !val || !isNaN(Date.parse(val)), {
message: "Formation date must be a valid date",
}),
companyType: z.string()
.min(1, "Company type is required")
.max(30, "Company type cannot exceed 30 characters"),
websiteUrl: z.string().url().max(80, "Website URL cannot exceed 80 characters").optional(),
instagramUrl: z.string().url().max(80, "Instagram URL cannot exceed 80 characters").optional(),
facebookUrl: z.string().url().max(80, "Facebook URL cannot exceed 80 characters").optional(),
linkedinUrl: z.string().url().max(80, "LinkedIn URL cannot exceed 80 characters").optional(),
twitterUrl: z.string().url().max(80, "Twitter URL cannot exceed 80 characters").optional(),
});
// =======================================================
// HOST COMPANY DETAILS (Main Company)
// =======================================================
export const hostCompanyDetailsSchema = z.object({
companyName: z.string()
.min(1, "Company name is required")
.max(100, "Company name cannot exceed 100 characters"),
address1: z.string()
.min(1, "Address1 is required")
.max(150, "Address1 cannot exceed 150 characters"),
address2: z.string()
.max(150, "Address2 cannot exceed 150 characters")
.optional(),
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")
.max(30, "Pincode cannot exceed 30 characters"),
logoPath: z.string()
.max(400, "Logo path cannot exceed 400 characters")
.optional(),
isSubsidairy: z.boolean(),
registrationNumber: z.string()
.max(30, "Registration number cannot exceed 30 characters")
.optional(),
panNumber: z.string()
.min(1, "PAN number is required")
.max(30, "PAN number cannot exceed 30 characters"),
gstNumber: z.string()
.max(30, "GST number cannot exceed 30 characters")
.optional(),
formationDate: z.string()
.optional()
.refine((val) => !val || !isNaN(Date.parse(val)), {
message: "Formation date must be a valid date",
}),
companyType: z.string()
.min(1, "Company type is required")
.max(30, "Company type cannot exceed 30 characters"),
websiteUrl: z.string().url().max(50, "Website URL cannot exceed 50 characters").optional(),
instagramUrl: z.string().url().max(80, "Instagram URL cannot exceed 80 characters").optional(),
facebookUrl: z.string().url().max(80, "Facebook URL cannot exceed 80 characters").optional(),
linkedinUrl: z.string().url().max(80, "LinkedIn URL cannot exceed 80 characters").optional(),
twitterUrl: z.string().url().max(80, "Twitter URL cannot exceed 80 characters").optional(),
parentCompany: parentCompanySchema.optional(),
});
// =======================================================
// DOCUMENTS VALIDATION
// =======================================================
export const hostDocumentsSchema = z.array(
z.object({
documentTypeXid: z.number(),
documentName: z.string().max(50, "Document name cannot exceed 50 characters"), // per parent max 50 / host max 20 — safe limit 50
fieldName: z.string(),
owner: z.enum(['host', 'parent']).optional(),
isOptional: z.boolean().optional(),
})
);

View File

@@ -0,0 +1,13 @@
/**
* Host Module Validation Schemas Index
* Export all host-related validation schemas
*/
// Authentication & Onboarding
export * from './auth.validation';
export * from './login.validation';
export * from './addPaymentDetails.validation';
export * from './hostCompanyDetails.validation';
// Activity Management
export * from './activity.validation';

View File

@@ -0,0 +1,16 @@
/**
* Host Login Validation Schema
* Production-ready Zod validation for host login
*/
import { z } from 'zod';
import { emailSchema } from '../validation.utils';
export const loginForHostSchema = z.object({
emailAddress: emailSchema,
userPassword: z
.string()
.min(1, 'Password is required'),
});
export type LoginForHostInput = z.infer<typeof loginForHostSchema>;

View File

@@ -0,0 +1,16 @@
/**
* Validation Module Index
* Central export for all validation schemas and utilities
*/
// Validation Utilities
export * from './validation.utils';
// Host Module Validations
export * as hostValidation from './host';
// Minglar Admin Module Validations
export * as minglarValidation from './minglaradmin';
// Prepopulate Module Validations
export * as prepopulateValidation from './prepopulate';

View File

@@ -0,0 +1,107 @@
/**
* Minglar Admin Authentication Validation Schemas
* Production-ready Zod validations for admin authentication
* Compatible with Zod v4
*/
import { z } from 'zod';
import { emailSchema, simplePasswordSchema, otpSchema } from '../validation.utils';
// ============================================
// REGISTRATION
// ============================================
/**
* Minglar admin registration schema
*/
export const minglarRegistrationSchema = z.object({
email: emailSchema,
});
export type MinglarRegistrationInput = z.infer<typeof minglarRegistrationSchema>;
// ============================================
// LOGIN
// ============================================
/**
* Minglar admin login schema
*/
export const minglarLoginSchema = z.object({
emailAddress: emailSchema,
userPassword: z
.string()
.min(1, 'Password is required'),
});
export type MinglarLoginInput = z.infer<typeof minglarLoginSchema>;
// ============================================
// CREATE PASSWORD
// ============================================
/**
* Create password schema with confirmation
*/
export const minglarCreatePasswordSchema = z
.object({
password: simplePasswordSchema,
confirmPassword: z.string().min(1, 'Confirm password is required'),
})
.refine((data) => data.password === data.confirmPassword, {
message: 'Password and confirm password do not match',
path: ['confirmPassword'],
});
export type MinglarCreatePasswordInput = z.infer<typeof minglarCreatePasswordSchema>;
// ============================================
// VERIFY OTP
// ============================================
/**
* OTP verification schema
*/
export const minglarVerifyOtpSchema = z.object({
otp: otpSchema,
});
export type MinglarVerifyOtpInput = z.infer<typeof minglarVerifyOtpSchema>;
// ============================================
// UPDATE PROFILE
// ============================================
/**
* Admin profile update schema
*/
export const minglarUpdateProfileSchema = z.object({
firstName: z
.string()
.min(1, 'First name is required')
.max(50, 'First name cannot exceed 50 characters')
.optional(),
lastName: z
.string()
.min(1, 'Last name is required')
.max(50, 'Last name cannot exceed 50 characters')
.optional(),
mobileNumber: z
.string()
.max(15, 'Mobile number cannot exceed 15 digits')
.regex(/^[0-9]*$/, 'Mobile number must contain only digits')
.optional(),
isdCode: z
.string()
.max(6, 'ISD code cannot exceed 6 characters')
.optional(),
dateOfBirth: z
.string()
.refine((val) => !val || !isNaN(Date.parse(val)), 'Invalid date format')
.optional(),
profileImage: z
.string()
.max(500, 'Profile image path cannot exceed 500 characters')
.optional(),
});
export type MinglarUpdateProfileInput = z.infer<typeof minglarUpdateProfileSchema>;

View File

@@ -0,0 +1,252 @@
/**
* Minglar Admin Host Hub Validation Schemas
* Production-ready Zod validations for host management by admins
*/
import { z } from 'zod';
import { idSchema, searchQuerySchema, paginationSchema } from '../validation.utils';
// ============================================
// GET ALL HOST APPLICATIONS
// ============================================
/**
* Query params for getting all host applications
*/
export const getAllHostApplicationsQuerySchema = z.object({
search: searchQuerySchema,
userStatus: z
.string()
.max(20, 'Status cannot exceed 20 characters')
.optional(),
roleFilter: z
.string()
.max(30, 'Role filter cannot exceed 30 characters')
.optional(),
...paginationSchema.shape,
});
export type GetAllHostApplicationsQuery = z.infer<typeof getAllHostApplicationsQuerySchema>;
// ============================================
// ASSIGN AM TO HOST
// ============================================
/**
* Assign account manager to host schema
*/
export const assignAmToHostSchema = z.object({
hostXid: idSchema.describe('Host ID'),
accountManagerXid: idSchema.describe('Account Manager ID'),
});
export type AssignAmToHostInput = z.infer<typeof assignAmToHostSchema>;
// ============================================
// UPDATE HOST STATUS
// ============================================
/**
* Update host status schema
*/
export const updateHostStatusSchema = z.object({
hostXid: idSchema.describe('Host ID'),
status: z
.enum(['approved', 'rejected', 'pending', 'resubmit'])
.describe('New host status'),
remarks: z
.string()
.max(500, 'Remarks cannot exceed 500 characters')
.optional(),
});
export type UpdateHostStatusInput = z.infer<typeof updateHostStatusSchema>;
// ============================================
// GET HOST BY ID
// ============================================
/**
* Get host by ID query params
*/
export const getHostByIdQuerySchema = z.object({
hostXid: z.coerce
.number()
.int('Host ID must be an integer')
.positive('Host ID must be positive'),
});
export type GetHostByIdQuery = z.infer<typeof getHostByIdQuerySchema>;
// ============================================
// ADD HOST SUGGESTION
// ============================================
/**
* Add suggestion to host schema
*/
export const addHostSuggestionSchema = z.object({
hostXid: idSchema.describe('Host ID'),
title: z
.string()
.min(1, 'Title is required')
.max(100, 'Title cannot exceed 100 characters'),
comments: z
.string()
.min(1, 'Comments are required')
.max(500, 'Comments cannot exceed 500 characters'),
isParent: z.boolean().optional().default(false),
});
export type AddHostSuggestionInput = z.infer<typeof addHostSuggestionSchema>;
// ============================================
// AGREEMENT SETTINGS
// ============================================
/**
* Update agreement settings schema
*/
export const updateAgreementSettingsSchema = z.object({
hostXid: idSchema.describe('Host ID'),
agreementStartDate: z
.string()
.refine((val) => !isNaN(Date.parse(val)), 'Invalid date format'),
durationNumber: z
.number()
.int('Duration must be an integer')
.positive('Duration must be positive'),
durationFrequency: z.enum(['days', 'months', 'years']),
isCommisionBase: z.boolean(),
commisionPer: z
.number()
.min(0, 'Commission percentage must be at least 0')
.max(100, 'Commission percentage cannot exceed 100')
.optional(),
amountPerBooking: z
.number()
.int('Amount must be an integer')
.positive('Amount must be positive')
.optional(),
payoutDurationNum: z
.number()
.int('Payout duration must be an integer')
.positive('Payout duration must be positive')
.optional(),
payoutDurationFrequency: z.enum(['days', 'months', 'years']).optional(),
});
export type UpdateAgreementSettingsInput = z.infer<typeof updateAgreementSettingsSchema>;
// ============================================
// ACCEPT/REJECT HOST APPLICATION (by hostXid)
// ============================================
/**
* Host application action schema (accept/reject by AM or Admin)
*/
export const hostApplicationActionSchema = z.object({
hostXid: idSchema.describe('Host ID'),
});
export type HostApplicationActionInput = z.infer<typeof hostApplicationActionSchema>;
// ============================================
// ADD PQQ SUGGESTION
// ============================================
/**
* Add PQQ suggestion schema
*/
export const addPqqSuggestionSchema = z.object({
title: z
.string()
.min(1, 'Title is required')
.max(100, 'Title cannot exceed 100 characters'),
comments: z
.string()
.min(1, 'Comments are required')
.max(500, 'Comments cannot exceed 500 characters'),
activity_pqq_header_xid: idSchema.describe('Activity PQQ Header ID'),
});
export type AddPqqSuggestionInput = z.infer<typeof addPqqSuggestionSchema>;
// ============================================
// GET HOST BY ID (path param)
// ============================================
/**
* Get host by ID from path params
*/
export const getHostByIdPathSchema = z.object({
host_xid: z.coerce
.number()
.int('Host ID must be an integer')
.positive('Host ID must be positive'),
});
export type GetHostByIdPathInput = z.infer<typeof getHostByIdPathSchema>;
/**
* Get host by ID from path params (alternative with 'id')
*/
export const getHostByIdAltPathSchema = z.object({
id: z.coerce
.number()
.int('Host ID must be an integer')
.positive('Host ID must be positive'),
});
export type GetHostByIdAltPathInput = z.infer<typeof getHostByIdAltPathSchema>;
// ============================================
// REJECT PQQ BY AM
// ============================================
/**
* Reject PQQ by AM schema
*/
export const rejectPqqByAmSchema = z.object({
activityId: idSchema.describe('Activity ID'),
});
export type RejectPqqByAmInput = z.infer<typeof rejectPqqByAmSchema>;
// ============================================
// EDIT AGREEMENT DETAILS
// ============================================
/**
* Edit agreement details schema
*/
export const editAgreementDetailsSchema = z.object({
host_xid: idSchema.describe('Host ID'),
agreementStartDate: z
.string()
.min(1, 'Agreement start date is required')
.refine((val) => !isNaN(Date.parse(val)), 'Invalid date format'),
duration: z
.number()
.int('Duration must be an integer')
.positive('Duration must be positive'),
isCommisionBase: z.boolean(),
commisionPer: z
.number()
.min(0, 'Commission percentage must be at least 0')
.max(100, 'Commission percentage cannot exceed 100')
.optional(),
amountPerBooking: z
.number()
.int('Amount must be an integer')
.positive('Amount must be positive')
.optional(),
durationFrequency: z.string().min(1, 'Duration frequency is required'),
payoutDurationNum: z
.number()
.int('Payout duration must be an integer')
.positive('Payout duration must be positive')
.optional(),
payoutDurationFrequency: z.string().optional(),
});
export type EditAgreementDetailsInput = z.infer<typeof editAgreementDetailsSchema>;

View File

@@ -0,0 +1,13 @@
/**
* Minglar Admin Module Validation Schemas Index
* Export all minglar admin-related validation schemas
*/
// Authentication
export * from './auth.validation';
// Teammate Management
export * from './teammate.validation';
// Host Hub Management
export * from './hosthub.validation';

View File

@@ -0,0 +1,117 @@
/**
* Minglar Admin Teammate Management Validation Schemas
* Production-ready Zod validations for teammate/invite management
*/
import { z } from 'zod';
import { emailSchema, idSchema, searchQuerySchema, paginationSchema } from '../validation.utils';
import { ROLE } from '../../constants/common.constant';
// ============================================
// INVITE TEAMMATE
// ============================================
/**
* Invite teammate schema
*/
export const inviteTeammateSchema = z.object({
emailAddress: emailSchema,
roleXid: z
.number()
.int('Role ID must be an integer')
.refine(
(val) => [ROLE.CO_ADMIN, ROLE.ACCOUNT_MANAGER].includes(val),
'Invalid role. Only Co-Admin and Account Manager roles can be assigned'
),
isFixedSalary: z.boolean(),
perValue: z
.number()
.positive('Per value must be greater than 0')
.optional(),
}).refine(
(data) => {
// If Account Manager and not fixed salary, perValue is required
if (data.roleXid === ROLE.ACCOUNT_MANAGER && !data.isFixedSalary) {
return data.perValue !== undefined && data.perValue > 0;
}
return true;
},
{
message: 'Per value is required for commission-based Account Managers',
path: ['perValue'],
}
);
export type InviteTeammateInput = z.infer<typeof inviteTeammateSchema>;
// ============================================
// GET ALL COADMIN AND AM (with search)
// ============================================
/**
* Query params for getting co-admins and account managers
*/
export const getAllCoadminAndAMQuerySchema = z.object({
search: searchQuerySchema,
});
export type GetAllCoadminAndAMQuery = z.infer<typeof getAllCoadminAndAMQuerySchema>;
// ============================================
// GET ALL INVITED COADMIN AND AM (with search)
// ============================================
/**
* Query params for getting invited co-admins and account managers
*/
export const getAllInvitedCoadminAndAMQuerySchema = z.object({
search: searchQuerySchema,
});
export type GetAllInvitedCoadminAndAMQuery = z.infer<typeof getAllInvitedCoadminAndAMQuerySchema>;
// ============================================
// GET INVITATION DETAILS (with search)
// ============================================
/**
* Query params for getting invitation details
*/
export const getInvitationDetailsQuerySchema = z.object({
search: searchQuerySchema,
});
export type GetInvitationDetailsQuery = z.infer<typeof getInvitationDetailsQuerySchema>;
// ============================================
// UPDATE TEAMMATE STATUS
// ============================================
/**
* Update teammate status schema
*/
export const updateTeammateStatusSchema = z.object({
userId: idSchema.describe('User ID'),
status: z.enum(['active', 'inactive', 'suspended']),
});
export type UpdateTeammateStatusInput = z.infer<typeof updateTeammateStatusSchema>;
// ============================================
// ASSIGN REVENUE
// ============================================
/**
* Assign revenue to teammate schema
*/
export const assignRevenueSchema = z.object({
userId: idSchema.describe('User ID'),
isFixedSalary: z.boolean(),
perValue: z
.number()
.positive('Per value must be greater than 0'),
});
export type AssignRevenueInput = z.infer<typeof assignRevenueSchema>;

View File

@@ -0,0 +1,6 @@
/**
* Prepopulate Module Validation Schemas Index
* Export all prepopulate-related validation schemas
*/
export * from './prepopulate.validation';

View File

@@ -0,0 +1,53 @@
/**
* Prepopulate Module Validation Schemas
* Production-ready Zod validations for prepopulate endpoints
*/
import { z } from 'zod';
// ============================================
// GET BANK BRANCHES BY BANK ID
// ============================================
/**
* Get bank branches by bank ID query params
*/
export const getBankBranchesByBankIdQuerySchema = z.object({
bankXid: z.coerce
.number()
.int('Bank ID must be an integer')
.positive('Bank ID must be positive'),
});
export type GetBankBranchesByBankIdQuery = z.infer<typeof getBankBranchesByBankIdQuerySchema>;
// ============================================
// GET CITY BY STATE ID
// ============================================
/**
* Get city by state ID query params
*/
export const getCityByStateIdQuerySchema = z.object({
stateXid: z.coerce
.number()
.int('State ID must be an integer')
.positive('State ID must be positive'),
});
export type GetCityByStateIdQuery = z.infer<typeof getCityByStateIdQuerySchema>;
// ============================================
// GET AM DETAIL BY ID (path param)
// ============================================
/**
* Get AM detail by ID path params
*/
export const getAmDetailByIdPathSchema = z.object({
amXid: z.coerce
.number()
.int('Account Manager ID must be an integer')
.positive('Account Manager ID must be positive'),
});
export type GetAmDetailByIdPath = z.infer<typeof getAmDetailByIdPathSchema>;

View File

@@ -0,0 +1,358 @@
/**
* Production-Ready Validation Utilities
* Centralized validation helpers for Zod schemas
* Compatible with Zod v4
*/
import { z, ZodError, ZodSchema } from 'zod';
import ApiError from '../helper/ApiError';
/**
* Formats Zod validation errors into user-friendly messages
*/
export function formatZodErrors(error: ZodError): string {
return error.issues
.map((issue) => {
const path = issue.path.length > 0 ? `${issue.path.join('.')}: ` : '';
return `${path}${issue.message}`;
})
.join('; ');
}
/**
* Validates data against a Zod schema and throws ApiError on failure
* @param schema - Zod schema to validate against
* @param data - Data to validate
* @param errorCode - HTTP status code for validation errors (default: 400)
* @returns Validated and typed data
*/
export function validateSchema<T>(
schema: ZodSchema<T>,
data: unknown,
errorCode: number = 400
): T {
const result = schema.safeParse(data);
if (!result.success) {
const errorMessage = formatZodErrors(result.error);
throw new ApiError(errorCode, `Validation failed: ${errorMessage}`);
}
return result.data;
}
/**
* Safe validation that returns result object instead of throwing
*/
export function safeValidate<T>(
schema: ZodSchema<T>,
data: unknown
): { success: true; data: T } | { success: false; errors: string } {
const result = schema.safeParse(data);
if (!result.success) {
return {
success: false,
errors: formatZodErrors(result.error),
};
}
return {
success: true,
data: result.data,
};
}
/**
* Parses JSON body from API Gateway event safely
*/
export function parseBody<T>(
body: string | null,
schema: ZodSchema<T>
): T {
if (!body) {
throw new ApiError(400, 'Request body is required');
}
let parsedBody: unknown;
try {
parsedBody = JSON.parse(body);
} catch {
throw new ApiError(400, 'Invalid JSON in request body');
}
return validateSchema(schema, parsedBody);
}
/**
* Parses query string parameters with validation
*/
export function parseQueryParams<T>(
params: Record<string, string | undefined> | null,
schema: ZodSchema<T>
): T {
return validateSchema(schema, params || {});
}
// ============================================
// COMMON REUSABLE FIELD SCHEMAS (Zod v4 compatible)
// ============================================
/**
* Email validation with proper format
*/
export const emailSchema = z
.string()
.min(1, 'Email is required')
.email('Invalid email format')
.max(150, 'Email cannot exceed 150 characters')
.transform((val) => val.toLowerCase().trim());
/**
* Password validation with security requirements
*/
export const passwordSchema = z
.string()
.min(8, 'Password must be at least 8 characters')
.max(255, 'Password cannot exceed 255 characters')
.regex(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/,
'Password must contain at least one uppercase letter, one lowercase letter, one number and one special character'
);
/**
* Simple password (for cases where strong password isn't required)
*/
export const simplePasswordSchema = z
.string()
.min(8, 'Password must be at least 8 characters')
.max(255, 'Password cannot exceed 255 characters');
/**
* Mobile number validation (international format)
*/
export const mobileNumberSchema = z
.string()
.min(7, 'Mobile number must be at least 7 digits')
.max(15, 'Mobile number cannot exceed 15 digits')
.regex(/^[0-9]+$/, 'Mobile number must contain only digits')
.optional();
/**
* ISD Code validation
*/
export const isdCodeSchema = z
.string()
.max(6, 'ISD code cannot exceed 6 characters')
.regex(/^\+?[0-9]+$/, 'Invalid ISD code format')
.optional();
/**
* Name validation (first name, last name, etc.)
*/
export const nameSchema = z
.string()
.min(1, 'Name is required')
.max(50, 'Name cannot exceed 50 characters')
.regex(/^[a-zA-Z\s'-]+$/, 'Name can only contain letters, spaces, hyphens and apostrophes');
/**
* Optional name schema
*/
export const optionalNameSchema = nameSchema.optional();
/**
* Positive integer ID validation
*/
export const idSchema = z
.number()
.int('ID must be an integer')
.positive('ID must be a positive number');
/**
* Optional positive integer ID
*/
export const optionalIdSchema = z
.number()
.int('ID must be an integer')
.positive('ID must be a positive number')
.optional();
/**
* Coerce string to number for query params
*/
export const numericStringSchema = z
.string()
.transform((val) => parseInt(val, 10))
.pipe(z.number().int().positive());
/**
* Optional numeric string
*/
export const optionalNumericStringSchema = z
.string()
.optional()
.transform((val) => (val ? parseInt(val, 10) : undefined))
.pipe(z.number().int().positive().optional());
/**
* Address line validation
*/
export const addressLine1Schema = z
.string()
.min(1, 'Address is required')
.max(150, 'Address cannot exceed 150 characters');
export const addressLine2Schema = z
.string()
.max(150, 'Address cannot exceed 150 characters')
.optional();
/**
* Pin/Zip code validation
*/
export const pinCodeSchema = z
.string()
.min(4, 'Pin code must be at least 4 characters')
.max(30, 'Pin code cannot exceed 30 characters');
/**
* URL validation
*/
export const urlSchema = z
.string()
.url('Invalid URL format')
.max(500, 'URL cannot exceed 500 characters')
.optional()
.or(z.literal(''));
/**
* Social media URL (with empty string handling)
*/
export const socialUrlSchema = z
.string()
.max(80, 'URL cannot exceed 80 characters')
.refine(
(val) => !val || val === '' || z.string().url().safeParse(val).success,
'Invalid URL format'
)
.optional();
/**
* Date string validation
*/
export const dateStringSchema = z
.string()
.refine((val) => !isNaN(Date.parse(val)), 'Invalid date format')
.transform((val) => new Date(val));
/**
* Optional date string
*/
export const optionalDateStringSchema = z
.string()
.refine((val) => !val || !isNaN(Date.parse(val)), 'Invalid date format')
.optional();
/**
* Boolean schema with string coercion (for query params)
*/
export const booleanStringSchema = z
.string()
.transform((val) => val === 'true' || val === '1')
.pipe(z.boolean());
/**
* OTP validation (6 digits)
*/
export const otpSchema = z
.string()
.length(6, 'OTP must be exactly 6 digits')
.regex(/^[0-9]+$/, 'OTP must contain only digits');
/**
* Search query validation
*/
export const searchQuerySchema = z
.string()
.max(100, 'Search query cannot exceed 100 characters')
.transform((val) => val.trim())
.optional();
/**
* Pagination parameters
*/
export const paginationSchema = z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().positive().max(100).default(10),
});
/**
* File path validation for S3
*/
export const filePathSchema = z
.string()
.max(500, 'File path cannot exceed 500 characters')
.optional();
/**
* Registration/Reference number validation
*/
export const registrationNumberSchema = z
.string()
.max(30, 'Registration number cannot exceed 30 characters')
.optional();
/**
* PAN number validation (India)
*/
export const panNumberSchema = z
.string()
.max(30, 'PAN number cannot exceed 30 characters')
.regex(/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/, 'Invalid PAN number format')
.optional()
.or(z.literal(''));
/**
* GST number validation (India)
*/
export const gstNumberSchema = z
.string()
.max(30, 'GST number cannot exceed 30 characters')
.regex(
/^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}$/,
'Invalid GST number format'
)
.optional()
.or(z.literal(''));
/**
* IFSC code validation (India)
*/
export const ifscCodeSchema = z
.string()
.min(1, 'IFSC code is required')
.regex(/^[A-Z]{4}0[A-Z0-9]{6}$/, 'Invalid IFSC code format');
/**
* Bank account number validation
*/
export const accountNumberSchema = z
.string()
.min(9, 'Account number must be at least 9 digits')
.max(18, 'Account number cannot exceed 18 digits')
.regex(/^[0-9]+$/, 'Account number must contain only digits');
/**
* Company name validation
*/
export const companyNameSchema = z
.string()
.min(1, 'Company name is required')
.max(100, 'Company name cannot exceed 100 characters');
/**
* Token validation
*/
export const tokenSchema = z
.string()
.min(1, 'Authentication token is required');

View File

@@ -12,13 +12,12 @@ 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'),
JWT_ACCESS_EXPIRATION_MINUTES: yup
.number()
.default(30)
.default(1440)
.required('minutes after which access tokens expire'),
JWT_REFRESH_EXPIRATION_DAYS: yup
.number()
@@ -32,32 +31,37 @@ const envVarsSchema = yup
.number()
.default(10)
.required('minutes after which verify email token expires'),
AWS_REGION: yup.string().required('AWS region is required'),
S3_BUCKET_NAME: yup.string().required('S3 bucket name is required'),
//SMTP and BREVO
// BREVO_SMTP_HOST: yup
// .string()
// .nullable()
// .required('server that will send the emails'),
// BREVO_SMTP_PORT: yup
// .number()
// .nullable()
// .required('port to connect to the email server'),
// BREVO_SMTP_USER: yup
// .string()
// .nullable()
// .required('username for email server'),
// BREVO_SMTP_PASS: yup
// .string()
// .nullable()
// .required('password for email server'),
// BREVO_FROM_EMAIL: yup
// .string()
// .nullable()
// .required('the from field in the emails sent by the app'),
// BREVO_EMAIL_API_KEY: yup
// .string()
// .nullable()
// .required('the from field in the emails sent by the app api key'),
// BREVO_API_BASEURL: yup.string().required('Brevo base URL is required'),
BREVO_SMTP_HOST: yup
.string()
.nullable()
.required('server that will send the emails'),
BREVO_SMTP_PORT: yup
.number()
.nullable()
.required('port to connect to the email server'),
BREVO_SMTP_USER: yup
.string()
.nullable()
.required('username for email server'),
BREVO_SMTP_PASS: yup
.string()
.nullable()
.required('password for email server'),
BREVO_FROM_EMAIL: yup
.string()
.nullable()
.required('the from field in the emails sent by the app'),
BREVO_EMAIL_API_KEY: yup
.string()
.nullable()
.required('the from field in the emails sent by the app api key'),
BREVO_API_BASEURL: yup.string().required('Brevo base URL is required'),
// Minglar Admin
MINGLAR_ADMIN_EMAIL: yup.string().required('Minglar admin email address is required.'),
MINGLAR_ADMIN_NAME: yup.string().required('Minglar admin name is required.'),
// //one signal
// ONESIGNAL_APPID: yup.string().required('One signal app id is required'),
// ONESIGNAL_REST_APIKEY: yup
@@ -130,23 +134,30 @@ function getConfig() {
socketPath: '/var/run/mysqld/mysqld.sock',
},
},
aws: {
region: envVars.AWS_REGION,
bucketName: envVars.S3_BUCKET_NAME,
},
byPassOTP: envVars.BYPASS_OTP,
// BaseURL: envVars.BASEURL,
// FRONTEND_URL: envVars.FRONTEND_URL,
// email: {
// smtp: {
// host: envVars?.BREVO_SMTP_HOST,
// port: envVars?.BREVO_SMTP_PORT,
// secure: envVars?.BREVO_SMTP_PORT == 465, // true for 465, false for other ports
// auth: {
// user: envVars?.BREVO_SMTP_USER,
// pass: envVars?.BREVO_SMTP_PASS,
// },
// },
// from: envVars?.BREVO_FROM_EMAIL,
// api_key: envVars?.BREVO_EMAIL_API_KEY,
// BrevobaseURL: envVars?.BREVO_API_BASEURL,
// },
email: {
smtp: {
host: envVars?.BREVO_SMTP_HOST,
port: envVars?.BREVO_SMTP_PORT,
secure: envVars?.BREVO_SMTP_PORT == 465, // true for 465, false for other ports
auth: {
user: envVars?.BREVO_SMTP_USER,
pass: envVars?.BREVO_SMTP_PASS,
},
},
from: envVars?.BREVO_FROM_EMAIL,
api_key: envVars?.BREVO_EMAIL_API_KEY,
BrevobaseURL: envVars?.BREVO_API_BASEURL,
},
//Minglar admin
MinglarAdminEmail: envVars.MINGLAR_ADMIN_EMAIL,
MinglarAdminName: envVars.MINGLAR_ADMIN_NAME,
// oneSignal: {
// appID: envVars.ONESIGNAL_APPID,
// restApiKey: envVars.ONESIGNAL_REST_APIKEY,

View File

@@ -16,7 +16,7 @@ async function bootstrap() {
app.enableCors({
origin: [
'http://localhost:3000', // Local Swagger UI
'http://localhost:3006', // Local Frontend
'http://localhost:5173', // Local Frontend
'https://editor.swagger.io', // Swagger Editor
'https://klc-admin.wdiprojects.com',
'https://admin-uat.klc.betadelivery.com',

View File

@@ -21,7 +21,7 @@ export class CreateHostDto {
@IsOptional()
@IsString()
userPasscode?: string;
userPassword?: string;
@IsOptional()
@IsInt()
@@ -49,3 +49,46 @@ 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;
}
}
export class AddPaymentDetailsDTO {
bankXid: number;
bankBranchXid: number;
accountNumber: string;
accountHolderName: string;
ifscCode: string;
hostXid: number;
currencyXid: number;
constructor(bankXid: number, bankBranchXid: number, accountNumber: string, accountHolderName: string, ifscCode: string, hostXid: number, currencyXid: number) {
this.bankXid = bankXid;
this.bankBranchXid = bankBranchXid;
this.accountNumber = accountNumber;
this.accountHolderName = accountHolderName;
this.ifscCode = ifscCode;
this.hostXid = hostXid;
}
}

View File

@@ -0,0 +1,51 @@
import { verifyHostToken } from '@/common/middlewares/jwt/authForHost';
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { HostService } from '../../../services/host.service';
import { PrePopulateService } from '../../../../prepopulate/services/prepopulate.service';
import { parseQueryParams } from '../../../../../common/utils/validation/validation.utils';
import { optionalSearchQuerySchema } from '../../../../../common/utils/validation/host/activity.validation';
const prismaService = new PrismaService();
const hostService = new HostService(prismaService);
const prePopulateService = new PrePopulateService(prismaService);
/**
* Get all activity types with interest handler
*/
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
// Verify authentication token
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(401, 'This is a protected route. Please provide a valid token.');
}
// Verify token and get user info
const userInfo = await verifyHostToken(token);
// Parse and validate query params using Zod
const { search, q } = parseQueryParams(event.queryStringParameters, optionalSearchQuerySchema);
const searchTerm = search || q || undefined;
const data = await hostService.getAllActivityTypesWithInterest(searchTerm);
const frequencies = await prePopulateService.getAllFrequencies();
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Data retrieved successfully',
data,
frequencies
}),
};
});

View File

@@ -0,0 +1,51 @@
import { verifyHostToken } from '@/common/middlewares/jwt/authForHost';
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { HostService } from '../../../services/host.service';
import { parseQueryParams } from '../../../../../common/utils/validation/validation.utils';
import { optionalSearchQuerySchema } from '../../../../../common/utils/validation/host/activity.validation';
const prismaService = new PrismaService();
const hostService = new HostService(prismaService);
/**
* Get all host activities handler
*/
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
// Verify authentication token
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(401, 'This is a protected route. Please provide a valid token.');
}
// Verify token and get user info
const userInfo = await verifyHostToken(token);
// Parse and validate query params using Zod
const { search, q } = parseQueryParams(event.queryStringParameters, optionalSearchQuerySchema);
const searchTerm = search || q || undefined;
const data = await hostService.getAllHostActivity(searchTerm, Number(userInfo.id));
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Data retrieved successfully',
data,
}),
};
});

View File

@@ -0,0 +1,41 @@
import { verifyOnlyMinglarAdminToken } from '@/common/middlewares/jwt/authForOnlyMinglarAdmin';
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { PrePopulateService } from '../../../../prepopulate/services/prepopulate.service';
import { HostService } from '../../../services/host.service';
import { verifyMinglarAdminHostToken } from '@/common/middlewares/jwt/authForMinglarAdmin&Host';
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
await verifyMinglarAdminHostToken(token);
const result = await hostService.getAllPQQQuesAndSubmittedAns();
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Data retrieved successfully',
data: result,
}),
};
});

View File

@@ -0,0 +1,310 @@
import config from '@/config/config';
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import AWS from 'aws-sdk';
import Busboy from 'busboy';
import crypto from 'crypto';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { HostService } from '../../../services/host.service';
import { LAST_QUESTION_ID } from '@/common/utils/constants/host.constant';
const prisma = new PrismaService();
const pqqService = new HostService(prisma);
const s3 = new AWS.S3({ region: config.aws.region });
// Function to extract S3 key from URL
function getS3KeyFromUrl(url: string): string {
const bucketBaseUrl = `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/`;
return url.replace(bucketBaseUrl, '');
}
// Function to delete file from S3
async function deleteFromS3(s3Key: string): Promise<void> {
try {
await s3.deleteObject({
Bucket: config.aws.bucketName,
Key: s3Key,
}).promise();
console.log(`✅ File deleted from S3: ${s3Key}`);
} catch (error) {
console.error(`❌ Error deleting file from S3: ${s3Key}`, error);
// Don't throw error here, continue with upload
}
}
async function uploadToS3(buffer: Buffer, mimeType: string, originalName: string, prefix: string, existingUrl?: string): Promise<string> {
let s3Key: string;
// If existing URL provided, use the same S3 key to replace the file
if (existingUrl) {
s3Key = getS3KeyFromUrl(existingUrl);
// Delete existing file first
await deleteFromS3(s3Key);
} else {
// Generate new unique key for new file
const uniqueKey = `${crypto.randomUUID()}_${originalName}`;
s3Key = `${prefix}/${uniqueKey}`;
}
// Upload new file (replaces existing if same key)
await s3.upload({
Bucket: config.aws.bucketName,
Key: s3Key,
Body: buffer,
ContentType: mimeType,
ACL: 'private'
}).promise();
console.log(`✅ File uploaded to S3: ${s3Key}`);
return `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${s3Key}`;
}
export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
try {
// 1) Auth
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) throw new ApiError(401, 'Missing token.');
const user = await verifyHostToken(token);
// 2) Content-Type check
const contentType = event.headers["content-type"] || event.headers["Content-Type"];
if (!contentType?.startsWith("multipart/form-data"))
throw new ApiError(400, "Content-Type must be multipart/form-data");
if (!event.isBase64Encoded)
throw new ApiError(400, "Body must be base64 encoded");
const bodyBuffer = Buffer.from(event.body!, "base64");
const fields: any = {};
const files: Array<{ buffer: Buffer; mimeType: string; fileName: string; fieldName: string }> = [];
// 3) Parse multipart data
await new Promise<void>((resolve, reject) => {
const bb = Busboy({ headers: { 'content-type': contentType } });
bb.on('file', (fieldname, file, info) => {
const { filename, mimeType } = info;
// Skip if no filename (empty file field)
if (!filename) {
file.resume();
return;
}
const chunks: Buffer[] = [];
let totalSize = 0;
const MAX_SIZE = 5 * 1024 * 1024; // 5 MB
file.on('data', (chunk) => {
totalSize += chunk.length;
if (totalSize > MAX_SIZE) {
file.resume();
return reject(new ApiError(400, `File ${filename} exceeds 5MB limit.`));
}
chunks.push(chunk);
});
file.on('end', () => {
// Only add file if we have data
if (chunks.length > 0) {
files.push({
buffer: Buffer.concat(chunks),
mimeType,
fileName: filename,
fieldName: fieldname,
});
}
});
file.on('error', (err) => {
reject(new ApiError(400, `File upload error: ${err.message}`));
});
});
bb.on('field', (fieldname, val) => {
// Handle empty or null values
if (val === '' || val === 'null' || val === 'undefined') {
fields[fieldname] = null;
} else {
try {
fields[fieldname] = JSON.parse(val);
} catch {
fields[fieldname] = val;
}
}
});
bb.on('close', () => {
console.log("✅ Busboy parsing completed");
console.log("📌 Fields:", fields);
console.log("📁 Files:", files.length);
resolve();
});
bb.on('error', (err) => {
console.error("❌ Busboy error:", err);
reject(new ApiError(400, `Multipart parsing error: ${err.message}`));
});
bb.end(bodyBuffer);
});
// 4) Extract required fields - only activityXid, pqqQuestionXid, pqqAnswerXid are required
const activityXid = Number(fields.activityXid);
const pqqQuestionXid = Number(fields.pqqQuestionXid);
const pqqAnswerXid = Number(fields.pqqAnswerXid);
// Comments and files are optional
const comments = fields.comments || null;
// Validate required fields
if (!activityXid || isNaN(activityXid)) throw new ApiError(400, "Valid activityXid is required");
if (!pqqQuestionXid || isNaN(pqqQuestionXid)) throw new ApiError(400, "Valid pqqQuestionXid is required");
if (pqqQuestionXid !== LAST_QUESTION_ID.Q_ID) throw new ApiError(400, "Wrong question id.")
if (!pqqAnswerXid || isNaN(pqqAnswerXid)) throw new ApiError(400, "Valid pqqAnswerXid is required");
// console.log(`📝 Processing - Activity: ${activityXid}, Question: ${pqqQuestionXid}, Answer: ${pqqAnswerXid}`);
// console.log(`💬 Comments: ${comments ? 'Provided' : 'Not provided'}`);
// console.log(`📎 Files: ${files.length}`);
// 5) UPSERT: Check if header already exists for this combination
const existingHeader = await pqqService.findHeaderByCompositeKey(
activityXid,
pqqQuestionXid,
pqqAnswerXid
);
let header;
if (existingHeader) {
console.log("🔄 Updating existing PQQ header");
// Update existing header (comments can be null)
header = await pqqService.updateHeader(
existingHeader.id,
comments
);
} else {
console.log("🆕 Creating new PQQ header");
// Create new header (comments can be null)
header = await pqqService.createHeader(
activityXid,
pqqQuestionXid,
pqqAnswerXid,
comments
);
}
// Calculate score after answer submission
const score = await pqqService.calculatePqqScoreForUser(activityXid);
// 6) Get existing supporting files for this header
const existingSupportingFiles = await pqqService.getSupportingFilesByHeaderId(header.id);
console.log(`📁 Found ${existingSupportingFiles.length} existing supporting files`);
// 7) Handle file UPSERT - only if files are provided
const uploadedFiles: any[] = [];
if (files.length > 0) {
console.log("📤 Processing file uploads...");
for (let i = 0; i < files.length; i++) {
const file = files[i];
const existingFile = existingSupportingFiles[i] || null;
const url = await uploadToS3(
file.buffer,
file.mimeType,
file.fileName,
`ActivityOnboarding/supportings/${activityXid}`,
existingFile ? existingFile.mediaFileName : undefined
);
let supporting;
if (existingFile) {
// Update existing supporting file record
supporting = await pqqService.updateSupportingFile(
existingFile.id,
file.mimeType,
url
);
console.log(`🔄 Updated supporting file: ${existingFile.id}`);
} else {
// Create new supporting file record
supporting = await pqqService.addSupportingFile(
header.id,
file.mimeType,
url
);
console.log(`🆕 Created new supporting file: ${supporting.id}`);
}
uploadedFiles.push(supporting);
}
// 8) Delete any remaining existing files that weren't replaced
if (existingSupportingFiles.length > files.length) {
const filesToDelete = existingSupportingFiles.slice(files.length);
console.log(`🗑️ Deleting ${filesToDelete.length} unused supporting files`);
for (const fileToDelete of filesToDelete) {
await pqqService.deleteSupportingFile(fileToDelete.id);
// Also delete from S3
const s3Key = getS3KeyFromUrl(fileToDelete.mediaFileName);
await deleteFromS3(s3Key);
console.log(`🗑️ Deleted supporting file: ${fileToDelete.id}`);
}
}
} else {
console.log("📭 No files provided in request");
// If no files provided but existing files exist, delete them (cleanup)
if (existingSupportingFiles.length > 0) {
console.log(`🗑️ No new files provided, deleting ${existingSupportingFiles.length} existing files`);
for (const fileToDelete of existingSupportingFiles) {
await pqqService.deleteSupportingFile(fileToDelete.id);
const s3Key = getS3KeyFromUrl(fileToDelete.mediaFileName);
await deleteFromS3(s3Key);
console.log(`🗑️ Deleted supporting file: ${fileToDelete.id}`);
}
}
}
// 9) Prepare response
const responseMessage = existingHeader ? "PQQ answer updated successfully" : "PQQ answer submitted successfully";
return {
statusCode: 200,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*"
},
body: JSON.stringify({
success: true,
message: responseMessage,
data: {
headerId: header.id,
activityXid,
pqqQuestionXid,
pqqAnswerXid,
comments: comments,
score,
files: {
uploaded: uploadedFiles,
total: uploadedFiles.length
},
operation: existingHeader ? 'updated' : 'created',
fileOperation: files.length > 0 ?
(existingSupportingFiles.length > 0 ? 'replaced' : 'added') :
(existingSupportingFiles.length > 0 ? 'removed' : 'unchanged')
}
})
};
} catch (error: any) {
console.error("❌ Error in submitPqqAnswer:", error);
throw error;
}
});

View File

@@ -0,0 +1,48 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import { PrismaService } from '../../../../../common/database/prisma.service';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { HostService } from '../../../services/host.service';
import { verifyMinglarAdminHostToken } from '@/common/middlewares/jwt/authForMinglarAdmin&Host';
import { parseQueryParams } from '../../../../../common/utils/validation/validation.utils';
import { getPqqByQuestionIdQuerySchema } from '../../../../../common/utils/validation/host/activity.validation';
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.');
}
// Verify token and get user info
const userInfo = await verifyMinglarAdminHostToken(token);
const userId = Number(userInfo.id);
// Parse and validate query params using Zod
const { question_xid, activity_xid } = parseQueryParams(
event.queryStringParameters,
getPqqByQuestionIdQuerySchema
);
// Fetch user with their HostHeader stepper info
const pqqQuestionDetails = await hostService.getPQQQuestionDetail(question_xid, activity_xid);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Stepper information retrieved successfully',
data: pqqQuestionDetails,
}),
};
});

View File

@@ -0,0 +1,55 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import { PrismaService } from '../../../../../common/database/prisma.service';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { verifyHostToken } from '@/common/middlewares/jwt/authForHost';
import { HostService } from '../../../services/host.service';
import { parseQueryParams } from '../../../../../common/utils/validation/validation.utils';
import { getLatestPqqQuestionQuerySchema } from '../../../../../common/utils/validation/host/activity.validation';
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.');
}
// Verify token and get user info
const userInfo = await verifyHostToken(token);
const userId = Number(userInfo.id);
// Parse and validate query params using Zod
const { activity_xid } = parseQueryParams(
event.queryStringParameters,
getLatestPqqQuestionQuerySchema
);
// Fetch user with their HostHeader stepper info
const pqqQuestionDetails = await hostService.getLatestQuestionDetailsPQQ(activity_xid);
const result = {
pqqQuestionXid: pqqQuestionDetails.pqqQuestionXid,
pqqAnswerXid: pqqQuestionDetails.pqqAnswerXid,
pqqSubCategoryXid: pqqQuestionDetails.pqqQuestions.pqqSubCategoryXid,
categoryXid: pqqQuestionDetails.pqqQuestions.pqqSubCategories.categoryXid
}
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Latest information retrieved successfully',
data: result,
}),
};
});

View File

@@ -0,0 +1,43 @@
import { verifyHostToken } from '@/common/middlewares/jwt/authForHost';
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { HostService } from '../../../services/host.service';
import { parseBody } from '../../../../../common/utils/validation/validation.utils';
import { createActivitySchema } from '../../../../../common/utils/validation/host/activity.validation';
const prismaService = new PrismaService();
const hostService = new HostService(prismaService);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) throw new ApiError(401, 'This is a protected route. Please provide a valid token.');
const userInfo = await verifyHostToken(token);
// Parse and validate request body using Zod
const { activityTypeXid, frequenciesXid } = parseBody(event.body, createActivitySchema);
await hostService.createActivity(
userInfo.id,
activityTypeXid,
frequenciesXid,
);
return {
statusCode: 201,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Activity created successfully',
data: null,
}),
};
});

View File

@@ -0,0 +1,304 @@
import config from '@/config/config';
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import AWS from 'aws-sdk';
import Busboy from 'busboy';
import crypto from 'crypto';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { verifyHostToken } from '@/common/middlewares/jwt/authForHost';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { HostService } from '../../../services/host.service';
const prisma = new PrismaService();
const pqqService = new HostService(prisma);
const s3 = new AWS.S3({ region: config.aws.region });
// Function to extract S3 key from URL
function getS3KeyFromUrl(url: string): string {
const bucketBaseUrl = `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/`;
return url.replace(bucketBaseUrl, '');
}
// Function to delete file from S3
async function deleteFromS3(s3Key: string): Promise<void> {
try {
await s3.deleteObject({
Bucket: config.aws.bucketName,
Key: s3Key,
}).promise();
console.log(`✅ File deleted from S3: ${s3Key}`);
} catch (error) {
console.error(`❌ Error deleting file from S3: ${s3Key}`, error);
// Don't throw error here, continue with upload
}
}
async function uploadToS3(buffer: Buffer, mimeType: string, originalName: string, prefix: string, existingUrl?: string): Promise<string> {
let s3Key: string;
// If existing URL provided, use the same S3 key to replace the file
// if (existingUrl) {
// s3Key = getS3KeyFromUrl(existingUrl);
// // Delete existing file first
// await deleteFromS3(s3Key);
// } else {
// // Generate new unique key for new file
// const uniqueKey = `${crypto.randomUUID()}_${originalName}`;
// s3Key = `${prefix}/${uniqueKey}`;
// }
if (existingUrl) {
// Delete old file, but DO NOT reuse its name
const oldKey = getS3KeyFromUrl(existingUrl);
await deleteFromS3(oldKey);
}
// Create new key always
const uniqueKey = `${crypto.randomUUID()}_${originalName}`;
s3Key = `${prefix}/${uniqueKey}`;
// Upload new file (replaces existing if same key)
await s3.upload({
Bucket: config.aws.bucketName,
Key: s3Key,
Body: buffer,
ContentType: mimeType,
ACL: 'private'
}).promise();
console.log(`✅ File uploaded to S3: ${s3Key}`);
return `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${s3Key}`;
}
export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
try {
// 1) Auth
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) throw new ApiError(401, 'Missing token.');
const user = await verifyHostToken(token);
// 2) Content-Type check
const contentType = event.headers["content-type"] || event.headers["Content-Type"];
if (!contentType?.includes("multipart/form-data"))
throw new ApiError(400, "Content-Type must be multipart/form-data");
// 3) Body decoding (FIXED same as addCompanyDetails)
const bodyBuffer = event.isBase64Encoded
? Buffer.from(event.body!, "base64")
: Buffer.from(event.body!, "binary");
const fields: any = {};
const files: Array<{ buffer: Buffer; mimeType: string; fileName: string; fieldName: string }> = [];
// 4) Parse multipart data (FIXED using bb.write + bb.end exactly like working lambda)
await new Promise<void>((resolve, reject) => {
const bb = Busboy({ headers: { 'content-type': contentType } });
bb.on('file', (fieldname, file, info) => {
const { filename, mimeType } = info;
if (!filename) {
file.resume();
return;
}
const chunks: Buffer[] = [];
let size = 0;
const MAX_SIZE = 5 * 1024 * 1024;
file.on("data", (chunk) => {
size += chunk.length;
if (size > MAX_SIZE) {
file.destroy(new Error(`File ${filename} exceeds 5MB limit.`));
return;
}
chunks.push(chunk);
});
file.on("end", () => {
if (chunks.length > 0) {
files.push({
buffer: Buffer.concat(chunks),
mimeType,
fileName: filename,
fieldName: fieldname,
});
}
});
file.on("error", (err) =>
reject(new ApiError(400, `File upload error: ${err.message}`))
);
});
bb.on("field", (fieldname, val) => {
if (val === '' || val === 'null' || val === 'undefined') fields[fieldname] = null;
else {
try {
fields[fieldname] = JSON.parse(val);
} catch {
fields[fieldname] = val;
}
}
});
bb.on("close", () => resolve());
bb.on("error", (err) =>
reject(new ApiError(400, `Multipart parsing error: ${err.message}`))
);
// IMPORTANT FIX for HTTP API
bb.write(bodyBuffer);
bb.end();
});
// 4) Extract required fields - only activityXid, pqqQuestionXid, pqqAnswerXid are required
const activityXid = Number(fields.activityXid);
const pqqQuestionXid = Number(fields.pqqQuestionXid);
const pqqAnswerXid = Number(fields.pqqAnswerXid);
// Comments and files are optional
const comments = fields.comments || null;
// Validate required fields
if (!activityXid || isNaN(activityXid)) throw new ApiError(400, "Valid activityXid is required");
if (!pqqQuestionXid || isNaN(pqqQuestionXid)) throw new ApiError(400, "Valid pqqQuestionXid is required");
if (!pqqAnswerXid || isNaN(pqqAnswerXid)) throw new ApiError(400, "Valid pqqAnswerXid is required");
// console.log(`📝 Processing - Activity: ${activityXid}, Question: ${pqqQuestionXid}, Answer: ${pqqAnswerXid}`);
// console.log(`💬 Comments: ${comments ? 'Provided' : 'Not provided'}`);
// console.log(`📎 Files: ${files.length}`);
// 5) UPSERT: Check if header already exists for this combination
const existingHeader = await pqqService.findHeaderByCompositeKey(
activityXid,
pqqQuestionXid,
pqqAnswerXid
);
let header;
if (existingHeader) {
console.log("🔄 Updating existing PQQ header");
// Update existing header (comments can be null)
header = await pqqService.updateHeader(
existingHeader.id,
comments
);
} else {
console.log("🆕 Creating new PQQ header");
// Create new header (comments can be null)
header = await pqqService.createHeader(
activityXid,
pqqQuestionXid,
pqqAnswerXid,
comments
);
}
// 6) Get existing supporting files for this header
const existingSupportingFiles = await pqqService.getSupportingFilesByHeaderId(header.id);
console.log(`📁 Found ${existingSupportingFiles.length} existing supporting files`);
// 7) Handle file UPSERT - only if files are provided
const uploadedFiles: any[] = [];
if (files.length > 0) {
console.log("📤 Processing file uploads...");
for (let i = 0; i < files.length; i++) {
const file = files[i];
const existingFile = existingSupportingFiles[i] || null;
const url = await uploadToS3(
file.buffer,
file.mimeType,
file.fileName,
`ActivityOnboarding/supportings/${activityXid}`,
existingFile ? existingFile.mediaFileName : undefined
);
let supporting;
if (existingFile) {
// Update existing supporting file record
supporting = await pqqService.updateSupportingFile(
existingFile.id,
file.mimeType,
url
);
console.log(`🔄 Updated supporting file: ${existingFile.id}`);
} else {
// Create new supporting file record
supporting = await pqqService.addSupportingFile(
header.id,
file.mimeType,
url
);
console.log(`🆕 Created new supporting file: ${supporting.id}`);
}
uploadedFiles.push(supporting);
}
// 8) Delete any remaining existing files that weren't replaced
if (existingSupportingFiles.length > files.length) {
const filesToDelete = existingSupportingFiles.slice(files.length);
console.log(`🗑️ Deleting ${filesToDelete.length} unused supporting files`);
for (const fileToDelete of filesToDelete) {
await pqqService.deleteSupportingFile(fileToDelete.id);
// Also delete from S3
const s3Key = getS3KeyFromUrl(fileToDelete.mediaFileName);
await deleteFromS3(s3Key);
console.log(`🗑️ Deleted supporting file: ${fileToDelete.id}`);
}
}
} else {
console.log("📭 No files provided in request");
// If no files provided but existing files exist, delete them (cleanup)
if (existingSupportingFiles.length > 0) {
console.log(`🗑️ No new files provided, deleting ${existingSupportingFiles.length} existing files`);
for (const fileToDelete of existingSupportingFiles) {
await pqqService.deleteSupportingFile(fileToDelete.id);
const s3Key = getS3KeyFromUrl(fileToDelete.mediaFileName);
await deleteFromS3(s3Key);
console.log(`🗑️ Deleted supporting file: ${fileToDelete.id}`);
}
}
}
// 9) Prepare response
const responseMessage = existingHeader ? "PQQ answer updated successfully" : "PQQ answer submitted successfully";
return {
statusCode: 200,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*"
},
body: JSON.stringify({
success: true,
message: responseMessage,
data: {
headerId: header.id,
activityXid,
pqqQuestionXid,
pqqAnswerXid,
comments: comments,
files: {
uploaded: uploadedFiles,
total: uploadedFiles.length
},
operation: existingHeader ? 'updated' : 'created',
fileOperation: files.length > 0 ?
(existingSupportingFiles.length > 0 ? 'replaced' : 'added') :
(existingSupportingFiles.length > 0 ? 'removed' : 'unchanged')
}
})
};
} catch (error: any) {
console.error("❌ Error in submitPqqAnswer:", error);
throw error;
}
});

View File

@@ -0,0 +1,43 @@
import { verifyHostToken } from '@/common/middlewares/jwt/authForHost';
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { HostService } from '../../../services/host.service';
import { parseBody } from '../../../../../common/utils/validation/validation.utils';
import { updateSuggestionReviewedSchema } from '../../../../../common/utils/validation/host/activity.validation';
const prismaService = new PrismaService();
const hostService = new HostService(prismaService);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) throw new ApiError(401, 'This is a protected route. Please provide a valid token.');
const userInfo = await verifyHostToken(token);
// Parse and validate body using Zod
const { activityPqqHeaderXid, activityPQQSuggestionId } = parseBody(event.body, updateSuggestionReviewedSchema);
await hostService.markPQQSuggestionReviewed(
userInfo.id,
Number(activityPqqHeaderXid),
Number(activityPQQSuggestionId)
);
return {
statusCode: 201,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Suggestion reviewed successfully',
data: null,
}),
};
});

View File

@@ -0,0 +1,45 @@
import { verifyHostToken } from '@/common/middlewares/jwt/authForHost';
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { HostService } from '../../../services/host.service';
const prismaService = new PrismaService();
const hostService = new HostService(prismaService);
/**
* Add suggestion handler for host applications
* Allows Minglar Admin, Co_Admin, and Account Manager to add suggestions
* Types: Setup Profile, Review Account, Add Payment Details, Agreement
*/
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
// Verify authentication token
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(401, 'This is a protected route. Please provide a valid token.');
}
// Verify token and get user info
const userInfo = await verifyHostToken(token);
// Add suggestion using service
await hostService.acceptMinglarAgreement(userInfo.id);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Application accepted successfully',
data: null,
}),
};
});

View File

@@ -0,0 +1,45 @@
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';
import { parseBody } from '../../../../../common/utils/validation/validation.utils';
import { createPasswordSchema } from '../../../../../common/utils/validation/host/auth.validation';
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 and validate request body using Zod
const { password } = parseBody(event.body, createPasswordSchema);
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,69 @@
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';
import { parseBody } from '../../../../../common/utils/validation/validation.utils';
import { hostLoginSchema } from '../../../../../common/utils/validation/host/auth.validation';
const prismaService = new PrismaService();
const hostService = new HostService(prismaService);
const tokenService = new TokenService(prismaService);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
// Parse and validate request body using Zod
const { emailAddress, userPassword } = parseBody(event.body, hostLoginSchema);
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,43 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { MinglarService } from '../../../../minglaradmin/services/minglar.service';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { verifyHostToken } from '@/common/middlewares/jwt/authForHost';
const prismaService = new PrismaService();
const minglarService = new MinglarService(prismaService);
/**
* Get suggestions handler
* Retrieves suggestions based on user's role and host assignments
*/
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
// Verify authentication token
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(401, 'This is a protected route. Please provide a valid token.');
}
// Verify token and get user info
const userInfo = await verifyHostToken(token);
// Get suggestions using service
const suggestions = await minglarService.getHostSuggestions(userInfo.id);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Suggestions retrieved successfully',
data: suggestions,
}),
};
});

View File

@@ -0,0 +1,110 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import * as bcrypt from 'bcryptjs';
import { OtpGeneratorSixDigit } from '../../../../../common/utils/helper/OtpGenerator';
import { encryptUserId } from '../../../../../common/utils/helper/CodeGenerator';
import { HostService } from '../../../services/host.service';
import { sendOtpEmailForHost } from '../../../services/sendOTPEmail.service';
import { ROLE } from '../../../../../common/utils/constants/common.constant';
import { parseBody } from '../../../../../common/utils/validation/validation.utils';
import { hostSignUpSchema } from '../../../../../common/utils/validation/host/auth.validation';
const prismaService = new PrismaService();
const hostService = new HostService(prismaService);
export async function generateHostRefNumber(tx: any) {
const lastrecord = await tx.user.findFirst({
orderBy: {
id: 'desc',
},
select: {
id: true,
},
});
const nextId = lastrecord ? lastrecord.id + 1 : 1;
return `HS-${String(nextId).padStart(6, '0')}`;;
}
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
// Parse and validate request body using Zod
const { email } = parseBody(event.body, hostSignUpSchema);
// Use a single transaction for user creation/lookup and OTP storage
const transactionResult = await prismaService.$transaction(async (tx) => {
const user = await tx.user.findUnique({
where: { emailAddress: email },
select: { emailAddress: true, id: true, userPassword: true },
});
if (user && user.userPassword) {
throw new ApiError(409, 'User is already registered. Please login.');
}
let newUserLocal;
const referenceNumber = await generateHostRefNumber(tx);
if (user && !user.userPassword) {
// reuse existing invited user record
newUserLocal = user;
} else {
// create new user record within the transaction
newUserLocal = await tx.user.create({
data: { emailAddress: email, roleXid: ROLE.HOST, userRefNumber: referenceNumber },
});
}
// Generate OTP (6-digit) and store within the same transaction
const otp = OtpGeneratorSixDigit.generateOtp();
const hashedOtp = await bcrypt.hash(otp, 10);
const expiry = new Date(Date.now() + 5 * 60 * 1000); // 5 minutes
// delete old active OTPs for this user/purpose
await tx.userOtp.deleteMany({
where: { userXid: Number(newUserLocal.id), otpType: 'Register', isActive: true },
});
await tx.userOtp.create({
data: {
userXid: Number(newUserLocal.id),
otpType: 'Register',
otpCode: hashedOtp,
expiresOn: expiry,
isVerified: false,
isActive: true,
},
});
const encryptedId = encryptUserId(String(newUserLocal.id));
return { newUser: newUserLocal, otp, encryptedId };
});
if (!transactionResult || !transactionResult.otp) {
throw new ApiError(500, 'Failed to generate OTP');
}
// Send OTP email outside the DB transaction
// await sendOtpEmailForHost(transactionResult.newUser.emailAddress, transactionResult.otp);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'OTP sent successfully.',
data: {},
}),
};
});

View File

@@ -0,0 +1,371 @@
// modules/host/handlers/addCompanyDetails.ts
import config from '@/config/config';
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import AWS from 'aws-sdk';
import Busboy from 'busboy';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { verifyHostToken } from '@/common/middlewares/jwt/authForHost';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import {
hostCompanyDetailsSchema,
hostDocumentsSchema,
parentCompanySchema,
} from '../../../../../common/utils/validation/host/hostCompanyDetails.validation';
import { HostService } from '../../../services/host.service';
import { sendEmailToAM, sendEmailToMinglarAdmin } from '../../../services/sendHostResubmitEmailToAM.service';
const prisma = new PrismaService();
const hostService = new HostService(prisma);
const s3 = new AWS.S3({
region: config.aws.region,
});
function normalizeJsonField(fields: any, key: string) {
if (!fields[key]) return undefined;
const val = fields[key];
if (typeof val === 'object') return val;
if (typeof val === 'string') {
try {
return JSON.parse(val);
} catch (err) {
throw new ApiError(400, `Invalid JSON in field: ${key}`);
}
}
throw new ApiError(400, `Invalid input: ${key} must be object or JSON string.`);
}
function cleanEmptyStrings(obj: any) {
if (!obj || typeof obj !== 'object') return obj;
const cleaned: any = {};
for (const key of Object.keys(obj)) {
if (obj[key] === '') cleaned[key] = undefined;
else if (typeof obj[key] === 'object') cleaned[key] = cleanEmptyStrings(obj[key]);
else cleaned[key] = obj[key];
}
return cleaned;
}
export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
try {
/** 1) AUTH */
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.');
const userInfo = await verifyHostToken(token);
/** 2) CHECK CONTENT TYPE */
const contentType = event.headers['content-type'] || event.headers['Content-Type'];
if (!contentType?.includes('multipart/form-data')) {
throw new ApiError(400, 'Content-Type must be multipart/form-data.');
}
/** 3) HANDLE BODY */
const bodyBuffer = event.isBase64Encoded
? Buffer.from(event.body as string, 'base64')
: Buffer.from(event.body as string, 'binary');
const fields: Record<string, any> = {};
const files: Array<{ buffer: Buffer; mimeType: string; fileName: string; fieldName: string }> = [];
await new Promise<void>((resolve, reject) => {
const bb = Busboy({ headers: { 'content-type': contentType } });
bb.on('file', (fieldname, file, info) => {
const { filename, mimeType } = info;
const chunks: Buffer[] = [];
let totalSize = 0;
const MAX_SIZE = 5 * 1024 * 1024;
file.on('data', (chunk) => {
totalSize += chunk.length;
if (totalSize > MAX_SIZE) {
file.destroy(new Error(`File ${filename} exceeds 5MB limit.`));
return;
}
chunks.push(chunk);
});
file.on('end', () => {
if (chunks.length > 0) {
files.push({
buffer: Buffer.concat(chunks),
mimeType: mimeType || 'application/octet-stream',
fileName: filename || 'unknown',
fieldName: fieldname,
});
}
});
file.on('error', (error) => reject(new ApiError(400, `File upload error: ${error.message}`)));
});
bb.on('field', (fieldname, val) => (fields[fieldname] = val));
bb.on('close', () => resolve());
bb.on('error', (error) => reject(new ApiError(400, `Multipart parsing error: ${error.message}`)));
bb.write(bodyBuffer);
bb.end();
});
/** 4) Extract and clean isDraft flag */
const isDraft = fields.isDraft === 'true' || fields.isDraft === true;
/** 5) PROCESS companyDetails ONCE ONLY (IMPORTANT FIX) */
let companyDetailsRaw = normalizeJsonField(fields, 'companyDetails');
if (!companyDetailsRaw) throw new ApiError(400, 'companyDetails is required.');
if (isDraft) {
companyDetailsRaw = cleanEmptyStrings(companyDetailsRaw);
// IMPORTANT: also clean parent company nested fields
if (companyDetailsRaw.parentCompany) {
companyDetailsRaw.parentCompany = cleanEmptyStrings(companyDetailsRaw.parentCompany);
}
}
/** 6) Profile update if provided */
if (fields.userProfile) {
const userProfileRaw = normalizeJsonField(fields, 'userProfile');
if (userProfileRaw) {
const { firstName, lastName, mobileNumber } = userProfileRaw;
await prisma.user.update({
where: { id: userInfo.id },
data: {
...(firstName && { firstName }),
...(lastName && { lastName }),
...(mobileNumber && { mobileNumber }),
},
});
}
}
/** 7) VALIDATION - SKIPPED IF DRAFT */
let parsedCompany: any = companyDetailsRaw;
if (!isDraft) {
const validate = hostCompanyDetailsSchema.safeParse(companyDetailsRaw);
if (!validate.success) {
const message = validate.error.issues.map((i) => i.message).join(', ');
throw new ApiError(400, `Validation failed: ${message}`);
}
parsedCompany = validate.data;
}
/** 8) DOCUMENT METADATA */
const documentsMetadataRaw = normalizeJsonField(fields, 'documents');
if (!Array.isArray(documentsMetadataRaw)) throw new ApiError(400, 'documents must be an array.');
if (!isDraft) {
const docsParse = hostDocumentsSchema.safeParse(documentsMetadataRaw);
if (!docsParse.success) {
const message = docsParse.error.issues.map((i) => i.message).join(', ');
throw new ApiError(400, `Documents validation failed: ${message}`);
}
}
const documentsMetadata = documentsMetadataRaw.map((d: any) => ({
...d,
owner: d.owner || 'host',
}));
const documentMetadata = documentsMetadata.map((doc: any) => {
const file = files.find((f) => f.fieldName === doc.fieldName);
// In DRAFT mode → allow missing documents
if (isDraft && !file) {
return { ...doc, file: null };
}
// In FINAL mode → file must exist
if (!file) {
throw new ApiError(400, `File not found for field: ${doc.fieldName}`);
}
return { ...doc, file };
});
/** 9) SPLIT host & parent docs */
const hostDocs = documentMetadata.filter((d) => d.owner === 'host');
const parentDocs = documentMetadata.filter((d) => d.owner === 'parent');
/** 10) VALIDATE PARENT COMPANY (ONLY IN FINAL SUBMISSION) */
let parsedParentCompany: any = null;
if (!isDraft && parsedCompany.isSubsidairy) {
if (!parsedCompany.parentCompany) {
throw new ApiError(400, 'isSubsidairy is true but parentCompany object is missing.');
}
const parentCheck = parentCompanySchema.safeParse(parsedCompany.parentCompany);
if (!parentCheck.success) {
const message = parentCheck.error.issues.map((i) => i.message).join(', ');
throw new ApiError(400, `Parent company validation failed: ${message}`);
}
parsedParentCompany = parentCheck.data;
} else {
parsedParentCompany = parsedCompany.parentCompany || null;
}
/** 11) UPLOAD DOCUMENTS */
async function uploadToS3(buffer, mimeType, originalName, folderType, documentTypeXid?, fieldName?) {
const ext = originalName.split('.').pop() || 'jpg';
let s3Key = '';
if (folderType === 'logo') {
s3Key = `Documents/Host/${userInfo.id}/logo/company_logo.${ext}`;
}
else if (folderType === 'parent_company_logo') {
s3Key = `Documents/Host/${userInfo.id}/parent_company/logo/parent_company_logo.${ext}`;
}
else if (folderType === 'documents') {
s3Key = `Documents/Host/${userInfo.id}/documents/${documentTypeXid}_${fieldName}.${ext}`;
}
else if (folderType === 'parent_company') {
s3Key = `Documents/Host/${userInfo.id}/parent_company/${documentTypeXid}_${fieldName}.${ext}`;
}
await s3
.upload({
Bucket: config.aws.bucketName,
Key: s3Key,
Body: buffer,
ContentType: mimeType,
ACL: 'private',
})
.promise();
return `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${s3Key}`;
}
/** Upload host docs */
const uploadedHostDocs: Array<any> = [];
for (const doc of hostDocs) {
if (isDraft && !doc.file) continue;
const path = await uploadToS3(
doc.file.buffer,
doc.file.mimeType,
doc.file.fileName,
'documents',
doc.documentTypeXid,
doc.fieldName,
);
uploadedHostDocs.push({
documentTypeXid: doc.documentTypeXid,
documentName: doc.fieldName,
filePath: path,
});
}
/** Upload parent docs */
const uploadedParentDocs: Array<any> = [];
for (const doc of parentDocs) {
if (!doc.file && isDraft) continue; // skip missing files in draft mode
const path = await uploadToS3(
doc.file.buffer,
doc.file.mimeType,
doc.file.fileName,
'parent_company',
doc.documentTypeXid,
doc.fieldName,
);
uploadedParentDocs.push({
documentTypeXid: doc.documentTypeXid,
documentName: doc.documentName,
filePath: path,
});
}
/** UPLOAD LOGO (if provided) */
const logoFile = files.find((f) => f.fieldName === 'companyLogo' || f.fieldName === 'companyLogoFile');
if (logoFile) {
const logoUrl = await uploadToS3(logoFile.buffer, logoFile.mimeType, logoFile.fileName, 'logo');
parsedCompany.logoPath = logoUrl;
}
/** UPLOAD PARENT COMPANY LOGO (if provided) */
const parentLogoFile = files.find((f) => f.fieldName === 'parentCompanyLogo');
if (parentLogoFile) {
const parentLogoUrl = await uploadToS3(
parentLogoFile.buffer,
parentLogoFile.mimeType,
parentLogoFile.fileName,
'parent_company_logo',
);
if (parsedParentCompany) {
parsedParentCompany.logoPath = parentLogoUrl;
} else {
// if no parent object exists yet (drafts or other flows), attach it safely
parsedParentCompany = parsedParentCompany || {};
parsedParentCompany.logoPath = parentLogoUrl;
}
}
/** 12) SAVE / UPDATE HOST ENTRY */
const createdOrUpdated = await hostService.addOrUpdateCompanyDetails(
userInfo.id,
parsedCompany,
uploadedHostDocs,
parsedParentCompany,
uploadedParentDocs,
isDraft,
);
if (!createdOrUpdated) throw new ApiError(400, 'Failed to add/update company details.');
/** 13) SEND EMAIL ONLY IN FINAL SUBMISSION */
if (!isDraft) {
const details = await hostService.getSuggestionDetails(userInfo.id);
if (details.hostDetails.accountManagerXid) {
await sendEmailToAM(
details.hostDetails.accountManager.emailAddress,
details.hostDetails.accountManager.firstName,
details.hostDetails.companyName,
details.hostDetails.user.userRefNumber,
);
} else {
await sendEmailToMinglarAdmin(
config.MinglarAdminEmail,
config.MinglarAdminName,
details.hostDetails.companyName,
details.hostDetails.user.userRefNumber,
);
}
}
/** RESPONSE */
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: isDraft ? 'Company details saved as draft successfully.' : 'Company details uploaded successfully.',
data: {
id: createdOrUpdated.id,
isDraft,
},
}),
};
} catch (error: any) {
console.error('❌ Error in addCompanyDetails:', error);
throw error;
}
});

View File

@@ -0,0 +1,70 @@
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';
import { hostBankDetailsSchema } from '@/common/utils/validation/host/addPaymentDetails.validation';
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 hostId = userInfo.id;
if (Number.isNaN(hostId)) {
throw new ApiError(400, 'Host id must be a number');
}
const host = await hostService.getHostIdByUserXid(hostId);
if (!host) {
throw new ApiError(404, 'Host not found');
}
// Parse request body
let body: { bankXid?: number; bankBranchXid?: number; accountNumber?: string; confirmAccountNumber?: string; accountHolderName?: string; ifscCode?: string; currencyXid?: number };
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
// ✅ Validate payload using Zod
const validationResult = hostBankDetailsSchema.safeParse({
...(body as object),
hostXid: host.id, // inject hostId from token (not from user input)
});
if (!validationResult.success) {
const errorMessages = validationResult.error.issues.map(e => e.message).join(', ');
throw new ApiError(400, `Validation failed: ${errorMessages}`);
}
const validatedData = validationResult.data;
await hostService.addPaymentDetails(validatedData);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Payment details added successfully',
}),
};
});

View File

@@ -0,0 +1,41 @@
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 { parseBody } from '../../../../../common/utils/validation/validation.utils';
import { verifyOtpWithEmailSchema } from '../../../../../common/utils/validation/host/auth.validation';
const prismaService = new PrismaService();
const hostService = new HostService(prismaService);
const tokenService = new TokenService(prismaService);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
// Parse and validate request body using Zod
const { email, otp } = parseBody(event.body, verifyOtpWithEmailSchema);
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

@@ -0,0 +1,50 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { safeHandler } from '../../../common/utils/handlers/safeHandler';
import { PrismaService } from '../../../common/database/prisma.service';
import ApiError from '../../../common/utils/helper/ApiError';
import { verifyHostToken } from '../../../common/middlewares/jwt/authForHost';
import { HostService } from '../services/host.service';
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.');
}
// Verify token and get user info
const userInfo = await verifyHostToken(token);
const userId = Number(userInfo.id);
if (!userId || isNaN(userId)) {
throw new ApiError(400, 'Invalid user ID');
}
// Fetch user with their HostHeader stepper info
const host = await hostService.getHostById(userId);
if (!host) {
throw new ApiError(404, 'Host record not found');
}
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Stepper information retrieved successfully',
data: {
stepper: host.stepper,
},
}),
};
});

View File

@@ -0,0 +1,50 @@
import { verifyMinglarAdminHostToken } from '@/common/middlewares/jwt/authForMinglarAdmin&Host';
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { PrismaService } from '../../../common/database/prisma.service';
import { safeHandler } from '../../../common/utils/handlers/safeHandler';
import ApiError from '../../../common/utils/helper/ApiError';
import { HostService } from '../services/host.service';
const prismaService = new PrismaService();
const hostService = new HostService(prismaService);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
// Get host ID from path parameters
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.');
}
const userInfo = await verifyMinglarAdminHostToken(token);
const id = Number(userInfo.id)
if (!id) {
throw new ApiError(400, 'Host ID is required');
}
if (isNaN(id)) {
throw new ApiError(400, 'Invalid host ID format');
}
const hostDetails = await hostService.getHostById(id);
if (!hostDetails) {
throw new ApiError(404, 'Host not found');
}
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Host details retrieved successfully',
data: hostDetails,
}),
};
});

View File

@@ -10,8 +10,8 @@ export const handler = safeHandler(async (
const result = await prisma.hostHeader.findMany({
select: {
id: true,
hostParent: true,
hostRefNumber: true,
hostStatusDisplay: true,
accountManager: true,
},

View File

@@ -0,0 +1,62 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
import { PrismaService } from "../../../common/database/prisma.service";
import { safeHandler } from "../../../common/utils/handlers/safeHandler";
import ApiError from "../../../common/utils/helper/ApiError";
import { resendOtpHelper } from "../../../common/utils/helper/resendOtpHelper";
import { resendOtpEmail } from "../services/resendOTPEmail.service";
import { parseBody } from "../../../common/utils/validation/validation.utils";
import { resendOtpSchema } from "../../../common/utils/validation/host/auth.validation";
const prisma = new PrismaService();
// allowed purposes
const ALLOWED_PURPOSES = ["Register", "Login", "ForgotPassword"] as const;
type OtpPurpose = typeof ALLOWED_PURPOSES[number];
export const handler = safeHandler(
async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
// Parse and validate body using Zod
const { email, purpose: purposeFromBody } = parseBody(event.body, resendOtpSchema);
// allow passing purpose via query string too (useful for GET requests)
const qsPurpose = event.queryStringParameters?.purpose;
const purpose = (purposeFromBody || qsPurpose || "Register") as OtpPurpose;
// find user (you can adapt the isActive / userStatus checks per your rules)
const user = await prisma.user.findUnique({
where: { emailAddress: email, isActive: true },
select: { id: true, emailAddress: true, role: true },
});
if (!user) {
throw new ApiError(404, "User not found");
}
const role = user.role.roleName
// call resend helper (old OTPs become inactive + verified, new OTP gets created)
const otpResult = await resendOtpHelper(
prisma,
user.id,
user.emailAddress,
purpose,
6, // 6-digit OTP
5 // expires in 5 minutes
);
// send email (use appropriate template based on 'purpose' inside the email service)
await resendOtpEmail(user.emailAddress, otpResult.otp, role);
return {
statusCode: 200,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify({
success: true,
message: "OTP resent successfully.",
data: { purpose },
}),
};
}
);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,39 @@
import { brevoService } from "@/common/email/brevoApi";
import ApiError from "@/common/utils/helper/ApiError";
export async function resendOtpEmail(
emailAddress: string,
otp: string | number,
role: string
): Promise<{
sent: boolean;
// messageId: string
}> {
const subject = "New OTP from Minglar Team";
const htmlContent = `
<p>Dear ${role},</p>
<p>Your new OTP is: <strong>${otp}</strong></p>
<p>This code is valid for 5 minutes. Please do not share it with anyone.</p>
<p>Best regards,<br/>Minglar Team</p>
`;
try {
const result = await brevoService.sendEmail({
recipients: [{ email: emailAddress }],
subject,
htmlContent,
});
// console.log("📧 Email sent successfully:", result);
return {
sent: true,
// messageId: result.messageId
};
} catch (err) {
console.error("Brevo email send failed:", err);
throw new ApiError(500, "Failed to send OTP to host via email.");
}
}

View File

@@ -0,0 +1,78 @@
import { brevoService } from "@/common/email/brevoApi";
import ApiError from "@/common/utils/helper/ApiError";
export async function sendEmailToAM(
emailAddress: string,
amName: string,
hostCompanyName: string,
hostRefNumber: string
): Promise<{
sent: boolean;
// messageId: string
}> {
const subject = `Host Application Re-Submited : ${hostCompanyName}`;
const htmlContent = `
<p>Dear ${amName},</p>
<p>Host ${hostCompanyName} with reference number: <strong>${hostRefNumber}</strong> has re-submited the application with implimented suggestions.</p>
<p>Please review their appliaction and take the necessary action.</p>
<p>Best regards,<br/>Minglar Team</p>
`;
try {
const result = await brevoService.sendEmail({
recipients: [{ email: emailAddress }],
subject,
htmlContent,
});
// console.log("📧 Email sent successfully:", result);
return {
sent: true,
// messageId: result.messageId
};
} catch (err) {
console.error("Brevo email send failed:", err);
throw new ApiError(500, "Failed to send OTP to host via email.");
}
}
export async function sendEmailToMinglarAdmin(
emailAddress: string,
minglarAdminName: string,
hostCompanyName: string,
hostRefNumber: string
): Promise<{
sent: boolean;
// messageId: string
}> {
const subject = `New Host Application Recieved : ${hostCompanyName}`;
const htmlContent = `
<p>Dear ${minglarAdminName},</p>
<p>Host ${hostCompanyName} with reference number: <strong>${hostRefNumber}</strong> has submited their application.</p>
<p>Please review their appliaction and take the necessary action.</p>
<p>Best regards,<br/>Minglar Team</p>
`;
try {
const result = await brevoService.sendEmail({
recipients: [{ email: emailAddress }],
subject,
htmlContent,
});
// console.log("📧 Email sent successfully:", result);
return {
sent: true,
// messageId: result.messageId
};
} catch (err) {
console.error("Brevo email send failed:", err);
throw new ApiError(500, "Failed to send OTP to host via email.");
}
}

View File

@@ -0,0 +1,38 @@
import { brevoService } from "@/common/email/brevoApi";
import ApiError from "@/common/utils/helper/ApiError";
export async function sendOtpEmailForHost(
emailAddress: string,
otp: string | number
): Promise<{
sent: boolean;
// messageId: string
}> {
const subject = "OTP for Host Registration";
const htmlContent = `
<p>Dear Host,</p>
<p>Your OTP for registration is: <strong>${otp}</strong></p>
<p>This code is valid for 5 minutes. Please do not share it with anyone.</p>
<p>Best regards,<br/>Minglar Team</p>
`;
try {
const result = await brevoService.sendEmail({
recipients: [{ email: emailAddress }],
subject,
htmlContent,
});
// console.log("📧 Email sent successfully:", result);
return {
sent: true,
// messageId: result.messageId
};
} catch (err) {
console.error("Brevo email send failed:", err);
throw new ApiError(500, "Failed to send OTP to host via email.");
}
}

View File

@@ -0,0 +1,159 @@
import { PrismaService } from "../../../common/database/prisma.service";
import jwt, { JwtPayload } from "jsonwebtoken";
import moment from "moment";
import config from "../../../config/config";
export class TokenService {
constructor(private readonly prisma: PrismaService = new PrismaService()) {}
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 this.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 this.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 this.prisma.token.findFirst({
where: {
id: user_xid,
deviceId,
},
});
if (!existingToken) return false;
await this.prisma.token.delete({ where: { id: existingToken.id } });
return true;
}
async isTokenBlackListed(token: string): Promise<boolean> {
const existing = await this.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

@@ -0,0 +1,99 @@
// src/modules/host/dto/host.dto.ts
import { IsInt, IsOptional, IsString, IsBoolean, IsEmail } from 'class-validator';
export class CreateMinglarDto {
@IsString()
firstName: string;
@IsString()
lastName: string;
@IsEmail()
emailAddress: string;
@IsOptional()
@IsString()
isdCode?: string;
@IsOptional()
@IsString()
mobileNumber?: string;
@IsOptional()
@IsString()
userPassword?: string;
@IsOptional()
@IsInt()
roleXid?: number;
@IsOptional()
@IsBoolean()
isActive?: boolean;
}
export class UpdateMinglarDto {
@IsOptional()
@IsString()
firstName?: string;
@IsOptional()
@IsString()
lastName?: string;
@IsOptional()
@IsEmail()
emailAddress?: string;
@IsOptional()
@IsBoolean()
isActive?: boolean;
}
export class GetMinglarLoginResponseDTO {
id: number;
firstName: string | null;
lastName: string | null;
emailAddress: string;
mobileNumber: string | null;
isActive: boolean;
roleXid: number;
isProfileUpdated: boolean;
profileImage: string;
userStatus: string;
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.profileImage = user.profileImage;
this.isProfileUpdated = user.isProfileUpdated;
this.userStatus = user.userStatus;
this.accessToken = accessToken;
this.refreshToken = refreshToken;
}
}
export class AddPaymentDetailsDTO {
bankXid: number;
bankBranchXid: number;
accountNumber: number;
accountHolderName: string;
ifscCode: string;
hostXid: number;
constructor(bankXid: number, bankBranchXid: number, accountNumber: number, accountHolderName: string, ifscCode: string, hostXid: number) {
this.bankXid = bankXid;
this.bankBranchXid = bankBranchXid;
this.accountNumber = accountNumber;
this.accountHolderName = accountHolderName;
this.ifscCode = ifscCode;
this.hostXid = hostXid;
}
}

View File

@@ -0,0 +1,45 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { safeHandler } from '../../../common/utils/handlers/safeHandler';
import { PrismaService } from '../../../common/database/prisma.service';
import { MinglarService } from '../services/minglar.service';
import { verifyMinglarAdminToken } from '@/common/middlewares/jwt/authForMinglarAdmin';
import { parseBody } from '../../../common/utils/validation/validation.utils';
import { minglarCreatePasswordSchema } from '../../../common/utils/validation/minglaradmin/auth.validation';
const prismaService = new PrismaService();
const minglarService = new MinglarService(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 (await import('../../../common/utils/helper/ApiError')).default(400, 'This is a protected route. Please provide a valid token.');
}
// Authenticate user using the shared authForHost function
const userInfo = await verifyMinglarAdminToken(token);
const user_xid = userInfo.id;
// Parse and validate request body using Zod
const { password } = parseBody(event.body, minglarCreatePasswordSchema);
await minglarService.createPassword(user_xid, password);
const userDetails = await minglarService.getBasicUserDetails(user_xid)
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Password created successfully',
data: userDetails,
}),
};
});

View File

@@ -0,0 +1,57 @@
import {
APIGatewayProxyEvent,
APIGatewayProxyResult,
Context,
} from 'aws-lambda';
import { safeHandler } from '../../../common/utils/handlers/safeHandler';
import { PrismaService } from '../../../common/database/prisma.service';
import { MinglarService } from '../services/minglar.service';
import ApiError from '../../../common/utils/helper/ApiError';
import { verifyMinglarAdminToken } from '../../../common/middlewares/jwt/authForMinglarAdmin';
import { parseQueryParams } from '../../../common/utils/validation/validation.utils';
import { getAmDetailByIdPathSchema } from '../../../common/utils/validation/prepopulate/prepopulate.validation';
const prismaService = new PrismaService();
const minglarService = new MinglarService(prismaService);
/**
* Get AM details by ID handler
*/
export const handler = safeHandler(
async (
event: APIGatewayProxyEvent,
context?: Context,
): Promise<APIGatewayProxyResult> => {
// Verify authentication token
const token =
event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(
401,
'This is a protected route. Please provide a valid token.',
);
}
await verifyMinglarAdminToken(token);
// Parse and validate path params using Zod
const { amXid } = parseQueryParams(event.pathParameters, getAmDetailByIdPathSchema);
// Get AM details by ID from service
const getAmDetailsByid = await minglarService.getAMdetailById(amXid);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Account Manager details retrieved successfully',
data: getAmDetailsByid,
}),
};
},
);

View File

@@ -0,0 +1,51 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { verifyMinglarAdminToken } from '../../../../../common/middlewares/jwt/authForMinglarAdmin';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { MinglarService } from '../../../services/minglar.service';
import { sendEmailToHostForApprovedApplication } from '../../../services/approvalMailtoHost.service'
import { parseBody } from '../../../../../common/utils/validation/validation.utils';
import { hostApplicationActionSchema } from '../../../../../common/utils/validation/minglaradmin/hosthub.validation';
const prismaService = new PrismaService();
const minglarService = new MinglarService(prismaService);
/**
* Accept host application handler
*/
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
// Verify authentication token
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(401, 'This is a protected route. Please provide a valid token.');
}
// Verify token and get user info
const userInfo = await verifyMinglarAdminToken(token);
// Parse and validate request body using Zod
const { hostXid } = parseBody(event.body, hostApplicationActionSchema);
// Add suggestion using service
await minglarService.acceptHostApplication(hostXid, userInfo.id);
const hostDetails = await minglarService.getUserDetails(userInfo.id)
await sendEmailToHostForApprovedApplication(hostDetails.emailAddress)
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Application accepted successfully',
data: null,
}),
};
});

View File

@@ -0,0 +1,66 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { MinglarService } from '../../../services/minglar.service';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { verifyMinglarAdminToken } from '../../../../../common/middlewares/jwt/authForMinglarAdmin';
import { HOST_SUGGESTION_TITLES } from '../../../../../common/utils/constants/minglar.constant';
import { parseBody } from '../../../../../common/utils/validation/validation.utils';
import { addPqqSuggestionSchema } from '../../../../../common/utils/validation/minglaradmin/hosthub.validation';
const prismaService = new PrismaService();
const minglarService = new MinglarService(prismaService);
/**
* Add suggestion handler for host applications
* Allows Minglar Admin, Co_Admin, and Account Manager to add suggestions
* Types: Setup Profile, Review Account, Add Payment Details, Agreement
*/
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
// Verify authentication token
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(401, 'This is a protected route. Please provide a valid token.');
}
// Verify token and get user info
const userInfo = await verifyMinglarAdminToken(token);
// Get user details
const user = await prismaService.user.findUnique({
where: { id: userInfo.id },
select: { id: true, roleXid: true }
});
if (!user) {
throw new ApiError(404, 'User not found');
}
// Parse and validate request body using Zod
const { title, comments, activity_pqq_header_xid } = parseBody(event.body, addPqqSuggestionSchema);
// Validate title is one of the allowed types
const allowedTitles = Object.values(HOST_SUGGESTION_TITLES);
if (!allowedTitles.includes(title)) {
throw new ApiError(400, `Invalid title. Allowed values: ${allowedTitles.join(', ')}`);
}
// Add suggestion using service
await minglarService.addPqqSuggestion(title, comments, activity_pqq_header_xid,user.id);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Suggestion added successfully',
data: null,
}),
};
});

View File

@@ -0,0 +1,66 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { MinglarService } from '../../../services/minglar.service';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { verifyMinglarAdminToken } from '../../../../../common/middlewares/jwt/authForMinglarAdmin';
import { HOST_SUGGESTION_TITLES } from '../../../../../common/utils/constants/minglar.constant';
import { parseBody } from '../../../../../common/utils/validation/validation.utils';
import { addHostSuggestionSchema } from '../../../../../common/utils/validation/minglaradmin/hosthub.validation';
const prismaService = new PrismaService();
const minglarService = new MinglarService(prismaService);
/**
* Add suggestion handler for host applications
* Allows Minglar Admin, Co_Admin, and Account Manager to add suggestions
* Types: Setup Profile, Review Account, Add Payment Details, Agreement
*/
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
// Verify authentication token
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(401, 'This is a protected route. Please provide a valid token.');
}
// Verify token and get user info
const userInfo = await verifyMinglarAdminToken(token);
// Get user details
const user = await prismaService.user.findUnique({
where: { id: userInfo.id },
select: { id: true, roleXid: true }
});
if (!user) {
throw new ApiError(404, 'User not found');
}
// Parse and validate request body using Zod
const { hostXid, title, comments } = parseBody(event.body, addHostSuggestionSchema);
// Validate title is one of the allowed types
const allowedTitles = Object.values(HOST_SUGGESTION_TITLES);
if (!allowedTitles.includes(title)) {
throw new ApiError(400, `Invalid title. Allowed values: ${allowedTitles.join(', ')}`);
}
// Add suggestion using service
await minglarService.addHostSuggestion(hostXid, title, comments, user.id);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Suggestion added 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 { MinglarService } from '../../../services/minglar.service';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { verifyMinglarAdminToken } from '../../../../../common/middlewares/jwt/authForMinglarAdmin';
import { paginationService } from '../../../../../common/utils/pagination/pagination.service';
import { parseQueryParams } from '../../../../../common/utils/validation/validation.utils';
import { getAllHostApplicationsQuerySchema } from '../../../../../common/utils/validation/minglaradmin/hosthub.validation';
const prismaService = new PrismaService();
const minglarService = new MinglarService(prismaService);
/**
* Get all host applications handler with pagination
*/
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
// Verify authentication token
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(401, 'This is a protected route. Please provide a valid token.');
}
// Verify token and get user info
const userInfo = await verifyMinglarAdminToken(token);
// Get user details including role
const user = await prismaService.user.findUnique({
where: { id: userInfo.id },
select: { id: true, roleXid: true }
});
if (!user) {
throw new ApiError(404, 'User not found');
}
// Parse and validate query params using Zod
const { search, userStatus, roleFilter } = parseQueryParams(
event.queryStringParameters,
getAllHostApplicationsQuerySchema
);
// Parse pagination parameters
const paginationParams = paginationService.getPaginationFromEvent(event);
const paginationOptions = paginationService.parsePaginationParams(paginationParams);
// Get paginated host applications
const { data, totalCount } = await minglarService.getAllHostApplications(
user.id,
Number(user.roleXid),
search,
userStatus,
paginationOptions,
roleFilter
);
// Create paginated response
const paginatedResponse = paginationService.createPaginatedResponse(
data,
totalCount,
paginationOptions
);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Host applications retrieved successfully',
...paginatedResponse,
}),
};
});

View File

@@ -0,0 +1,48 @@
import { verifyMinglarAdminHostToken } from '@/common/middlewares/jwt/authForMinglarAdmin&Host';
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { MinglarService } from '../../../services/minglar.service';
import { PrismaService } from '@/common/database/prisma.service';
import { safeHandler } from '@/common/utils/handlers/safeHandler';
import ApiError from '@/common/utils/helper/ApiError';
import { verifyMinglarAdminToken } from '@/common/middlewares/jwt/authForMinglarAdmin';
import { parseQueryParams } from '@/common/utils/validation/validation.utils';
import { getHostByIdPathSchema } from '@/common/utils/validation/minglaradmin/hosthub.validation';
const prismaService = new PrismaService();
const minglarService = new MinglarService(prismaService);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
// Get host ID from path parameters
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.');
}
await verifyMinglarAdminToken(token);
// Parse and validate path params using Zod
const { host_xid } = parseQueryParams(event.pathParameters, getHostByIdPathSchema);
const hostDetails = await minglarService.getHostDetailsById(Number(host_xid));
if (!hostDetails) {
throw new ApiError(404, 'Host not found');
}
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Host details retrieved successfully',
data: hostDetails,
}),
};
});

View File

@@ -0,0 +1,51 @@
import { verifyMinglarAdminToken } from '@/common/middlewares/jwt/authForMinglarAdmin';
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { MinglarService } from '../../../services/minglar.service';
import { sendAMRejectionMailtoHost } from '../../../services/rejectionMailtoHost.service';
import { parseBody } from '../../../../../common/utils/validation/validation.utils';
import { hostApplicationActionSchema } from '../../../../../common/utils/validation/minglaradmin/hosthub.validation';
const prismaService = new PrismaService();
const minglarService = new MinglarService(prismaService);
/**
* Reject host application handler for Account Managers
*/
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
// Verify authentication token
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(401, 'This is a protected route. Please provide a valid token.');
}
// Verify token and get user info
const userInfo = await verifyMinglarAdminToken(token);
// Parse and validate request body using Zod
const { hostXid } = parseBody(event.body, hostApplicationActionSchema);
// Add suggestion using service
await minglarService.rejectHostApplicationAM(hostXid, userInfo.id);
const hostDetails = await minglarService.getUserDetails(userInfo.id)
await sendAMRejectionMailtoHost(hostDetails.emailAddress)
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Application rejected successfully',
data: null,
}),
};
});

View File

@@ -0,0 +1,39 @@
import { verifyMinglarAdminToken } from '@/common/middlewares/jwt/authForMinglarAdmin';
import { MinglarService } from '@/modules/minglaradmin/services/minglar.service';
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { parseBody } from '../../../../../common/utils/validation/validation.utils';
import { rejectPqqByAmSchema } from '../../../../../common/utils/validation/minglaradmin/hosthub.validation';
const prismaService = new PrismaService();
const minglarService = new MinglarService(prismaService);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) throw new ApiError(401, 'This is a protected route. Please provide a valid token.');
const userInfo = await verifyMinglarAdminToken(token);
// Parse and validate body using Zod
const { activityId } = parseBody(event.body, rejectPqqByAmSchema);
await minglarService.rejectPQQbyAM(activityId);
return {
statusCode: 201,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Rejected successfully',
data: null,
}),
};
});

View File

@@ -0,0 +1,54 @@
import { verifyOnlyMinglarAdminToken } from '../../../../../common/middlewares/jwt/authForOnlyMinglarAdmin';
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { sendEmailToHostForMinglarApproval } from '../../../services/approvalMailtoHost.service';
import { MinglarService } from '../../../services/minglar.service';
import { parseBody } from '../../../../../common/utils/validation/validation.utils';
import { hostApplicationActionSchema } from '../../../../../common/utils/validation/minglaradmin/hosthub.validation';
const prismaService = new PrismaService();
const minglarService = new MinglarService(prismaService);
/**
* Accept host application handler for Minglar Admin
*/
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
// Verify authentication token
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(401, 'This is a protected route. Please provide a valid token.');
}
// Verify token and get user info
const userInfo = await verifyOnlyMinglarAdminToken(token);
// Parse and validate request body using Zod
const { hostXid } = parseBody(event.body, hostApplicationActionSchema);
// Add suggestion using service
await minglarService.acceptHostApplicationMinglarAdmin(hostXid, userInfo.id);
const hostDetails = await minglarService.getUserDetails(userInfo.id)
if (!hostDetails?.emailAddress) {
throw new ApiError(404, 'Host details or email address not found');
}
await sendEmailToHostForMinglarApproval(hostDetails.emailAddress)
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Application accepted successfully',
data: null,
}),
};
});

View File

@@ -0,0 +1,77 @@
import {
APIGatewayProxyEvent,
APIGatewayProxyResult,
Context,
} from 'aws-lambda';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { MinglarService } from '../../../services/minglar.service';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { verifyOnlyMinglarAdminToken } from '../../../../../common/middlewares/jwt/authForOnlyMinglarAdmin';
import { sendAMEmailForHostAssign } from '../../../services/AMEmail.service';
import { parseBody } from '../../../../../common/utils/validation/validation.utils';
import { assignAmToHostSchema } from '../../../../../common/utils/validation/minglaradmin/hosthub.validation';
const prismaService = new PrismaService();
const minglarService = new MinglarService(prismaService);
/**
* Get all host applications handler
* Returns host details with status, submission date, and account manager info
*/
export const handler = safeHandler(
async (
event: APIGatewayProxyEvent,
context?: Context,
): Promise<APIGatewayProxyResult> => {
// Verify authentication token
const token =
event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(
401,
'This is a protected route. Please provide a valid token.',
);
}
// Verify token and get user info
const userInfo = await verifyOnlyMinglarAdminToken(token);
// Get user details including role
const user = await prismaService.user.findUnique({
where: { id: userInfo.id },
select: { id: true, roleXid: true },
});
if (!user) {
throw new ApiError(404, 'User not found');
}
// Parse and validate request body using Zod
const { hostXid: host_xid, accountManagerXid: account_manager_xid } = parseBody(
event.body,
assignAmToHostSchema
);
// Get all host applications from service based on user role
await minglarService.assignAMToHost(user.id, host_xid, account_manager_xid);
try {
await minglarService.notifyAMOfAssignment(account_manager_xid);
} catch (err) {
console.error('Failed to notify AM after assignment:', err);
}
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'AM assigned to host successfully',
}),
};
},
);

View File

@@ -0,0 +1,72 @@
import { verifyOnlyMinglarAdminToken } from '@/common/middlewares/jwt/authForOnlyMinglarAdmin';
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { MinglarService } from '../../../services/minglar.service';
import { parseBody } from '../../../../../common/utils/validation/validation.utils';
import { editAgreementDetailsSchema } from '../../../../../common/utils/validation/minglaradmin/hosthub.validation';
const prismaService = new PrismaService();
const minglarService = new MinglarService(prismaService);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
// Verify authentication token
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(401, 'This is a protected route. Please provide a valid token.');
}
// Verify token and get user info
const userInfo = await verifyOnlyMinglarAdminToken(token);
// Get user details including role
const user = await prismaService.user.findUnique({
where: { id: userInfo.id },
select: { id: true, roleXid: true }
});
if (!user) {
throw new ApiError(404, 'User not found');
}
// Parse and validate request body using Zod
const {
host_xid,
agreementStartDate,
duration,
isCommisionBase,
commisionPer,
amountPerBooking,
durationFrequency,
payoutDurationNum,
payoutDurationFrequency
} = parseBody(event.body, editAgreementDetailsSchema);
await minglarService.editAgreementDetails(
host_xid,
agreementStartDate,
duration,
isCommisionBase,
commisionPer,
amountPerBooking,
durationFrequency,
payoutDurationNum,
payoutDurationFrequency
);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Details edited successfully',
}),
};
});

View File

@@ -0,0 +1,54 @@
import { verifyMinglarAdminToken } from '@/common/middlewares/jwt/authForMinglarAdmin';
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { PrePopulateService } from '../../../../prepopulate/services/prepopulate.service';
import { MinglarService } from '../../../services/minglar.service';
import { parseQueryParams } from '../../../../../common/utils/validation/validation.utils';
import { getHostByIdAltPathSchema } from '../../../../../common/utils/validation/minglaradmin/hosthub.validation';
import { optionalSearchQuerySchema } from '../../../../../common/utils/validation/host/activity.validation';
const prismaService = new PrismaService();
const minglarService = new MinglarService(prismaService);
const prePopulateService = new PrePopulateService(prismaService);
/**
* Get all activities of a host handler
*/
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
// Verify authentication token
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(401, 'This is a protected route. Please provide a valid token.');
}
// Verify token and get user info
const userInfo = await verifyMinglarAdminToken(token);
// Parse and validate path params using Zod
const { id: hostXid } = parseQueryParams(event.pathParameters, getHostByIdAltPathSchema);
// Parse and validate query params using Zod
const { search, q } = parseQueryParams(event.queryStringParameters, optionalSearchQuerySchema);
const searchTerm = search || q || undefined;
const data = await minglarService.getAllHostActivityForMinglar(searchTerm, hostXid);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Data retrieved successfully',
data,
}),
};
});

Some files were not shown because too many files have changed in this diff Show More