From 953f5214a0b5837d35515c58c464e2ba554f8568 Mon Sep 17 00:00:00 2001 From: Arati Tekdi Date: Mon, 15 Dec 2025 17:54:12 +0530 Subject: [PATCH] Added sendotpMail for forgot, added param in cohortmember entity, and respective changes in API. --- .../postgres/cohortMembers-adapter.ts | 39 +- src/adapters/postgres/user-adapter.ts | 873 ++++++++++++------ src/adapters/userservicelocator.ts | 33 +- src/cohortMembers/cohortMembers.controller.ts | 25 +- .../dto/cohortMember-update.dto.ts | 4 + .../entities/cohort-member.entity.ts | 3 + src/user/user.controller.ts | 4 +- 7 files changed, 665 insertions(+), 316 deletions(-) diff --git a/src/adapters/postgres/cohortMembers-adapter.ts b/src/adapters/postgres/cohortMembers-adapter.ts index 7512e8e8..9d9f4f8d 100644 --- a/src/adapters/postgres/cohortMembers-adapter.ts +++ b/src/adapters/postgres/cohortMembers-adapter.ts @@ -41,7 +41,7 @@ export class PostgresCohortMembersService { private readonly notificationRequest: NotificationRequest, private fieldsService: PostgresFieldsService, private userService: PostgresUserService - ) { } + ) {} //Get cohort member async getCohortMembers( @@ -128,7 +128,7 @@ export class PostgresCohortMembersService { `${API_RESPONSES.SERVER_ERROR}`, `Error: ${e.message}`, apiId - ) + ); const errorMessage = e.message || API_RESPONSES.INTERNAL_SERVER_ERROR; return APIResponse.error( res, @@ -384,7 +384,7 @@ export class PostgresCohortMembersService { `${API_RESPONSES.SERVER_ERROR}`, `Error: ${e.message}`, apiId - ) + ); const errorMessage = e.message || API_RESPONSES.INTERNAL_SERVER_ERROR; return APIResponse.error( res, @@ -450,8 +450,10 @@ export class PostgresCohortMembersService { if (fieldShowHide === "false") { results.userDetails.push(data); } else { - const fieldValues = - await this.fieldsService.getCustomFieldDetails(data.userId, 'Users'); + const fieldValues = await this.fieldsService.getCustomFieldDetails( + data.userId, + "Users" + ); //get data by cohort membership Id let fieldValuesForCohort = await this.fieldsService.getFieldsAndFieldsValues( @@ -567,7 +569,7 @@ export class PostgresCohortMembersService { `${API_RESPONSES.SERVER_ERROR}`, `Error: ${e.message}`, apiId - ) + ); const errorMessage = e.message || API_RESPONSES.INTERNAL_SERVER_ERROR; return APIResponse.error( res, @@ -623,7 +625,7 @@ export class PostgresCohortMembersService { } let query = `SELECT U."userId", U."enrollmentId", U."username", "firstName", "middleName", "lastName", R."name" AS role, U."mobile",U."deviceId", - CM."status", CM."statusReason",CM."cohortMembershipId",CM."status",CM."createdAt", CM."updatedAt",U."createdBy",U."updatedBy", COUNT(*) OVER() AS total_count FROM public."CohortMembers" CM + CM."status", CM."statusReason",CM."cohortMembershipId",CM."params",CM."status",CM."createdAt", CM."updatedAt",U."createdBy",U."updatedBy", COUNT(*) OVER() AS total_count FROM public."CohortMembers" CM INNER JOIN public."Users" U ON CM."userId" = U."userId" INNER JOIN public."UserRolesMapping" UR @@ -700,9 +702,11 @@ export class PostgresCohortMembersService { } } - let cohortMembershipToUpdate = await this.cohortMembersRepository.findOne({ - where: { cohortMembershipId: cohortMembershipId }, - }); + let cohortMembershipToUpdate = await this.cohortMembersRepository.findOne( + { + where: { cohortMembershipId: cohortMembershipId }, + } + ); if (!cohortMembershipToUpdate) { return APIResponse.error( @@ -766,7 +770,7 @@ export class PostgresCohortMembersService { `${API_RESPONSES.SERVER_ERROR}`, `Error: ${error.message}`, apiId - ) + ); return APIResponse.error( response, @@ -776,8 +780,6 @@ export class PostgresCohortMembersService { HttpStatus.INTERNAL_SERVER_ERROR ); } - - } public async deleteCohortMemberById( @@ -820,7 +822,7 @@ export class PostgresCohortMembersService { `${API_RESPONSES.SERVER_ERROR}`, `Error: ${e.message}`, apiId - ) + ); const errorMessage = e.message || API_RESPONSES.SERVER_ERROR; return APIResponse.error( res, @@ -954,7 +956,7 @@ export class PostgresCohortMembersService { `${API_RESPONSES.SERVER_ERROR}`, `Error: ${error.message}`, apiId - ) + ); errors.push( API_RESPONSES.ERROR_UPDATE_COHORTMEMBER( userId, @@ -967,10 +969,7 @@ export class PostgresCohortMembersService { } // Handling of Addition of User in Cohort - if ( - cohortMembersDto?.cohortId && - cohortMembersDto?.cohortId.length > 0 - ) { + if (cohortMembersDto?.cohortId && cohortMembersDto?.cohortId.length > 0) { for (const cohortId of cohortMembersDto.cohortId) { const cohortMembers = { ...cohortMembersBase, @@ -1038,7 +1037,7 @@ export class PostgresCohortMembersService { `${API_RESPONSES.SERVER_ERROR}`, `Error: ${error.message}`, apiId - ) + ); errors.push( API_RESPONSES.ERROR_SAVING_COHORTMEMBER( userId, diff --git a/src/adapters/postgres/user-adapter.ts b/src/adapters/postgres/user-adapter.ts index 0eb7b904..8f53b1de 100644 --- a/src/adapters/postgres/user-adapter.ts +++ b/src/adapters/postgres/user-adapter.ts @@ -3,7 +3,10 @@ import { User } from "../../user/entities/user-entity"; import { FieldValues } from "src/fields/entities/fields-values.entity"; import { InjectRepository } from "@nestjs/typeorm"; import { DataSource, ILike, In, Repository } from "typeorm"; -import { tenantRoleMappingDto, UserCreateDto } from "../../user/dto/user-create.dto"; +import { + tenantRoleMappingDto, + UserCreateDto, +} from "../../user/dto/user-create.dto"; import jwt_decode from "jwt-decode"; import { getKeycloakAdminToken, @@ -18,7 +21,11 @@ import { ErrorResponse } from "src/error-response"; import { SuccessResponse } from "src/success-response"; import { CohortMembers } from "src/cohortMembers/entities/cohort-member.entity"; import { isUUID } from "class-validator"; -import { ExistUserDto, SuggestUserDto, UserSearchDto } from "src/user/dto/user-search.dto"; +import { + ExistUserDto, + SuggestUserDto, + UserSearchDto, +} from "src/user/dto/user-search.dto"; import { UserTenantMapping } from "src/userTenantMapping/entities/user-tenant-mapping.entity"; import { UserRoleMapping } from "src/rbac/assign-role/entities/assign-role.entity"; import { Tenants } from "src/userTenantMapping/entities/tenant.entity"; @@ -46,7 +53,7 @@ import { OtpSendDTO } from "src/user/dto/otpSend.dto"; import { OtpVerifyDTO } from "src/user/dto/otpVerify.dto"; import { SendPasswordResetOTPDto } from "src/user/dto/passwordReset.dto"; import { ActionType, UserUpdateDTO } from "src/user/dto/user-update.dto"; -import { randomInt } from 'crypto'; +import { randomInt } from "crypto"; import { UUID } from "aws-sdk/clients/cloudtrail"; import { AutomaticMemberService } from "src/automatic-member/automatic-member.service"; import { KafkaService } from "src/kafka/kafka.service"; @@ -105,21 +112,22 @@ export class PostgresUserService implements IServicelocator { ); this.reset_frontEnd_url = this.configService.get("RESET_FRONTEND_URL"); - this.otpExpiry = this.configService.get('OTP_EXPIRY') || 10; // default: 10 minutes - this.otpDigits = this.configService.get('OTP_DIGITS') || 6; - this.smsKey = this.configService.get('SMS_KEY'); - this.msg91TemplateKey = this.configService.get('MSG91_TEMPLATE_KEY'); + this.otpExpiry = this.configService.get("OTP_EXPIRY") || 10; // default: 10 minutes + this.otpDigits = this.configService.get("OTP_DIGITS") || 6; + this.smsKey = this.configService.get("SMS_KEY"); + this.msg91TemplateKey = + this.configService.get("MSG91_TEMPLATE_KEY"); this.dataSource = dataSource; // Store dataSource in class property } - public async getCoreColumnNames() { const userMetadata = this.dataSource.getMetadata(User); - const columnNames = userMetadata.columns.map((column) => column.propertyName); + const columnNames = userMetadata.columns.map( + (column) => column.propertyName + ); return columnNames; } - public async sendPasswordResetLink( request: any, username: string, @@ -402,7 +410,6 @@ export class PostgresUserService implements IServicelocator { } } - async findAllUserDetails(userSearchDto, tenantId?: string) { let { limit, offset, filters, exclude, sort } = userSearchDto; let excludeCohortIdes; @@ -429,7 +436,14 @@ export class PostgresUserService implements IServicelocator { if (filters && Object.keys(filters).length > 0) { //Fwtch all core fields let coreFields = await this.getCoreColumnNames(); - const allCoreField = [...coreFields, 'fromDate', 'toDate', 'role', 'tenantId', 'name']; + const allCoreField = [ + ...coreFields, + "fromDate", + "toDate", + "role", + "tenantId", + "name", + ]; for (const [key, value] of Object.entries(filters)) { //Check request filter are proesent on core file or cutom fields @@ -448,8 +462,13 @@ export class PostgresUserService implements IServicelocator { case "email": case "username": case "userId": - if (Array.isArray(value) && value.every((item) => typeof item === "string")) { - const status = value.map((item) => `'${item.trim().toLowerCase()}'`).join(","); + if ( + Array.isArray(value) && + value.every((item) => typeof item === "string") + ) { + const status = value + .map((item) => `'${item.trim().toLowerCase()}'`) + .join(","); whereCondition += ` U."${key}" IN(${status})`; } else { whereCondition += ` U."${key}" = '${value}'`; @@ -513,7 +532,6 @@ export class PostgresUserService implements IServicelocator { //If source config in source details from fields table is not exist then return false if (Object.keys(searchCustomFields).length > 0) { - const context = "USERS"; getUserIdUsingCustomFields = await this.fieldsService.filterUserUsingCustomFieldsOptimized( @@ -530,7 +548,9 @@ export class PostgresUserService implements IServicelocator { const userIdsDependsOnCustomFields = getUserIdUsingCustomFields .map((userId) => `'${userId}'`) .join(","); - whereCondition += `${index > 0 ? " AND " : ""} U."userId" IN (${userIdsDependsOnCustomFields})`; + whereCondition += `${ + index > 0 ? " AND " : "" + } U."userId" IN (${userIdsDependsOnCustomFields})`; index++; } @@ -558,15 +578,21 @@ export class PostgresUserService implements IServicelocator { } // Apply tenant filtering conditionally if tenantId is provided from headers - if (tenantId && tenantId.trim() !== '') { + if (tenantId && tenantId.trim() !== "") { if (index === 0 && whereCondition === "") { whereCondition = `WHERE UTM."tenantId" = '${tenantId}'`; } else { whereCondition += ` AND UTM."tenantId" = '${tenantId}'`; } - LoggerUtil.log(`Applying tenant filter for tenantId: ${tenantId}`, APIID.USER_LIST); + LoggerUtil.log( + `Applying tenant filter for tenantId: ${tenantId}`, + APIID.USER_LIST + ); } else { - LoggerUtil.warn(`No tenantId provided - returning users from all tenants`, APIID.USER_LIST); + LoggerUtil.warn( + `No tenantId provided - returning users from all tenants`, + APIID.USER_LIST + ); } //Get user core fields data @@ -588,16 +614,17 @@ export class PostgresUserService implements IServicelocator { // Get user custom field data for (const userData of userDetails) { const customFields = await this.fieldsService.getCustomFieldDetails( - userData.userId, 'Users' + userData.userId, + "Users" ); userData["customFields"] = Array.isArray(customFields) ? customFields.map((data) => ({ - fieldId: data?.fieldId, - label: data?.label, - selectedValues: data?.selectedValues, - type: data?.type, - })) + fieldId: data?.fieldId, + label: data?.label, + selectedValues: data?.selectedValues, + type: data?.type, + })) : []; result.getUserDetails.push(userData); @@ -680,7 +707,8 @@ export class PostgresUserService implements IServicelocator { const contextType = roleInUpper; // customFields = await this.fieldsService.getFieldValuesData(userData.userId, context, contextType, ['All'], true); customFields = await this.fieldsService.getCustomFieldDetails( - userData.userId, 'Users' + userData.userId, + "Users" ); } @@ -864,7 +892,9 @@ export class PostgresUserService implements IServicelocator { const updatedData = {}; const editIssues = {}; - const user = await this.usersRepository.findOne({ where: { userId: userDto.userId } }); + const user = await this.usersRepository.findOne({ + where: { userId: userDto.userId }, + }); if (!user) { return APIResponse.error( response, @@ -880,12 +910,19 @@ export class PostgresUserService implements IServicelocator { let deviceIds: any; if (userDto.userData.action === ActionType.ADD) { // add deviceId - deviceIds = await this.loginDeviceIdAction(userDto.userData.deviceId, userDto.userId, user.deviceId) + deviceIds = await this.loginDeviceIdAction( + userDto.userData.deviceId, + userDto.userId, + user.deviceId + ); userDto.userData.deviceId = deviceIds; - } else if (userDto.userData.action === ActionType.REMOVE) { //remove deviceId - deviceIds = await this.onLogoutDeviceId(userDto.userData.deviceId, userDto.userId, user.deviceId) + deviceIds = await this.onLogoutDeviceId( + userDto.userData.deviceId, + userDto.userId, + user.deviceId + ); userDto.userData.deviceId = deviceIds; } } @@ -897,9 +934,11 @@ export class PostgresUserService implements IServicelocator { //Update userdetails on keycloak if (username || firstName || lastName || email) { try { - const keycloakUpdateResult = await this.updateUsernameInKeycloak(keycloakReqBody); + const keycloakUpdateResult = await this.updateUsernameInKeycloak( + keycloakReqBody + ); - if (keycloakUpdateResult === 'exists') { + if (keycloakUpdateResult === "exists") { return APIResponse.error( response, apiId, @@ -945,23 +984,29 @@ export class PostgresUserService implements IServicelocator { userDto?.userId ); - // Synchronize user status with Keycloak if (userDto.userData?.status) { - const isUserActive = userDto.userData.status === 'active'; - + const isUserActive = userDto.userData.status === "active"; + // Async Keycloak status synchronization - non-blocking - this.syncUserStatusWithKeycloak(userDto.userId, isUserActive, apiId) - .catch(error => LoggerUtil.error( - 'Keycloak user status sync failed', + this.syncUserStatusWithKeycloak( + userDto.userId, + isUserActive, + apiId + ).catch((error) => + LoggerUtil.error( + "Keycloak user status sync failed", `Error: ${error.message}`, apiId - )); + ) + ); } if (userDto?.customFields?.length > 0) { const getFieldsAttributes = - await this.fieldsService.getEditableFieldsAttributes(userDto.userData.tenantId); + await this.fieldsService.getEditableFieldsAttributes( + userDto.userData.tenantId + ); const isEditableFieldId = []; const fieldIdAndAttributes = {}; @@ -1001,18 +1046,27 @@ export class PostgresUserService implements IServicelocator { } if (userDto.automaticMember && userDto?.automaticMember?.value === true) { - let assignTo; //Find Assign field value from custom fields - let foundField = userDto.customFields.find(field => field.fieldId === userDto.automaticMember.fieldId); + let foundField = userDto.customFields.find( + (field) => field.fieldId === userDto.automaticMember.fieldId + ); if (foundField) { assignTo = foundField.value; } // Check if an active automated member exists for the given userId, tenantId, and assigned ID. - const checkAutomaticMemberExists = await this.automaticMemberService.checkAutomaticMemberExists(userId, userDto.userData.tenantId, foundField.value[0]); + const checkAutomaticMemberExists = + await this.automaticMemberService.checkAutomaticMemberExists( + userId, + userDto.userData.tenantId, + foundField.value[0] + ); - if (checkAutomaticMemberExists.length > 0 && checkAutomaticMemberExists[0].isActive === true) { + if ( + checkAutomaticMemberExists.length > 0 && + checkAutomaticMemberExists[0].isActive === true + ) { return APIResponse.error( response, apiId, @@ -1022,17 +1076,33 @@ export class PostgresUserService implements IServicelocator { ); } - - if (checkAutomaticMemberExists.length > 0 && checkAutomaticMemberExists[0].isActive === false) { + if ( + checkAutomaticMemberExists.length > 0 && + checkAutomaticMemberExists[0].isActive === false + ) { // deactivate the current active automatic membership for the user in tenantId. - const getActiveAutomaticMembershipId = await this.automaticMemberService.getUserbyUserIdAndTenantId(userId, userDto.userData.tenantId, true); + const getActiveAutomaticMembershipId = + await this.automaticMemberService.getUserbyUserIdAndTenantId( + userId, + userDto.userData.tenantId, + true + ); - if (getActiveAutomaticMembershipId && getActiveAutomaticMembershipId.isActive === true) { - await this.automaticMemberService.update(getActiveAutomaticMembershipId.id, { isActive: false }) + if ( + getActiveAutomaticMembershipId && + getActiveAutomaticMembershipId.isActive === true + ) { + await this.automaticMemberService.update( + getActiveAutomaticMembershipId.id, + { isActive: false } + ); } // Activate the old inactive automatic membership for the user in tenantId and assigned ID. - await this.automaticMemberService.update(checkAutomaticMemberExists[0].id, { isActive: true }) + await this.automaticMemberService.update( + checkAutomaticMemberExists[0].id, + { isActive: true } + ); return await APIResponse.success( response, apiId, @@ -1042,7 +1112,12 @@ export class PostgresUserService implements IServicelocator { ); } - await this.updateAutomaticMemberMapping(userDto.automaticMember, assignTo, userId, userDto.userData.tenantId) + await this.updateAutomaticMemberMapping( + userDto.automaticMember, + assignTo, + userId, + userDto.userData.tenantId + ); } LoggerUtil.log( @@ -1061,12 +1136,13 @@ export class PostgresUserService implements IServicelocator { ); // Produce user updated event to Kafka asynchronously - after response is sent to client - this.publishUserEvent('updated', userDto.userId, apiId) - .catch(error => LoggerUtil.error( + this.publishUserEvent("updated", userDto.userId, apiId).catch((error) => + LoggerUtil.error( `Failed to publish user updated event to Kafka`, `Error: ${error.message}`, apiId - )); + ) + ); return apiResponse; } catch (e) { @@ -1089,14 +1165,29 @@ export class PostgresUserService implements IServicelocator { throw new Error("Method not implemented."); } - async updateAutomaticMemberMapping(automaticMember: any, fieldValue: any, userId: UUID, tenantId: UUID) { - + async updateAutomaticMemberMapping( + automaticMember: any, + fieldValue: any, + userId: UUID, + tenantId: UUID + ) { try { // deactivate the current active automatic membership for the user in tenantId. - const getActiveAutomaticMembershipId = await this.automaticMemberService.getUserbyUserIdAndTenantId(userId, tenantId, true); + const getActiveAutomaticMembershipId = + await this.automaticMemberService.getUserbyUserIdAndTenantId( + userId, + tenantId, + true + ); - if (getActiveAutomaticMembershipId && getActiveAutomaticMembershipId.isActive === true) { - await this.automaticMemberService.update(getActiveAutomaticMembershipId.id, { isActive: false }) + if ( + getActiveAutomaticMembershipId && + getActiveAutomaticMembershipId.isActive === true + ) { + await this.automaticMemberService.update( + getActiveAutomaticMembershipId.id, + { isActive: false } + ); } let createAutomaticMember = { @@ -1114,12 +1205,11 @@ export class PostgresUserService implements IServicelocator { // } }, tenantId: tenantId, - isActive: true - } + isActive: true, + }; //Assgn member to sdb - await this.automaticMemberService.create(createAutomaticMember) - + await this.automaticMemberService.create(createAutomaticMember); } catch (error) { LoggerUtil.error( `${API_RESPONSES.SERVER_ERROR}`, @@ -1129,36 +1219,41 @@ export class PostgresUserService implements IServicelocator { } } - async updateUsernameInKeycloak(updateField: UpdateField): Promise<'exists' | false | true> { + async updateUsernameInKeycloak( + updateField: UpdateField + ): Promise<"exists" | false | true> { try { - const keycloakResponse = await getKeycloakAdminToken(); const token = keycloakResponse.data.access_token; //Check user is exist in keycloakDB or not - const checkUserinKeyCloakandDb = await this.checkUserinKeyCloakandDb(updateField); + const checkUserinKeyCloakandDb = await this.checkUserinKeyCloakandDb( + updateField + ); if (checkUserinKeyCloakandDb) { - return 'exists'; + return "exists"; } //Update user in keyCloakService - let updateResult = await updateUserInKeyCloak(updateField, token) + let updateResult = await updateUserInKeyCloak(updateField, token); if (updateResult.success === false) { return false; } return true; - } catch (error) { LoggerUtil.error( `${API_RESPONSES.SERVER_ERROR}`, - `KeyCloak Error: ${error.message}`, + `KeyCloak Error: ${error.message}` ); return false; } } - - private async syncUserStatusWithKeycloak(userId: string, isActive: boolean, apiId: string): Promise { + private async syncUserStatusWithKeycloak( + userId: string, + isActive: boolean, + apiId: string + ): Promise { try { const keycloakResponse = await getKeycloakAdminToken(); const token = keycloakResponse.data.access_token; @@ -1170,20 +1265,22 @@ export class PostgresUserService implements IServicelocator { if (result.success) { LoggerUtil.log( - `Keycloak user status synchronized successfully: ${isActive ? 'enabled' : 'disabled'}`, + `Keycloak user status synchronized successfully: ${ + isActive ? "enabled" : "disabled" + }`, apiId, userId ); } else { LoggerUtil.error( - 'Keycloak user status synchronization failed', + "Keycloak user status synchronization failed", `Status: ${result.statusCode}, Message: ${result.message}`, apiId ); } } catch (error) { LoggerUtil.error( - 'Keycloak user status synchronization error', + "Keycloak user status synchronization error", `Failed to sync user status: ${error.message}`, apiId ); @@ -1191,7 +1288,11 @@ export class PostgresUserService implements IServicelocator { } } - async loginDeviceIdAction(userDeviceId: string, userId: string, existingDeviceId: string[]): Promise { + async loginDeviceIdAction( + userDeviceId: string, + userId: string, + existingDeviceId: string[] + ): Promise { let deviceIds = existingDeviceId || []; // Check if the device ID already exists if (deviceIds.includes(userDeviceId)) { @@ -1206,18 +1307,25 @@ export class PostgresUserService implements IServicelocator { return deviceIds; // Return the updated device list } - async onLogoutDeviceId(deviceIdforRemove: string, userId: string, existingDeviceId: string[]) { + async onLogoutDeviceId( + deviceIdforRemove: string, + userId: string, + existingDeviceId: string[] + ) { let deviceIds = existingDeviceId || []; // Check if the device ID exists if (!deviceIds.includes(deviceIdforRemove)) { return deviceIds; // No action if device ID does not exist } // Remove the device ID - deviceIds = deviceIds.filter(id => id !== deviceIdforRemove); + deviceIds = deviceIds.filter((id) => id !== deviceIdforRemove); return deviceIds; } - async updateBasicUserDetails(userId: string, userData: Partial): Promise { + async updateBasicUserDetails( + userId: string, + userData: Partial + ): Promise { try { // Fetch the user by ID const user = await this.usersRepository.findOne({ where: { userId } }); @@ -1228,14 +1336,12 @@ export class PostgresUserService implements IServicelocator { await Object.assign(user, userData); return this.usersRepository.save(user); - } catch (error) { // Re-throw or handle the error as needed - throw new Error('An error occurred while updating user details'); + throw new Error("An error occurred while updating user details"); } } - async createUser( request: any, userCreateDto: UserCreateDto, @@ -1250,7 +1356,7 @@ export class PostgresUserService implements IServicelocator { username: userCreateDto?.username, email: userCreateDto?.email, firstName: userCreateDto?.firstName, - lastName: userCreateDto?.lastName + lastName: userCreateDto?.lastName, }; // Log user creation attempt with context @@ -1268,7 +1374,7 @@ export class PostgresUserService implements IServicelocator { userCreateDto.createdBy = decoded?.sub; userCreateDto.updatedBy = decoded?.sub; } - stepTimings['jwt_extraction'] = Date.now() - jwtStartTime; + stepTimings["jwt_extraction"] = Date.now() - jwtStartTime; // Step 2: Validate custom fields const customFieldStartTime = Date.now(); @@ -1290,7 +1396,8 @@ export class PostgresUserService implements IServicelocator { ); } } - stepTimings['custom_field_validation'] = Date.now() - customFieldStartTime; + stepTimings["custom_field_validation"] = + Date.now() - customFieldStartTime; // Step 3: Validate request body and roles const validationStartTime = Date.now(); @@ -1318,11 +1425,14 @@ export class PostgresUserService implements IServicelocator { HttpStatus.BAD_REQUEST ); } - stepTimings['request_validation'] = Date.now() - validationStartTime; + stepTimings["request_validation"] = Date.now() - validationStartTime; // Step 4: Validate automatic member vs cohort assignment const businessLogicStartTime = Date.now(); - if (userCreateDto.automaticMember?.value === true && userCreateDto.tenantCohortRoleMapping?.[0]?.cohortIds?.length > 0) { + if ( + userCreateDto.automaticMember?.value === true && + userCreateDto.tenantCohortRoleMapping?.[0]?.cohortIds?.length > 0 + ) { LoggerUtil.error( `Invalid operation for ${userContext.username}: Cannot assign automatic member with cohort`, `User cannot be assigned as automatic member while also being assigned to a center`, @@ -1337,7 +1447,8 @@ export class PostgresUserService implements IServicelocator { HttpStatus.BAD_REQUEST ); } - stepTimings['business_logic_validation'] = Date.now() - businessLogicStartTime; + stepTimings["business_logic_validation"] = + Date.now() - businessLogicStartTime; // Step 5: Prepare username and check Keycloak const keycloakCheckStartTime = Date.now(); @@ -1365,7 +1476,7 @@ export class PostgresUserService implements IServicelocator { HttpStatus.BAD_REQUEST ); } - stepTimings['keycloak_user_check'] = Date.now() - keycloakCheckStartTime; + stepTimings["keycloak_user_check"] = Date.now() - keycloakCheckStartTime; // Step 6: Create user in Keycloak const keycloakCreateStartTime = Date.now(); @@ -1375,13 +1486,18 @@ export class PostgresUserService implements IServicelocator { userContext.username ); - const resKeycloak = await createUserInKeyCloak(userSchema, token, validatedRoles[0]?.title) + const resKeycloak = await createUserInKeyCloak( + userSchema, + token, + validatedRoles[0]?.title + ); // Capture Keycloak creation timing immediately after the call - stepTimings['keycloak_user_creation'] = Date.now() - keycloakCreateStartTime; + stepTimings["keycloak_user_creation"] = + Date.now() - keycloakCreateStartTime; // Handle the case where createUserInKeyCloak returns a string (error) - if (typeof resKeycloak === 'string') { + if (typeof resKeycloak === "string") { LoggerUtil.error( `Keycloak user creation failed for ${userContext.username}`, resKeycloak, @@ -1452,7 +1568,7 @@ export class PostgresUserService implements IServicelocator { academicYearId, response ); - stepTimings['database_user_creation'] = Date.now() - dbCreateStartTime; + stepTimings["database_user_creation"] = Date.now() - dbCreateStartTime; LoggerUtil.log( `User ${userContext.username} created successfully in database`, @@ -1486,9 +1602,9 @@ export class PostgresUserService implements IServicelocator { fieldDetail[`${fieldId}`] ? fieldDetail : { - ...fieldDetail, - [`${fieldId}`]: { fieldAttributes, fieldParams, name }, - }, + ...fieldDetail, + [`${fieldId}`]: { fieldAttributes, fieldParams, name }, + }, {} ); @@ -1515,7 +1631,8 @@ export class PostgresUserService implements IServicelocator { } } } - stepTimings['custom_fields_processing'] = Date.now() - customFieldsStartTime; + stepTimings["custom_fields_processing"] = + Date.now() - customFieldsStartTime; // Step 9: Log performance metrics const totalTime = Date.now() - startTime; @@ -1527,7 +1644,7 @@ export class PostgresUserService implements IServicelocator { // Log performance breakdown LoggerUtil.log( - `Performance breakdown for user creation (${userContext.username}): Total: ${totalTime}ms | JWT: ${stepTimings['jwt_extraction']}ms | Custom Fields Validation: ${stepTimings['custom_field_validation']}ms | Request Validation: ${stepTimings['request_validation']}ms | Business Logic: ${stepTimings['business_logic_validation']}ms | Keycloak Check: ${stepTimings['keycloak_user_check']}ms | Keycloak Creation: ${stepTimings['keycloak_user_creation']}ms | Database Creation: ${stepTimings['database_user_creation']}ms | Custom Fields Processing: ${stepTimings['custom_fields_processing']}ms`, + `Performance breakdown for user creation (${userContext.username}): Total: ${totalTime}ms | JWT: ${stepTimings["jwt_extraction"]}ms | Custom Fields Validation: ${stepTimings["custom_field_validation"]}ms | Request Validation: ${stepTimings["request_validation"]}ms | Business Logic: ${stepTimings["business_logic_validation"]}ms | Keycloak Check: ${stepTimings["keycloak_user_check"]}ms | Keycloak Creation: ${stepTimings["keycloak_user_creation"]}ms | Database Creation: ${stepTimings["database_user_creation"]}ms | Custom Fields Processing: ${stepTimings["custom_fields_processing"]}ms`, apiId, userContext.username ); @@ -1542,15 +1659,15 @@ export class PostgresUserService implements IServicelocator { ); // Produce user created event to Kafka asynchronously - after response is sent to client - this.publishUserEvent('created', result.userId, apiId) - .catch(error => LoggerUtil.error( + this.publishUserEvent("created", result.userId, apiId).catch((error) => + LoggerUtil.error( `Failed to publish user created event to Kafka for ${userContext.username}`, `Error: ${error.message}`, apiId, userContext.username - )); + ) + ); } catch (e) { - LoggerUtil.error( `${API_RESPONSES.SERVER_ERROR}: ${request.url}`, `Error: ${e.message}`, @@ -1678,14 +1795,13 @@ export class PostgresUserService implements IServicelocator { ); } - - if (roleExists && roleExists?.length === 0) { - errorCollector.addError( - `Role Id '${roleId}' does not exist.` - ); + errorCollector.addError(`Role Id '${roleId}' does not exist.`); } else if (roleExists) { - if ((roleExists[0].tenantId || roleExists[0].tenantId !== null) && roleExists[0].tenantId !== tenantId) { + if ( + (roleExists[0].tenantId || roleExists[0].tenantId !== null) && + roleExists[0].tenantId !== tenantId + ) { errorCollector.addError( `Role Id '${roleId}' does not exist for this tenant '${tenantId}'.` ); @@ -1721,7 +1837,6 @@ export class PostgresUserService implements IServicelocator { return notExistCohort.length > 0 ? notExistCohort : []; } - // Can be Implemented after we know what are the unique entities async checkUserinKeyCloakandDb(userDto) { const keycloakResponse = await getKeycloakAdminToken(); @@ -1755,15 +1870,15 @@ export class PostgresUserService implements IServicelocator { response: Response ): Promise { const user = new User(); - user.userId = userCreateDto?.userId, - user.username = userCreateDto?.username, - user.firstName = userCreateDto?.firstName, - user.middleName = userCreateDto?.middleName, - user.lastName = userCreateDto?.lastName, - user.gender = userCreateDto?.gender, - user.email = userCreateDto?.email, - user.mobile = Number(userCreateDto?.mobile) || null, - user.createdBy = userCreateDto?.createdBy || userCreateDto?.createdBy; + (user.userId = userCreateDto?.userId), + (user.username = userCreateDto?.username), + (user.firstName = userCreateDto?.firstName), + (user.middleName = userCreateDto?.middleName), + (user.lastName = userCreateDto?.lastName), + (user.gender = userCreateDto?.gender), + (user.email = userCreateDto?.email), + (user.mobile = Number(userCreateDto?.mobile) || null), + (user.createdBy = userCreateDto?.createdBy || userCreateDto?.createdBy); if (userCreateDto?.dob) { user.dob = new Date(userCreateDto.dob); @@ -1772,16 +1887,35 @@ export class PostgresUserService implements IServicelocator { const createdBy = request.user?.userId || result.userId; if (userCreateDto.tenantCohortRoleMapping) { - if (userCreateDto.automaticMember && userCreateDto?.automaticMember?.value === true) { - await this.automaticMemberMapping(userCreateDto.automaticMember, userCreateDto.customFields, userCreateDto.tenantCohortRoleMapping, result.userId, createdBy) + if ( + userCreateDto.automaticMember && + userCreateDto?.automaticMember?.value === true + ) { + await this.automaticMemberMapping( + userCreateDto.automaticMember, + userCreateDto.customFields, + userCreateDto.tenantCohortRoleMapping, + result.userId, + createdBy + ); } else { - await this.tenantCohortRollMapping(userCreateDto.tenantCohortRoleMapping, academicYearId, result.userId, createdBy); + await this.tenantCohortRollMapping( + userCreateDto.tenantCohortRoleMapping, + academicYearId, + result.userId, + createdBy + ); } } return result; } - async tenantCohortRollMapping(tenantCohortRoleMapping: tenantRoleMappingDto[], academicYearId: UUID, userId: UUID, createdBy: UUID): Promise { + async tenantCohortRollMapping( + tenantCohortRoleMapping: tenantRoleMappingDto[], + academicYearId: UUID, + userId: UUID, + createdBy: UUID + ): Promise { try { for (const mapData of tenantCohortRoleMapping) { if (mapData.cohortIds) { @@ -1819,9 +1953,13 @@ export class PostgresUserService implements IServicelocator { } } - - async automaticMemberMapping(automaticMember: any, customFields: any, tenantCohortRoleMapping: tenantRoleMappingDto[], userId: UUID, createdBy: UUID): Promise { - + async automaticMemberMapping( + automaticMember: any, + customFields: any, + tenantCohortRoleMapping: tenantRoleMappingDto[], + userId: UUID, + createdBy: UUID + ): Promise { try { // Tenant and role mapping for (const mapData of tenantCohortRoleMapping) { @@ -1832,7 +1970,9 @@ export class PostgresUserService implements IServicelocator { await this.assignUserToTenantAndRoll(tenantRoleMappingData, createdBy); } let fieldValue; - let foundField = customFields.find(field => field.fieldId === automaticMember.fieldId); + let foundField = customFields.find( + (field) => field.fieldId === automaticMember.fieldId + ); if (foundField) { fieldValue = foundField.value; } @@ -1852,11 +1992,11 @@ export class PostgresUserService implements IServicelocator { // } }, tenantId: tenantCohortRoleMapping[0].tenantId, - isActive: true - } + isActive: true, + }; //Assgn member to sdb - await this.automaticMemberService.create(createAutomaticMember) + await this.automaticMemberService.create(createAutomaticMember); } catch (error) { LoggerUtil.error( `${API_RESPONSES.SERVER_ERROR}`, @@ -1866,7 +2006,6 @@ export class PostgresUserService implements IServicelocator { } } - async assignUserToTenantAndRoll(tenantsData, createdBy) { try { const tenantId = tenantsData?.tenantRoleMapping?.tenantId; @@ -2073,7 +2212,7 @@ export class PostgresUserService implements IServicelocator { "{username}": userData?.name, "{programName}": userData?.tenantData?.[0]?.tenantName ? userData.tenantData[0].tenantName.charAt(0).toUpperCase() + - userData.tenantData[0].tenantName.slice(1) + userData.tenantData[0].tenantName.slice(1) : "", }, email: { @@ -2143,7 +2282,7 @@ export class PostgresUserService implements IServicelocator { let fieldValue = fieldsData["value"][0]; const getOption = await this.fieldsService.findDynamicOptions( getFieldDetails.sourceDetails.table, - `"${getFieldDetails?.sourceDetails?.table}_id"='${fieldValue}'`, + `"${getFieldDetails?.sourceDetails?.table}_id"='${fieldValue}'` ); if (!getOption?.length) { return APIResponse.error( @@ -2201,8 +2340,8 @@ export class PostgresUserService implements IServicelocator { const roleIds = userCreateDto && userCreateDto.tenantCohortRoleMapping ? userCreateDto.tenantCohortRoleMapping.map( - (userRole) => userRole.roleId - ) + (userRole) => userRole.roleId + ) : []; let contextType; @@ -2232,7 +2371,11 @@ export class PostgresUserService implements IServicelocator { // Log the invalid field validation error with role context LoggerUtil.error( `Invalid custom fields provided for role`, - `Role: ${contextType || 'Unknown'}, Invalid Field IDs: ${invalidFieldIds.join(", ")}, User: ${userCreateDto.username || 'Unknown'}`, + `Role: ${ + contextType || "Unknown" + }, Invalid Field IDs: ${invalidFieldIds.join(", ")}, User: ${ + userCreateDto.username || "Unknown" + }`, apiId, userCreateDto.username ); @@ -2305,7 +2448,7 @@ export class PostgresUserService implements IServicelocator { // Prepare and format user data for Kafka event const kafkaUserData = { userId: userId, - deletedAt: new Date().toISOString() + deletedAt: new Date().toISOString(), }; // Send response to the client @@ -2318,12 +2461,13 @@ export class PostgresUserService implements IServicelocator { ); // Produce user deleted event to Kafka asynchronously - after response is sent to client - this.publishUserEvent('deleted', userId, apiId) - .catch(error => LoggerUtil.error( + this.publishUserEvent("deleted", userId, apiId).catch((error) => + LoggerUtil.error( `Failed to publish user deleted event to Kafka`, `Error: ${error.message}`, apiId - )); + ) + ); return apiResponse; } catch (e) { LoggerUtil.error( @@ -2345,7 +2489,11 @@ export class PostgresUserService implements IServicelocator { } //Generate Has code as per username or mobile Number - private generateOtpHash(mobileOrUsername: string, otp: string, reason: string) { + private generateOtpHash( + mobileOrUsername: string, + otp: string, + reason: string + ) { const ttl = this.otpExpiry * 60 * 1000; // Expiration in milliseconds const expires = Date.now() + ttl; const expiresInMinutes = ttl / (60 * 1000); @@ -2355,28 +2503,30 @@ export class PostgresUserService implements IServicelocator { } // send SignUP OTP - async sendOtp(body: OtpSendDTO, response: Response) { + async sendOtp(body: any, response: Response) { const apiId = APIID.SEND_OTP; try { const { mobile, reason } = body; - if (!mobile || !/^\d{10}$/.test(mobile)) { - return APIResponse.error( - response, - apiId, - API_RESPONSES.BAD_REQUEST, - API_RESPONSES.MOBILE_VALID, - HttpStatus.BAD_REQUEST - ); + if (mobile) { + if (!mobile || !/^\d{10}$/.test(mobile)) { + return APIResponse.error( + response, + apiId, + API_RESPONSES.BAD_REQUEST, + API_RESPONSES.MOBILE_VALID, + HttpStatus.BAD_REQUEST + ); + } } - + // Step 1: Prepare data for OTP generation and send based on channel let notificationPayload: any; let hash: string; let expires: number; - if (reason === 'signup' || reason === 'login') { - const channelOverride = ((body as any)?.channel || '').toLowerCase(); - if (channelOverride === 'sms') { + if (reason === "signup" || reason === "login") { + const channelOverride = ((body as any)?.channel || "").toLowerCase(); + if (channelOverride === "sms") { // Send via SMS ONLY for signup/login without triggering WhatsApp const mobileWithCode = this.formatMobileNumber(mobile); const otp = this.authUtils.generateOtp(this.otpDigits).toString(); @@ -2385,9 +2535,14 @@ export class PostgresUserService implements IServicelocator { expires = generated.expires; const replacements = { "{OTP}": otp, - "{otpExpiry}": generated.expiresInMinutes + "{otpExpiry}": generated.expiresInMinutes, }; - notificationPayload = await this.smsNotification("OTP", "SEND_OTP", replacements, [mobile]); + notificationPayload = await this.smsNotification( + "OTP", + "SEND_OTP", + replacements, + [mobile] + ); } else { // Default: WhatsApp ONLY for signup/login const otp = this.authUtils.generateOtp(this.otpDigits).toString(); @@ -2397,11 +2552,15 @@ export class PostgresUserService implements IServicelocator { expires = waResult.expires; } } else { - // Default (e.g., forgot) uses existing SMS path - const smsResult = await this.sendOTPOnMobile(mobile, reason); - notificationPayload = smsResult.notificationPayload; - hash = smsResult.hash; - expires = smsResult.expires; + if (reason == "forgot" && body.email) { + const result = await this.sendOtpOnMailForgot(body); + } else { + // Default (e.g., forgot) uses existing SMS path + const smsResult = await this.sendOTPOnMobile(mobile, reason); + notificationPayload = smsResult.notificationPayload; + hash = smsResult.hash; + expires = smsResult.expires; + } } // Step 2: Send success response @@ -2409,9 +2568,11 @@ export class PostgresUserService implements IServicelocator { data: { message: `OTP sent to ${mobile}`, hash: `${hash}.${expires}`, - sendStatus: notificationPayload?.result?.whatsapp?.data?.[0] || notificationPayload?.result?.sms?.data?.[0] + sendStatus: + notificationPayload?.result?.whatsapp?.data?.[0] || + notificationPayload?.result?.sms?.data?.[0], // sid: message.sid, // Twilio Message SID - } + }, }; return await APIResponse.success( response, @@ -2420,8 +2581,7 @@ export class PostgresUserService implements IServicelocator { HttpStatus.OK, API_RESPONSES.OTP_SEND_SUCCESSFULLY ); - } - catch (e) { + } catch (e) { LoggerUtil.error( `${API_RESPONSES.SERVER_ERROR}`, `Error: ${e.message}`, @@ -2442,24 +2602,35 @@ export class PostgresUserService implements IServicelocator { // Step 1: Format mobile number and generate OTP const mobileWithCode = this.formatMobileNumber(mobile); const otp = this.authUtils.generateOtp(this.otpDigits).toString(); - const { hash, expires, expiresInMinutes } = this.generateOtpHash(mobileWithCode, otp, reason); + const { hash, expires, expiresInMinutes } = this.generateOtpHash( + mobileWithCode, + otp, + reason + ); const replacements = { "{OTP}": otp, - "{otpExpiry}": expiresInMinutes + "{otpExpiry}": expiresInMinutes, }; // Step 2:send SMS notification - const notificationPayload = await this.smsNotification("OTP", "SEND_OTP", replacements, [mobile]); + const notificationPayload = await this.smsNotification( + "OTP", + "SEND_OTP", + replacements, + [mobile] + ); // Step 3: For signup/login, also send via WhatsApp only (do not trigger other channels) - if (reason === 'signup' || reason === 'login') { + if (reason === "signup" || reason === "login") { try { await this.sendOtpOnWhatsApp(mobile, otp, reason); } catch (waErr: any) { - LoggerUtil.warn(`WhatsApp OTP send failed: ${waErr?.message || waErr}`, APIID.SEND_OTP); + LoggerUtil.warn( + `WhatsApp OTP send failed: ${waErr?.message || waErr}`, + APIID.SEND_OTP + ); } } return { notificationPayload, hash, expires, expiresInMinutes }; - } - catch (error) { + } catch (error) { throw new Error(`Failed to send OTP: ${error.message}`); } } @@ -2467,11 +2638,18 @@ export class PostgresUserService implements IServicelocator { async sendOtpOnWhatsApp(whatsapp: string, otp: string, reason: string) { try { const formattedWhatsapp = this.formatMobileNumber(whatsapp); - const { hash, expires, expiresInMinutes } = this.generateOtpHash(formattedWhatsapp, otp, reason); - const notificationPayload = await this.whatsappNotificationRaw(whatsapp, otp, reason); + const { hash, expires, expiresInMinutes } = this.generateOtpHash( + formattedWhatsapp, + otp, + reason + ); + const notificationPayload = await this.whatsappNotificationRaw( + whatsapp, + otp, + reason + ); return { notificationPayload, hash, expires, expiresInMinutes }; - } - catch (error) { + } catch (error) { throw new Error(`Failed to send OTP via WhatsApp: ${error.message}`); } } @@ -2508,24 +2686,39 @@ export class PostgresUserService implements IServicelocator { }, }; - const mailSend = await this.notificationRequest.sendRawNotification(payload); - if (mailSend?.result?.whatsapp?.errors && mailSend.result.whatsapp.errors.length > 0) { - const errorMessages = mailSend.result.whatsapp.errors.map((error: { error: string; }) => error.error); + const mailSend = await this.notificationRequest.sendRawNotification( + payload + ); + if ( + mailSend?.result?.whatsapp?.errors && + mailSend.result.whatsapp.errors.length > 0 + ) { + const errorMessages = mailSend.result.whatsapp.errors.map( + (error: { error: string }) => error.error + ); const combinedErrorMessage = errorMessages.join(", "); - throw new Error(`${API_RESPONSES.WHATSAPP_ERROR} :${combinedErrorMessage}`); + throw new Error( + `${API_RESPONSES.WHATSAPP_ERROR} :${combinedErrorMessage}` + ); } if (!mailSend || !mailSend.result || !mailSend.result.whatsapp) { throw new Error("Invalid response from notification service"); } return mailSend; - } - catch (error) { + } catch (error) { LoggerUtil.error(API_RESPONSES.WHATSAPP_ERROR, error.message); - throw new Error(`${API_RESPONSES.WHATSAPP_NOTIFICATION_ERROR}: ${error.message}`); + throw new Error( + `${API_RESPONSES.WHATSAPP_NOTIFICATION_ERROR}: ${error.message}` + ); } } - async whatsappNotification(context: string, key: string, replacements: object, receipients: string[]) { + async whatsappNotification( + context: string, + key: string, + replacements: object, + receipients: string[] + ) { try { const notificationPayload = { isQueue: false, @@ -2539,16 +2732,24 @@ export class PostgresUserService implements IServicelocator { const result = await this.notificationRequest.sendNotification( notificationPayload ); - if (result?.result?.whatsapp?.errors && result.result.whatsapp.errors.length > 0) { - const errorMessages = result.result.whatsapp.errors.map((error: { error: string; }) => error.error); + if ( + result?.result?.whatsapp?.errors && + result.result.whatsapp.errors.length > 0 + ) { + const errorMessages = result.result.whatsapp.errors.map( + (error: { error: string }) => error.error + ); const combinedErrorMessage = errorMessages.join(", "); - throw new Error(`${API_RESPONSES.WHATSAPP_ERROR} :${combinedErrorMessage}`); + throw new Error( + `${API_RESPONSES.WHATSAPP_ERROR} :${combinedErrorMessage}` + ); } return result; - } - catch (error) { + } catch (error) { LoggerUtil.error(API_RESPONSES.WHATSAPP_ERROR, error.message); - throw new Error(`${API_RESPONSES.WHATSAPP_NOTIFICATION_ERROR}: ${error.message}`); + throw new Error( + `${API_RESPONSES.WHATSAPP_NOTIFICATION_ERROR}: ${error.message}` + ); } } // verify OTP based on reason [signup , forgot , login] @@ -2571,7 +2772,7 @@ export class PostgresUserService implements IServicelocator { } // Validate hash format - const [hashValue, expires] = hash.split('.'); + const [hashValue, expires] = hash.split("."); if (!hashValue || !expires || isNaN(parseInt(expires))) { return APIResponse.error( response, @@ -2597,7 +2798,7 @@ export class PostgresUserService implements IServicelocator { let resetToken: string | null = null; // Process based on reason - if (reason === 'signup' || reason === 'login') { + if (reason === "signup" || reason === "login") { if (!mobile) { return APIResponse.error( response, @@ -2608,8 +2809,7 @@ export class PostgresUserService implements IServicelocator { ); } identifier = this.formatMobileNumber(mobile); - } - else if (reason === 'forgot') { + } else if (reason === "forgot") { if (!username) { return APIResponse.error( response, @@ -2644,8 +2844,7 @@ export class PostgresUserService implements IServicelocator { this.jwt_password_reset_expires_In, this.jwt_secret ); - } - else { + } else { return APIResponse.error( response, apiId, @@ -2662,27 +2861,39 @@ export class PostgresUserService implements IServicelocator { // Base response const responseData = { success: true }; // For login flow, attempt to return Keycloak tokens for the user identified by mobile - if (reason === 'login') { + if (reason === "login") { try { - const dbUser = await this.usersRepository.findOne({ where: { mobile: Number(mobile) } }); + const dbUser = await this.usersRepository.findOne({ + where: { mobile: Number(mobile) }, + }); const usernameToLookup = dbUser?.username; if (usernameToLookup) { - const tokens = await getKeycloakTokensForUsername(usernameToLookup); + const tokens = await getKeycloakTokensForUsername( + usernameToLookup + ); if (tokens?.access_token) { - responseData['access_token'] = tokens.access_token; - if (tokens?.refresh_token) responseData['refresh_token'] = tokens.refresh_token; - if (tokens?.expires_in) responseData['expires_in'] = tokens.expires_in; - if (tokens?.refresh_expires_in) responseData['refresh_expires_in'] = tokens.refresh_expires_in; - if (tokens?.token_type) responseData['token_type'] = tokens.token_type; + responseData["access_token"] = tokens.access_token; + if (tokens?.refresh_token) + responseData["refresh_token"] = tokens.refresh_token; + if (tokens?.expires_in) + responseData["expires_in"] = tokens.expires_in; + if (tokens?.refresh_expires_in) + responseData["refresh_expires_in"] = + tokens.refresh_expires_in; + if (tokens?.token_type) + responseData["token_type"] = tokens.token_type; } } } catch (ex) { // Non-blocking: OTP success even if token exchange fails - LoggerUtil.warn(`Keycloak token exchange failed: ${ex?.message || ex}`, APIID.VERIFY_OTP); + LoggerUtil.warn( + `Keycloak token exchange failed: ${ex?.message || ex}`, + APIID.VERIFY_OTP + ); } } - if (reason === 'forgot' && resetToken) { - responseData['token'] = resetToken; + if (reason === "forgot" && resetToken) { + responseData["token"] = resetToken; } return APIResponse.success( @@ -2718,9 +2929,13 @@ export class PostgresUserService implements IServicelocator { } } - // send Mobile Notification - async smsNotification(context: string, key: string, replacements: object, receipients: string[]) { + async smsNotification( + context: string, + key: string, + replacements: object, + receipients: string[] + ) { try { //sms notification Body const notificationPayload = { @@ -2737,21 +2952,30 @@ export class PostgresUserService implements IServicelocator { notificationPayload ); // Check for errors in the response - if (mailSend?.result?.sms?.errors && mailSend.result.sms.errors.length > 0) { - const errorMessages = mailSend.result.sms.errors.map((error: { error: string; }) => error.error); + if ( + mailSend?.result?.sms?.errors && + mailSend.result.sms.errors.length > 0 + ) { + const errorMessages = mailSend.result.sms.errors.map( + (error: { error: string }) => error.error + ); const combinedErrorMessage = errorMessages.join(", "); // Combine all error messages into one string throw new Error(`${API_RESPONSES.SMS_ERROR} :${combinedErrorMessage}`); } return mailSend; - } - catch (error) { + } catch (error) { LoggerUtil.error(API_RESPONSES.SMS_ERROR, error.message); - throw new Error(`${API_RESPONSES.SMS_NOTIFICATION_ERROR}: ${error.message}`); + throw new Error( + `${API_RESPONSES.SMS_NOTIFICATION_ERROR}: ${error.message}` + ); } } //send OTP on mobile and email for forgot password reset - async sendPasswordResetOTP(body: SendPasswordResetOTPDto, response: Response): Promise { + async sendPasswordResetOTP( + body: SendPasswordResetOTPDto, + response: Response + ): Promise { const apiId = APIID.SEND_RESET_OTP; try { const username = body.username; @@ -2778,20 +3002,29 @@ export class PostgresUserService implements IServicelocator { ); } - const programName = userData?.tenantData[0]?.tenantName ?? ''; + const programName = userData?.tenantData[0]?.tenantName ?? ""; const reason = "forgot"; const otp = this.authUtils.generateOtp(this.otpDigits).toString(); - const { hash, expires, expiresInMinutes } = this.generateOtpHash(username, otp, reason); + const { hash, expires, expiresInMinutes } = this.generateOtpHash( + username, + otp, + reason + ); if (userData.mobile) { const replacements = { "{OTP}": otp, - "{otpExpiry}": expiresInMinutes + "{otpExpiry}": expiresInMinutes, }; try { - await this.smsNotification("OTP", "Reset_OTP", replacements, [userData.mobile]); - success.push({ type: 'SMS', message: API_RESPONSES.MOBILE_SENT_OTP }); + await this.smsNotification("OTP", "Reset_OTP", replacements, [ + userData.mobile, + ]); + success.push({ type: "SMS", message: API_RESPONSES.MOBILE_SENT_OTP }); } catch (e) { - error.push({ type: 'SMS', message: `${API_RESPONSES.MOBILE_OTP_SEND_FAILED} ${e.message}` }) + error.push({ + type: "SMS", + message: `${API_RESPONSES.MOBILE_OTP_SEND_FAILED} ${e.message}`, + }); } } @@ -2800,23 +3033,38 @@ export class PostgresUserService implements IServicelocator { "{OTP}": otp, "{otpExpiry}": expiresInMinutes, "{programName}": programName, - "{username}": username + "{username}": username, }; try { - await this.sendEmailNotification("OTP", "Reset_OTP", replacements, [userData.email]); - success.push({ type: 'Email', message: API_RESPONSES.EMAIL_SENT_OTP }) + await this.sendEmailNotification("OTP", "Reset_OTP", replacements, [ + userData.email, + ]); + success.push({ + type: "Email", + message: API_RESPONSES.EMAIL_SENT_OTP, + }); } catch (e) { - error.push({ type: 'Email', message: `${API_RESPONSES.EMAIL_OTP_SEND_FAILED}: ${e.message}` }) + error.push({ + type: "Email", + message: `${API_RESPONSES.EMAIL_OTP_SEND_FAILED}: ${e.message}`, + }); } } - // Error - if (error.length === 2) { // if both SMS and Email notification fail to sent - let errorMessage = ''; - if (error.some(e => e.type === 'SMS')) { - errorMessage += `SMS Error: ${error.filter(e => e.type === 'SMS').map(e => e.message).join(", ")}. `; + // Error + if (error.length === 2) { + // if both SMS and Email notification fail to sent + let errorMessage = ""; + if (error.some((e) => e.type === "SMS")) { + errorMessage += `SMS Error: ${error + .filter((e) => e.type === "SMS") + .map((e) => e.message) + .join(", ")}. `; } - if (error.some(e => e.type === 'Email')) { - errorMessage += `Email Error: ${error.filter(e => e.type === 'Email').map(e => e.message).join(", ")}.`; + if (error.some((e) => e.type === "Email")) { + errorMessage += `Email Error: ${error + .filter((e) => e.type === "Email") + .map((e) => e.message) + .join(", ")}.`; } return APIResponse.error( @@ -2830,8 +3078,8 @@ export class PostgresUserService implements IServicelocator { const result = { hash: `${hash}.${expires}`, success: success, - Error: error - } + Error: error, + }; return await APIResponse.success( response, apiId, @@ -2839,8 +3087,7 @@ export class PostgresUserService implements IServicelocator { HttpStatus.OK, API_RESPONSES.SEND_OTP ); - } - catch (e) { + } catch (e) { return APIResponse.error( response, apiId, @@ -2849,11 +3096,15 @@ export class PostgresUserService implements IServicelocator { HttpStatus.INTERNAL_SERVER_ERROR ); } - } //send Email Notification - async sendEmailNotification(context: string, key: string, replacements: object, emailReceipt) { + async sendEmailNotification( + context: string, + key: string, + replacements: object, + emailReceipt + ) { try { //Send Notification const notificationPayload = { @@ -2870,16 +3121,22 @@ export class PostgresUserService implements IServicelocator { const mailSend = await this.notificationRequest.sendNotification( notificationPayload ); - if (mailSend?.result?.email?.errors && mailSend.result.email.errors.length > 0) { - const errorMessages = mailSend.result.email.errors.map((error: { error: string; }) => error.error); + if ( + mailSend?.result?.email?.errors && + mailSend.result.email.errors.length > 0 + ) { + const errorMessages = mailSend.result.email.errors.map( + (error: { error: string }) => error.error + ); const combinedErrorMessage = errorMessages.join(", "); // Combine all error messages into one string throw new Error(`error :${combinedErrorMessage}`); } return mailSend; - } - catch (e) { + } catch (e) { LoggerUtil.error(API_RESPONSES.EMAIL_ERROR, e.message); - throw new Error(`${API_RESPONSES.EMAIL_NOTIFICATION_ERROR}: ${e.message}`); + throw new Error( + `${API_RESPONSES.EMAIL_NOTIFICATION_ERROR}: ${e.message}` + ); } } @@ -2887,11 +3144,16 @@ export class PostgresUserService implements IServicelocator { try { // Step 1: Generate OTP and hash const otp = this.authUtils.generateOtp(this.otpDigits).toString(); - const { hash, expires, expiresInMinutes } = this.generateOtpHash(email, otp, reason); + const { hash, expires, expiresInMinutes } = this.generateOtpHash( + email, + otp, + reason + ); // Step 2: Get program name from user's tenant data const userData: any = await this.findUserDetails(null, username); - const programName = userData?.tenantData?.[0]?.tenantName ?? 'Shiksha Graha'; + const programName = + userData?.tenantData?.[0]?.tenantName ?? "Shiksha Graha"; // Step 3: Prepare email replacements const replacements = { @@ -2900,25 +3162,25 @@ export class PostgresUserService implements IServicelocator { "{programName}": programName, "{username}": username, "{eventName}": "Shiksha Graha OTP", - "{action}": "register" + "{action}": "register", }; // console.log("hii",replacements,email) // Step 4: Send email notification - const notificationPayload = await this.sendEmailNotification("OTP", "SendOtpOnMail", replacements, [email]); + const notificationPayload = await this.sendEmailNotification( + "OTP", + "SendOtpOnMail", + replacements, + [email] + ); return { notificationPayload, hash, expires, expiresInMinutes }; - } - catch (error) { + } catch (error) { throw new Error(`Failed to send OTP via email: ${error.message}`); } } - async checkUser( - request: any, - response: any, - filters: ExistUserDto - ) { + async checkUser(request: any, response: any, filters: ExistUserDto) { const apiId = APIID.USER_LIST; try { const whereClause: any = {}; @@ -2926,7 +3188,12 @@ export class PostgresUserService implements IServicelocator { if (filters && Object.keys(filters).length > 0) { Object.entries(filters).forEach(([key, value]) => { if (value !== undefined && value !== null) { - if (key === 'firstName' || key === 'name' || key === 'middleName' || key === 'lastName') { + if ( + key === "firstName" || + key === "name" || + key === "middleName" || + key === "lastName" + ) { const sanitizedValue = this.sanitizeInput(value); whereClause[key] = ILike(`%${sanitizedValue}%`); } else { @@ -2938,7 +3205,14 @@ export class PostgresUserService implements IServicelocator { // Use the dynamic where clause to fetch matching data const findData = await this.usersRepository.find({ where: whereClause, - select: ['username', 'firstName', 'name', 'middleName', 'lastName', 'mobile'], // Select only these fields + select: [ + "username", + "firstName", + "name", + "middleName", + "lastName", + "mobile", + ], // Select only these fields }); if (findData.length === 0) { @@ -2976,16 +3250,19 @@ export class PostgresUserService implements IServicelocator { } } sanitizeInput(value) { - if (typeof value === 'string') { + if (typeof value === "string") { // Escape special characters for SQL - return value.replace(/[%_\\]/g, '\\$&'); + return value.replace(/[%_\\]/g, "\\$&"); } // For other types, return the value as is or implement specific sanitization logic return value; } - - async suggestUsername(request: Request, response: Response, suggestUserDto: SuggestUserDto) { + async suggestUsername( + request: Request, + response: Response, + suggestUserDto: SuggestUserDto + ) { const apiId = APIID.USER_LIST; try { // Fetch user data from the database to check if the username already exists @@ -2994,7 +3271,7 @@ export class PostgresUserService implements IServicelocator { }); if (findData) { - // Define a function to generate a username + // Define a function to generate a username const generateUsername = (): string => { const randomNum = randomInt(100, 1000); // Secure random 3-digit number return `${suggestUserDto.firstName}${suggestUserDto.lastName}${randomNum}`; @@ -3035,7 +3312,6 @@ export class PostgresUserService implements IServicelocator { API_RESPONSES.NOT_FOUND, HttpStatus.NOT_FOUND ); - } catch (error) { // Handle errors gracefully const errorMessage = error.message || API_RESPONSES.SERVER_ERROR; @@ -3056,7 +3332,7 @@ export class PostgresUserService implements IServicelocator { * @param apiId API ID for logging */ private async publishUserEvent( - eventType: 'created' | 'updated' | 'deleted', + eventType: "created" | "updated" | "deleted", userId: string, apiId: string ): Promise { @@ -3064,10 +3340,10 @@ export class PostgresUserService implements IServicelocator { // For delete events, we may want to include just basic information since the user might already be removed let userData: any; - if (eventType === 'deleted') { + if (eventType === "deleted") { userData = { userId: userId, - deletedAt: new Date().toISOString() + deletedAt: new Date().toISOString(), }; } else { // For create and update, fetch complete data from DB @@ -3088,20 +3364,25 @@ export class PostgresUserService implements IServicelocator { "email", "createdAt", "updatedAt", - "status" - ] + "status", + ], }); if (!user) { - LoggerUtil.error(`Failed to fetch user data for Kafka event`, `User with ID ${userId} not found`); + LoggerUtil.error( + `Failed to fetch user data for Kafka event`, + `User with ID ${userId} not found` + ); userData = { userId }; } else { // Get tenant and role information const tenantRoleData = await this.userTenantRoleData(userId); // Get custom fields if any - const customFields = await this.fieldsService.getCustomFieldDetails(userId, 'Users'); - + const customFields = await this.fieldsService.getCustomFieldDetails( + userId, + "Users" + ); // Get cohort information for the user let cohorts = []; @@ -3134,9 +3415,12 @@ export class PostgresUserService implements IServicelocator { LEFT JOIN public."AcademicYears" ay ON cay."academicYearId" = ay."id" `; - const cohortResults = await this.usersRepository.query(cohortQuery, [userId]); + const cohortResults = await this.usersRepository.query( + cohortQuery, + [userId] + ); if (cohortResults && cohortResults.length > 0) { - cohorts = cohortResults.map(result => ({ + cohorts = cohortResults.map((result) => ({ // Batch details batchId: result.batchId, batchName: result.batchName, @@ -3152,7 +3436,7 @@ export class PostgresUserService implements IServicelocator { // Academic Year details academicYearId: result.academicYearId, - academicYearSession: result.academicYearSession + academicYearSession: result.academicYearSession, })); } } catch (cohortError) { @@ -3171,7 +3455,7 @@ export class PostgresUserService implements IServicelocator { tenantData: tenantRoleData, customFields: customFields || [], cohorts: cohorts, - eventTimestamp: new Date().toISOString() + eventTimestamp: new Date().toISOString(), }; } } catch (error) { @@ -3184,7 +3468,10 @@ export class PostgresUserService implements IServicelocator { } } await this.kafkaService.publishUserEvent(eventType, userData, userId); - LoggerUtil.log(`User ${eventType} event published to Kafka for user ${userId}`, apiId); + LoggerUtil.log( + `User ${eventType} event published to Kafka for user ${userId}`, + apiId + ); } catch (error) { LoggerUtil.error( `Failed to publish user ${eventType} event to Kafka`, @@ -3199,7 +3486,7 @@ export class PostgresUserService implements IServicelocator { try { const conditions: any[] = [ { email: ILike(identifier) }, - { username: ILike(identifier) } + { username: ILike(identifier) }, ]; const isNumeric = /^\d+$/.test(identifier.trim()); @@ -3210,8 +3497,42 @@ export class PostgresUserService implements IServicelocator { const user = await this.usersRepository.findOne({ where: conditions }); return user || null; } catch (error) { - LoggerUtil.error('Error finding user by identifier', error.message); + LoggerUtil.error("Error finding user by identifier", error.message); return null; } } + + async sendOtpOnMailForgot(body) { + const { email, username, replacements, reason, key } = body; + try { + // Step 1: Generate OTP and hash + const otp = this.authUtils.generateOtp(this.otpDigits).toString(); + const { hash, expires, expiresInMinutes } = this.generateOtpHash( + email, + otp, + reason + ); + + // Step 2: Prepare email replacements + const userReplacements = { + "{OTP}": otp, + "{username}": username || "User", + "{otpExpiry}": expiresInMinutes, + "{action}": reason, + ...(replacements || {}), + }; + + // Step 3: Send email notification + const notificationPayload = await this.sendEmailNotification( + "OTP", + key, + userReplacements, + [email] + ); + + return { notificationPayload, hash, expires, expiresInMinutes }; + } catch (error) { + throw new Error(`Failed to send OTP via email: ${error.message}`); + } + } } diff --git a/src/adapters/userservicelocator.ts b/src/adapters/userservicelocator.ts index 47c5dc91..a194bc03 100644 --- a/src/adapters/userservicelocator.ts +++ b/src/adapters/userservicelocator.ts @@ -1,7 +1,11 @@ import { Request, Response } from "express"; import { OtpSendDTO } from "src/user/dto/otpSend.dto"; import { UserCreateDto } from "src/user/dto/user-create.dto"; -import { ExistUserDto, SuggestUserDto, UserSearchDto } from "src/user/dto/user-search.dto"; +import { + ExistUserDto, + SuggestUserDto, + UserSearchDto, +} from "src/user/dto/user-search.dto"; import { OtpVerifyDTO } from "src/user/dto/otpVerify.dto"; import { UserData } from "src/user/user.controller"; import { SendPasswordResetOTPDto } from "src/user/dto/passwordReset.dto"; @@ -18,7 +22,12 @@ export interface IServicelocator { // ); getUsersDetailsById(userData: UserData, response: any); updateUser(userDto?: UserUpdateDTO, response?: Response): Promise; - createUser(request: any, userDto: UserCreateDto, academicYearId: string, response: Response); + createUser( + request: any, + userDto: UserCreateDto, + academicYearId: string, + response: Response + ); findUserDetails(userID: any, username: string, tenantId?: string); searchUser( tenantId: string, @@ -33,12 +42,24 @@ export interface IServicelocator { response: Response ); checkUser(request: Request, response: Response, existUserDto: ExistUserDto); - suggestUsername(request: Request, response:Response, suggestUserDto: SuggestUserDto); + suggestUsername( + request: Request, + response: Response, + suggestUserDto: SuggestUserDto + ); deleteUserById(userId: string, response: Response): Promise; - sendPasswordResetLink(request: any, username: string, redirectUrl: string, response: Response); + sendPasswordResetLink( + request: any, + username: string, + redirectUrl: string, + response: Response + ); forgotPassword(request: any, body: any, response: Response); - sendOtp(body: OtpSendDTO, response: Response): Promise; + sendOtp(body: any, response: Response): Promise; verifyOtp(body: OtpVerifyDTO, response: Response): Promise; - sendPasswordResetOTP(body: SendPasswordResetOTPDto, response: Response): Promise; + sendPasswordResetOTP( + body: SendPasswordResetOTPDto, + response: Response + ): Promise; findUserByIdentifier(identifier: string): Promise; } diff --git a/src/cohortMembers/cohortMembers.controller.ts b/src/cohortMembers/cohortMembers.controller.ts index 04aeca91..be447390 100644 --- a/src/cohortMembers/cohortMembers.controller.ts +++ b/src/cohortMembers/cohortMembers.controller.ts @@ -48,7 +48,7 @@ import { GetUserId } from "src/common/decorators/getUserId.decorator"; @Controller("cohortmember") @UseGuards(JwtAuthGuard) export class CohortMembersController { - constructor(private readonly cohortMemberAdapter: CohortMembersAdapter) { } + constructor(private readonly cohortMemberAdapter: CohortMembersAdapter) {} //create cohort members @UseFilters(new AllExceptionsFilter(APIID.COHORT_MEMBER_CREATE)) @@ -194,7 +194,7 @@ export class CohortMembersController { @ApiNotFoundResponse({ description: "Data not found" }) @ApiBadRequestResponse({ description: "Bad request" }) @ApiBody({ type: CohortMembersUpdateDto }) - @UsePipes(new ValidationPipe()) + @UsePipes(new ValidationPipe()) public async updateCohortMembers( @Param("cohortmembershipid") cohortMembersId: string, @Req() request, @@ -205,9 +205,7 @@ export class CohortMembersController { ) { const loginUser = userId; if (!loginUser || !isUUID(loginUser)) { - throw new BadRequestException( - "unauthorized!" - ); + throw new BadRequestException("unauthorized!"); } const result = await this.cohortMemberAdapter .buildCohortMembersAdapter() @@ -250,14 +248,19 @@ export class CohortMembersController { @UsePipes(new ValidationPipe()) // @ApiBasicAuth("access-token") @ApiHeader({ - name: "tenantid", required: true + name: "tenantid", + required: true, }) @ApiHeader({ - name: "academicyearid", required: true + name: "academicyearid", + required: true, }) @ApiQuery({ - name: 'userId', required: true, type: 'string', description: 'userId required', - example: '123e4567-e89b-12d3-a456-426614174000', + name: "userId", + required: true, + type: "string", + description: "userId required", + example: "123e4567-e89b-12d3-a456-426614174000", }) @ApiCreatedResponse({ description: "Cohort Member has been created successfully.", @@ -273,9 +276,7 @@ export class CohortMembersController { const tenantId = headers["tenantid"]; const academicyearId = headers["academicyearid"]; if (!loginUser || !isUUID(loginUser)) { - throw new BadRequestException( - "unauthorized!" - ); + throw new BadRequestException("unauthorized!"); } if (!tenantId || !isUUID(tenantId)) { throw new BadRequestException(API_RESPONSES.TENANTID_VALIDATION); diff --git a/src/cohortMembers/dto/cohortMember-update.dto.ts b/src/cohortMembers/dto/cohortMember-update.dto.ts index 7d72f0ba..70f7ef9d 100644 --- a/src/cohortMembers/dto/cohortMember-update.dto.ts +++ b/src/cohortMembers/dto/cohortMember-update.dto.ts @@ -80,6 +80,10 @@ export class CohortMembersUpdateDto { @Type(() => FieldValuesOptionDto) customFields?: FieldValuesOptionDto[]; + @IsOptional() + @Expose() + params?: Object; + constructor(obj: any) { Object.assign(this, obj); } diff --git a/src/cohortMembers/entities/cohort-member.entity.ts b/src/cohortMembers/entities/cohort-member.entity.ts index 95a9f606..f94d7cf0 100644 --- a/src/cohortMembers/entities/cohort-member.entity.ts +++ b/src/cohortMembers/entities/cohort-member.entity.ts @@ -48,4 +48,7 @@ export class CohortMembers { default: MemberStatus.ACTIVE, }) status: MemberStatus; + + @Column({ type: "json", nullable: true }) + params: Object; } diff --git a/src/user/user.controller.ts b/src/user/user.controller.ts index 08ecee92..d1df5520 100644 --- a/src/user/user.controller.ts +++ b/src/user/user.controller.ts @@ -319,10 +319,10 @@ export class UserController { @UseFilters(new AllExceptionsFilter(APIID.SEND_OTP)) @Post("send-otp") - @ApiBody({ type: OtpSendDTO }) + //@ApiBody({ type: OtpSendDTO }) @UsePipes(new ValidationPipe({ transform: true })) @ApiOkResponse({ description: API_RESPONSES.OTP_SEND_SUCCESSFULLY }) - async sendOtp(@Body() body: OtpSendDTO, @Res() response: Response) { + async sendOtp(@Body() body: any, @Res() response: Response) { return await this.userAdapter.buildUserAdapter().sendOtp(body, response); }