From fb431513fa85415244ab0e80bc0c4d277f2975c9 Mon Sep 17 00:00:00 2001 From: Shubham4026 <142581532+Shubham4026@users.noreply.github.com> Date: Tue, 6 May 2025 11:18:50 +0530 Subject: [PATCH 01/53] Update user-adapter.ts --- src/adapters/postgres/user-adapter.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/adapters/postgres/user-adapter.ts b/src/adapters/postgres/user-adapter.ts index 68158873..3aa39763 100644 --- a/src/adapters/postgres/user-adapter.ts +++ b/src/adapters/postgres/user-adapter.ts @@ -2173,11 +2173,13 @@ 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(); + console.log(this.otpDigits, this.otpExpiry,"testing OTP changes"); const { hash, expires, expiresInMinutes } = this.generateOtpHash(mobileWithCode, otp, reason); const replacements = { "{OTP}": otp, "{otpExpiry}": expiresInMinutes }; + console.log(replacements, "replacements"); // Step 2:send SMS notification const notificationPayload = await this.smsNotification("OTP", "SEND_OTP", replacements, [mobile]); return { notificationPayload, hash, expires, expiresInMinutes }; From 7cd6e587500813c426cd38e04c68c5799e630b20 Mon Sep 17 00:00:00 2001 From: souravbhowmik1999 Date: Wed, 7 May 2025 10:18:00 +0530 Subject: [PATCH 02/53] remove otp send on email --- src/adapters/postgres/user-adapter.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/adapters/postgres/user-adapter.ts b/src/adapters/postgres/user-adapter.ts index 63704d09..513b63e8 100644 --- a/src/adapters/postgres/user-adapter.ts +++ b/src/adapters/postgres/user-adapter.ts @@ -1218,9 +1218,9 @@ export class PostgresUserService implements IServicelocator { ); } - if(userCreateDto.email){ - let sendOtp = await this.sendOtpOnMail(userCreateDto.email, userCreateDto.firstName, 'signup'); - } + // if(userCreateDto.email){ + // let sendOtp = await this.sendOtpOnMail(userCreateDto.email, userCreateDto.firstName, 'signup'); + // } //Validaion if try to assign on cohort and automaticMember if (userCreateDto.automaticMember?.value === true && userCreateDto.tenantCohortRoleMapping?.[0]?.cohortIds?.length > 0) { From 4a85eadd9a6e2af3da94b2d8ad57a03cc40b43dd Mon Sep 17 00:00:00 2001 From: souravbhowmik1999 Date: Wed, 7 May 2025 10:29:42 +0530 Subject: [PATCH 03/53] Remove Send OTP from reate user --- src/adapters/postgres/user-adapter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/adapters/postgres/user-adapter.ts b/src/adapters/postgres/user-adapter.ts index 513b63e8..f6b02b94 100644 --- a/src/adapters/postgres/user-adapter.ts +++ b/src/adapters/postgres/user-adapter.ts @@ -1219,7 +1219,7 @@ export class PostgresUserService implements IServicelocator { } // if(userCreateDto.email){ - // let sendOtp = await this.sendOtpOnMail(userCreateDto.email, userCreateDto.firstName, 'signup'); + // let sendOtp = await this.sendOtpOnMail(userCreateDto.email, userCreateDto.firstName, 'signup'); // } //Validaion if try to assign on cohort and automaticMember From af233223c67129fe3e2bb4ea5247696a3e22845a Mon Sep 17 00:00:00 2001 From: souravbhowmik1999 Date: Fri, 9 May 2025 12:29:20 +0530 Subject: [PATCH 04/53] Assign Enrolment ID to Each User --- src/adapters/postgres/cohortMembers-adapter.ts | 2 +- src/adapters/postgres/user-adapter.ts | 3 ++- src/forms/forms.controller.ts | 2 ++ src/user/entities/user-entity.ts | 3 +++ 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/adapters/postgres/cohortMembers-adapter.ts b/src/adapters/postgres/cohortMembers-adapter.ts index 58bdff91..7512e8e8 100644 --- a/src/adapters/postgres/cohortMembers-adapter.ts +++ b/src/adapters/postgres/cohortMembers-adapter.ts @@ -622,7 +622,7 @@ export class PostgresCohortMembersService { whereCase += where.map(processCondition).join(" AND "); } - let query = `SELECT U."userId", U."username", "firstName", "middleName", "lastName", R."name" AS role, U."mobile",U."deviceId", + 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 INNER JOIN public."Users" U ON CM."userId" = U."userId" diff --git a/src/adapters/postgres/user-adapter.ts b/src/adapters/postgres/user-adapter.ts index f6b02b94..6c20d07d 100644 --- a/src/adapters/postgres/user-adapter.ts +++ b/src/adapters/postgres/user-adapter.ts @@ -553,7 +553,7 @@ export class PostgresUserService implements IServicelocator { } //Get user core fields data - const query = `SELECT U."userId", U."username",U."email", U."firstName",UTM."tenantId", U."middleName", U."lastName", U."gender", U."dob", R."name" AS role, U."mobile", U."createdBy",U."updatedBy", U."createdAt", U."updatedAt", U."status", COUNT(*) OVER() AS total_count + const query = `SELECT U."userId",U."enrollmentId", U."username",U."email", U."firstName",UTM."tenantId", U."middleName", U."lastName", U."gender", U."dob", R."name" AS role, U."mobile", U."createdBy",U."updatedBy", U."createdAt", U."updatedAt", U."status", COUNT(*) OVER() AS total_count FROM public."Users" U LEFT JOIN public."CohortMembers" CM ON CM."userId" = U."userId" @@ -746,6 +746,7 @@ export class PostgresUserService implements IServicelocator { where: whereClause, select: [ "userId", + "enrollmentId", "username", "firstName", "middleName", diff --git a/src/forms/forms.controller.ts b/src/forms/forms.controller.ts index 79d8b07c..e0e6ec43 100644 --- a/src/forms/forms.controller.ts +++ b/src/forms/forms.controller.ts @@ -23,6 +23,8 @@ import { APIID } from '@utils/api-id.config'; import { isUUID } from 'class-validator'; import { API_RESPONSES } from '@utils/response.messages'; import { GetUserId } from "src/common/decorators/getUserId.decorator"; +import { Request, Response } from "express"; + @Controller("form") @ApiTags("Forms") diff --git a/src/user/entities/user-entity.ts b/src/user/entities/user-entity.ts index 13a0c565..25197c40 100644 --- a/src/user/entities/user-entity.ts +++ b/src/user/entities/user-entity.ts @@ -34,6 +34,9 @@ export class User { @Column({ type: 'enum', enum: ['male', 'female', 'transgender'], nullable: false }) gender: string; + @Column({ type: 'varchar', length: 50, nullable: false }) + enrollmentId: string; + @Column({ type: "date", nullable: true }) dob: Date; From 07749e57c3de7d1d39e95535bec5c78c5712a981 Mon Sep 17 00:00:00 2001 From: souravbhowmik1999 Date: Fri, 23 May 2025 14:43:32 +0530 Subject: [PATCH 05/53] Add createdAt Field to User Read API Output --- src/adapters/postgres/user-adapter.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/adapters/postgres/user-adapter.ts b/src/adapters/postgres/user-adapter.ts index 6c20d07d..3e71b60b 100644 --- a/src/adapters/postgres/user-adapter.ts +++ b/src/adapters/postgres/user-adapter.ts @@ -608,7 +608,7 @@ export class PostgresUserService implements IServicelocator { userId: userData.userId, }, }); - + if (checkExistUser.length == 0) { return APIResponse.error( response, @@ -756,7 +756,10 @@ export class PostgresUserService implements IServicelocator { "mobile", "email", "temporaryPassword", + "createdAt", + "updatedAt", "createdBy", + "updatedBy", "deviceId", ], }); From 3183810bb428b68813bec2b365a8d107fa9b65ec Mon Sep 17 00:00:00 2001 From: Shubham Date: Tue, 27 May 2025 13:53:58 +0530 Subject: [PATCH 06/53] msg91 Key support --- src/adapters/postgres/user-adapter.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/adapters/postgres/user-adapter.ts b/src/adapters/postgres/user-adapter.ts index 3e71b60b..f43844a4 100644 --- a/src/adapters/postgres/user-adapter.ts +++ b/src/adapters/postgres/user-adapter.ts @@ -67,6 +67,7 @@ export class PostgresUserService implements IServicelocator { private readonly otpDigits: number; private readonly smsKey: string; private readonly dataSource: DataSource; + private readonly msg91TemplateKey: string; constructor( // private axiosInstance: AxiosInstance, @@ -105,6 +106,7 @@ export class PostgresUserService implements IServicelocator { 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 } @@ -2224,11 +2226,10 @@ export class PostgresUserService implements IServicelocator { const otp = this.authUtils.generateOtp(this.otpDigits).toString(); const { hash, expires, expiresInMinutes } = this.generateOtpHash(mobileWithCode, otp, reason); const replacements = { - "{OTP}": otp, - "{otpExpiry}": expiresInMinutes + "{var1}": otp, }; // Step 2:send SMS notification - const notificationPayload = await this.smsNotification("OTP", "SEND_OTP", replacements, [mobile]); + const notificationPayload = await this.smsNotification("OTP", this.msg91TemplateKey, replacements, [mobile]); return { notificationPayload, hash, expires, expiresInMinutes }; } catch (error) { From 1b80d2e62b5a295c18aa13407b4b3ac1a304f427 Mon Sep 17 00:00:00 2001 From: Shubham4026 <142581532+Shubham4026@users.noreply.github.com> Date: Tue, 27 May 2025 14:32:40 +0530 Subject: [PATCH 07/53] Update user-adapter.ts --- src/adapters/postgres/user-adapter.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/adapters/postgres/user-adapter.ts b/src/adapters/postgres/user-adapter.ts index f43844a4..cd0b1062 100644 --- a/src/adapters/postgres/user-adapter.ts +++ b/src/adapters/postgres/user-adapter.ts @@ -2229,6 +2229,7 @@ export class PostgresUserService implements IServicelocator { "{var1}": otp, }; // Step 2:send SMS notification + console.log(this.msg91TemplateKey,"Key"); const notificationPayload = await this.smsNotification("OTP", this.msg91TemplateKey, replacements, [mobile]); return { notificationPayload, hash, expires, expiresInMinutes }; } From eea7d92e57285308e569ca6d06f12663753af93c Mon Sep 17 00:00:00 2001 From: Abhilash Dubey <124042593+AbhilashKD@users.noreply.github.com> Date: Wed, 4 Jun 2025 14:56:04 +0530 Subject: [PATCH 08/53] Update Prod-Deployment.yaml --- .github/workflows/Prod-Deployment.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/Prod-Deployment.yaml b/.github/workflows/Prod-Deployment.yaml index 670461aa..aacfea43 100644 --- a/.github/workflows/Prod-Deployment.yaml +++ b/.github/workflows/Prod-Deployment.yaml @@ -84,6 +84,4 @@ jobs: kubectl apply -f manifest/backend-updated.yaml kubectl apply -f manifest/configmap.yaml sleep 10 - kubectl get pods - kubectl get services - kubectl get deployment + kubectl get pods | grep eventmanagement From dacd7fbb5f3996437d2c3a77f37c38551c73e037 Mon Sep 17 00:00:00 2001 From: Abhilash Dubey <124042593+AbhilashKD@users.noreply.github.com> Date: Wed, 4 Jun 2025 14:57:41 +0530 Subject: [PATCH 09/53] Update Prod-Deployment.yaml --- .github/workflows/Prod-Deployment.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Prod-Deployment.yaml b/.github/workflows/Prod-Deployment.yaml index aacfea43..3b1c2c5a 100644 --- a/.github/workflows/Prod-Deployment.yaml +++ b/.github/workflows/Prod-Deployment.yaml @@ -84,4 +84,4 @@ jobs: kubectl apply -f manifest/backend-updated.yaml kubectl apply -f manifest/configmap.yaml sleep 10 - kubectl get pods | grep eventmanagement + kubectl get pods | grep backend From a39dc8f8c770814a69de422f8052887a7677bbcf Mon Sep 17 00:00:00 2001 From: Shubham Date: Wed, 11 Jun 2025 14:05:14 +0530 Subject: [PATCH 10/53] Added AWS support for SMS --- src/adapters/postgres/user-adapter.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/adapters/postgres/user-adapter.ts b/src/adapters/postgres/user-adapter.ts index 8fc7072d..323f49f1 100644 --- a/src/adapters/postgres/user-adapter.ts +++ b/src/adapters/postgres/user-adapter.ts @@ -2227,12 +2227,13 @@ export class PostgresUserService implements IServicelocator { console.log(this.otpDigits, this.otpExpiry,"testing OTP changes"); const { hash, expires, expiresInMinutes } = this.generateOtpHash(mobileWithCode, otp, reason); const replacements = { - "{var1}": otp, + "{OTP}": otp, + "{otpExpiry}": expiresInMinutes }; console.log(replacements, "replacements"); // Step 2:send SMS notification console.log(this.msg91TemplateKey,"Key"); - const notificationPayload = await this.smsNotification("OTP", this.msg91TemplateKey, replacements, [mobile]); + const notificationPayload = await this.smsNotification("OTP", "SEND_OTP", replacements, [mobile]); return { notificationPayload, hash, expires, expiresInMinutes }; } catch (error) { From 5e1b90835d5960535ab85db0a95c538fc78fb214 Mon Sep 17 00:00:00 2001 From: Shubham Date: Fri, 13 Jun 2025 18:50:40 +0530 Subject: [PATCH 11/53] Academic Year chaanges in team leader --- src/adapters/postgres/cohort-adapter.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/adapters/postgres/cohort-adapter.ts b/src/adapters/postgres/cohort-adapter.ts index ee8a579a..e061067d 100644 --- a/src/adapters/postgres/cohort-adapter.ts +++ b/src/adapters/postgres/cohort-adapter.ts @@ -1033,13 +1033,14 @@ export class PostgresCohortService { return hierarchy; } - public async getCohortDetailsByIds(ids: string[]) { - return await this.cohortRepository.find({ - where: { - cohortId: In(ids), - }, - select: ["cohortId", "name", "parentId", "type", "status"], - }); + public async getCohortDetailsByIds(ids: string[], academicYearId) { + return await this.cohortRepository + .createQueryBuilder('cohort') + .innerJoin('CohortAcademicYear', 'cay', 'cohort.cohortId = cay.cohortId') + .where('cohort.cohortId IN (:...ids)', { ids }) + .andWhere('cay.academicYearId = :academicYearId', { academicYearId }) + .select(['cohort.cohortId', 'cohort.name', 'cohort.parentId', 'cohort.type', 'cohort.status']) + .getMany(); } public async automaticMemberCohortHierarchy(requiredData, academicYearId) { @@ -1060,7 +1061,7 @@ export class PostgresCohortService { throw new Error("No cohort IDs found for the given fieldId and value."); } - const existingCohortIds = await this.getCohortDetailsByIds(cohortIds); + const existingCohortIds = await this.getCohortDetailsByIds(cohortIds,academicYearId); return existingCohortIds; } From d7ad8e57b19e319df7e4effd923da660c9bb8802 Mon Sep 17 00:00:00 2001 From: souravbhowmik1999 Date: Mon, 16 Jun 2025 18:29:32 +0530 Subject: [PATCH 12/53] Set event Type --- src/kafka/kafka.service.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/kafka/kafka.service.ts b/src/kafka/kafka.service.ts index ad4a3b93..af3b0c80 100644 --- a/src/kafka/kafka.service.ts +++ b/src/kafka/kafka.service.ts @@ -115,9 +115,23 @@ export class KafkaService implements OnModuleInit, OnModuleDestroy { } const topic = this.configService.get('KAFKA_TOPIC', 'user-events'); - + let fullEventType = ''; + switch (eventType) { + case 'created': + fullEventType = 'USER_CREATED'; + break; + case 'updated': + fullEventType = 'USER_UPDATED'; + break; + case 'deleted': + fullEventType = 'USER_DELETED'; + break; + default: + fullEventType = 'UNKNOWN_EVENT'; + break; + } const payload = { - eventType, + eventType: fullEventType, timestamp: new Date().toISOString(), userId, data: userData From eb376d28c306a381de9fe533d6d2f7c2ca97eed9 Mon Sep 17 00:00:00 2001 From: Shubham Date: Thu, 19 Jun 2025 16:13:56 +0530 Subject: [PATCH 13/53] Update made for Cohort Change kafka --- src/adapters/postgres/cohort-adapter.ts | 50 ++++++++++++++++++++++++- src/cohort/cohort.module.ts | 4 +- src/kafka/kafka.service.ts | 42 ++++++++++++++++++++- 3 files changed, 93 insertions(+), 3 deletions(-) diff --git a/src/adapters/postgres/cohort-adapter.ts b/src/adapters/postgres/cohort-adapter.ts index e061067d..b8336c01 100644 --- a/src/adapters/postgres/cohort-adapter.ts +++ b/src/adapters/postgres/cohort-adapter.ts @@ -24,6 +24,7 @@ import { CohortAcademicYear } from "src/cohortAcademicYear/entities/cohortAcadem import { PostgresCohortMembersService } from "./cohortMembers-adapter"; import { LoggerUtil } from "src/common/logger/LoggerUtil"; import { AutomaticMemberService } from "src/automatic-member/automatic-member.service"; +import { KafkaService } from "../../kafka/kafka.service"; @Injectable() export class PostgresCohortService { @@ -42,7 +43,8 @@ export class PostgresCohortService { private readonly cohortAcademicYearService: CohortAcademicYearService, private readonly postgresAcademicYearService: PostgresAcademicYearService, private readonly postgresCohortMembersService: PostgresCohortMembersService, - private readonly automaticMemberService: AutomaticMemberService + private readonly automaticMemberService: AutomaticMemberService, + private readonly kafkaService: KafkaService ) { } public async getCohortsDetails(requiredData, res) { @@ -417,6 +419,18 @@ export class PostgresCohortService { cohortCreateDto.updatedBy ); + // Publish cohort created event to Kafka + try { + await this.kafkaService.publishCohortEvent('created', response, response.cohortId); + } catch (kafkaError) { + LoggerUtil.error( + 'Failed to publish cohort created event to Kafka', + `Error: ${kafkaError.message}`, + apiId + ); + // Don't fail the request if Kafka publishing fails + } + const resBody = new ReturnResponseBody({ ...response, academicYearId: academicYearId, @@ -609,6 +623,22 @@ export class PostgresCohortService { } } + // Publish cohort updated event to Kafka + try { + const updatedCohortData = { + ...existingCohorDetails, + ...cohortUpdateDto + }; + await this.kafkaService.publishCohortEvent('updated', updatedCohortData, cohortId); + } catch (kafkaError) { + LoggerUtil.error( + 'Failed to publish cohort updated event to Kafka', + `Error: ${kafkaError.message}`, + apiId + ); + // Don't fail the request if Kafka publishing fails + } + LoggerUtil.log( API_RESPONSES.COHORT_UPDATED_SUCCESSFULLY, ) @@ -949,6 +979,24 @@ export class PostgresCohortService { await this.cohortMembersRepository.delete({ cohortId: cohortId }); await this.fieldValuesRepository.delete({ itemId: cohortId }); + // Publish cohort deleted event to Kafka + try { + const deletedCohortData = { + cohortId, + status: 'archived', + updatedBy: userId, + deletedAt: new Date().toISOString() + }; + await this.kafkaService.publishCohortEvent('deleted', deletedCohortData, cohortId); + } catch (kafkaError) { + LoggerUtil.error( + 'Failed to publish cohort deleted event to Kafka', + `Error: ${kafkaError.message}`, + apiId + ); + // Don't fail the request if Kafka publishing fails + } + return APIResponse.success( response, apiId, diff --git a/src/cohort/cohort.module.ts b/src/cohort/cohort.module.ts index dd22b7d4..38afa97d 100644 --- a/src/cohort/cohort.module.ts +++ b/src/cohort/cohort.module.ts @@ -22,6 +22,7 @@ import { User } from "src/user/entities/user-entity"; import { Tenants } from "src/userTenantMapping/entities/tenant.entity"; import { AutomaticMember } from "src/automatic-member/entity/automatic-member.entity"; import { AutomaticMemberService } from "src/automatic-member/automatic-member.service"; +import { KafkaService } from "../kafka/kafka.service"; @Module({ @@ -51,7 +52,8 @@ import { AutomaticMemberService } from "src/automatic-member/automatic-member.se CohortAcademicYearService, PostgresAcademicYearService, PostgresCohortMembersService, - AutomaticMemberService + AutomaticMemberService, + KafkaService ], }) export class CohortModule { } diff --git a/src/kafka/kafka.service.ts b/src/kafka/kafka.service.ts index af3b0c80..edf62509 100644 --- a/src/kafka/kafka.service.ts +++ b/src/kafka/kafka.service.ts @@ -11,7 +11,7 @@ export class KafkaService implements OnModuleInit, OnModuleDestroy { constructor(private configService: ConfigService) { // Retrieve Kafka config from the configuration - this.isKafkaEnabled = this.configService.get('kafkaEnabled', true); // Default to true if not specified + this.isKafkaEnabled = this.configService.get('kafkaEnabled', false); // Default to true if not specified const brokers = this.configService.get('KAFKA_BROKERS', 'localhost:9092').split(','); const clientId = this.configService.get('KAFKA_CLIENT_ID', 'user-service'); @@ -140,4 +140,44 @@ export class KafkaService implements OnModuleInit, OnModuleDestroy { await this.publishMessage(topic, payload, userId); this.logger.log(`User ${eventType} event published for user ${userId}`); } + + /** + * Publish a cohort-related event to Kafka + * + * @param eventType - The type of cohort event (created, updatetrued, deleted) + * @param cohortData - The cohort data to include in the event + * @param cohortId - The ID of the cohort (used as the message key) + */ + async publishCohortEvent(eventType: 'created' | 'updated' | 'deleted', cohortData: any, cohortId: string): Promise { + if (!this.isKafkaEnabled) { + this.logger.warn('Kafka is disabled. Skipping cohort event publish.'); + return; // Do nothing if Kafka is disabled + } + + const topic = this.configService.get('KAFKA_COHORT_TOPIC', 'cohort-events'); + let fullEventType = ''; + switch (eventType) { + case 'created': + fullEventType = 'COHORT_CREATED'; + break; + case 'updated': + fullEventType = 'COHORT_UPDATED'; + break; + case 'deleted': + fullEventType = 'COHORT_DELETED'; + break; + default: + fullEventType = 'UNKNOWN_EVENT'; + break; + } + const payload = { + eventType: fullEventType, + timestamp: new Date().toISOString(), + cohortId, + data: cohortData + }; + + await this.publishMessage(topic, payload, cohortId); + this.logger.log(`Cohort ${eventType} event published for cohort ${cohortId}`); + } } From 7df3596ab022d0caef4634bacd7ad6dcb1e7c8c8 Mon Sep 17 00:00:00 2001 From: souravbhowmik1999 Date: Mon, 23 Jun 2025 15:04:50 +0530 Subject: [PATCH 14/53] Create kafka topic --- src/kafka/kafka.service.ts | 90 +++++++++++++++++++++++++++++++++++--- 1 file changed, 85 insertions(+), 5 deletions(-) diff --git a/src/kafka/kafka.service.ts b/src/kafka/kafka.service.ts index edf62509..6f60829f 100644 --- a/src/kafka/kafka.service.ts +++ b/src/kafka/kafka.service.ts @@ -1,13 +1,15 @@ import { Injectable, OnModuleInit, OnModuleDestroy, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { Kafka, Producer } from 'kafkajs'; +import { Kafka, Producer, Admin } from 'kafkajs'; @Injectable() export class KafkaService implements OnModuleInit, OnModuleDestroy { private readonly kafka: Kafka; private producer: Producer; + private admin: Admin; private readonly logger = new Logger(KafkaService.name); private isKafkaEnabled: boolean; // Flag to check if Kafka is enabled + private topicsCreated: Set = new Set(); // Track created topics constructor(private configService: ConfigService) { // Retrieve Kafka config from the configuration @@ -27,16 +29,18 @@ export class KafkaService implements OnModuleInit, OnModuleDestroy { }); this.producer = this.kafka.producer(); + this.admin = this.kafka.admin(); } } async onModuleInit() { if (this.isKafkaEnabled) { try { + await this.connectAdmin(); await this.connectProducer(); - this.logger.log('Kafka producer initialized successfully'); + this.logger.log('Kafka producer and admin initialized successfully'); } catch (error) { - this.logger.error('Failed to initialize Kafka producer', error); + this.logger.error('Failed to initialize Kafka services', error); } } else { this.logger.log('Kafka is disabled. Skipping producer initialization.'); @@ -46,6 +50,7 @@ export class KafkaService implements OnModuleInit, OnModuleDestroy { async onModuleDestroy() { if (this.isKafkaEnabled) { await this.disconnectProducer(); + await this.disconnectAdmin(); } } @@ -68,6 +73,78 @@ export class KafkaService implements OnModuleInit, OnModuleDestroy { } } + private async connectAdmin() { + try { + await this.admin.connect(); + this.logger.log('Kafka admin connected'); + } catch (error) { + this.logger.error(`Failed to connect Kafka admin: ${error.message}`, error.stack); + throw error; + } + } + + private async disconnectAdmin() { + try { + await this.admin.disconnect(); + this.logger.log('Kafka admin disconnected'); + } catch (error) { + this.logger.error(`Failed to disconnect Kafka admin: ${error.message}`, error.stack); + } + } + + /** + * Ensure a topic exists, creating it if necessary + * + * @param topicName - The name of the topic to ensure exists + * @returns A promise that resolves when the topic is confirmed to exist + */ + private async ensureTopicExists(topicName: string): Promise { + if (!this.isKafkaEnabled) { + return; + } + + // Check if we've already created this topic in this session + if (this.topicsCreated.has(topicName)) { + return; + } + + try { + // Get list of existing topics + const existingTopics = await this.admin.listTopics(); + + // Check if topic exists + if (existingTopics.includes(topicName)) { + this.topicsCreated.add(topicName); + this.logger.debug(`Topic ${topicName} already exists`); + return; + } + + // Create the topic if it doesn't exist + await this.admin.createTopics({ + topics: [ + { + topic: topicName, + numPartitions: 1, // You can make this configurable + replicationFactor: 1, // You can make this configurable + }, + ], + }); + + this.topicsCreated.add(topicName); + this.logger.log(`Topic ${topicName} created successfully`); + } catch (error) { + // Topic might already exist, check if it's a "topic already exists" error + if (error.message && error.message.includes('already exists')) { + this.topicsCreated.add(topicName); + this.logger.debug(`Topic ${topicName} already exists`); + return; + } + + this.logger.error(`Failed to ensure topic ${topicName} exists: ${error.message}`, error.stack); + throw error; + } + } + /** * Publish a message to a Kafka topic * @@ -83,6 +160,9 @@ export class KafkaService implements OnModuleInit, OnModuleDestroy { } try { + // Ensure the topic exists before publishing + await this.ensureTopicExists(topic); + const payload = { topic, messages: [ @@ -114,7 +194,7 @@ export class KafkaService implements OnModuleInit, OnModuleDestroy { return; // Do nothing if Kafka is disabled } - const topic = this.configService.get('KAFKA_TOPIC', 'user-events'); + const topic = this.configService.get('KAFKA_TOPIC', 'user-topic'); let fullEventType = ''; switch (eventType) { case 'created': @@ -154,7 +234,7 @@ export class KafkaService implements OnModuleInit, OnModuleDestroy { return; // Do nothing if Kafka is disabled } - const topic = this.configService.get('KAFKA_COHORT_TOPIC', 'cohort-events'); + const topic = this.configService.get('KAFKA_TOPIC', 'user-topic'); let fullEventType = ''; switch (eventType) { case 'created': From 76458d409652064d77d10ef2f0bea6cce8104c88 Mon Sep 17 00:00:00 2001 From: souravbhowmik1999 Date: Tue, 24 Jun 2025 20:25:59 +0530 Subject: [PATCH 15/53] add ui configuration --- src/adapters/postgres/user-adapter.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/adapters/postgres/user-adapter.ts b/src/adapters/postgres/user-adapter.ts index 323f49f1..a7b147a3 100644 --- a/src/adapters/postgres/user-adapter.ts +++ b/src/adapters/postgres/user-adapter.ts @@ -790,6 +790,7 @@ export class PostgresUserService implements IServicelocator { T."collectionFramework", T."channelId", T.name AS tenantName, + T.params, UTM."Id" AS userTenantMappingId FROM public."UserTenantMapping" UTM @@ -828,6 +829,7 @@ export class PostgresUserService implements IServicelocator { collectionFramework: data.collectionFramework, channelId: data.channelId, userTenantMappingId: data.usertenantmappingid, + params: data.params, roleId: roleId, roleName: roleName, // privileges: privileges, From 232197f8e7219df4194896adf33ce235a382b584 Mon Sep 17 00:00:00 2001 From: souravbhowmik1999 Date: Tue, 24 Jun 2025 21:22:52 +0530 Subject: [PATCH 16/53] add params on tenant creation --- src/tenant/dto/tenant-create.dto.ts | 4 +++ src/tenant/tenant.service.ts | 40 +++++++++++++++++++++-------- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/src/tenant/dto/tenant-create.dto.ts b/src/tenant/dto/tenant-create.dto.ts index 4a7957f3..44e80f74 100644 --- a/src/tenant/dto/tenant-create.dto.ts +++ b/src/tenant/dto/tenant-create.dto.ts @@ -36,6 +36,10 @@ export class TenantCreateDto { @IsOptional() params?: object; + @ApiPropertyOptional({ type: () => Object }) + @IsOptional() + contentFilter?: object; + //file path @ApiPropertyOptional({ type: () => [String] }) @IsArray() diff --git a/src/tenant/tenant.service.ts b/src/tenant/tenant.service.ts index bd423b43..2acf3b66 100644 --- a/src/tenant/tenant.service.ts +++ b/src/tenant/tenant.service.ts @@ -1,6 +1,6 @@ import { HttpStatus, Injectable } from '@nestjs/common'; import { Tenant } from './entities/tenent.entity'; -import { ILike, In,Repository } from 'typeorm'; +import { ILike, In, Repository } from 'typeorm'; import APIResponse from "src/common/responses/response"; import { InjectRepository } from '@nestjs/typeorm'; import { API_RESPONSES } from '@utils/response.messages'; @@ -38,14 +38,14 @@ export class TenantService { let query = `SELECT * FROM public."Roles" WHERE "tenantId" = '${tenantData.tenantId}'`; let getRole = await this.tenantRepository.query(query); - if(getRole.length == 0){ + if (getRole.length == 0) { let query = `SELECT * FROM public."Roles"`; getRole = await this.tenantRepository.query(query); } // Add role details to the tenantData object let roleDetails = []; - if(getRole.length == 0){ + if (getRole.length == 0) { tenantData['role'] = null; } @@ -86,7 +86,7 @@ export class TenantService { public async searchTenants(request: Request, tenantSearchDTO: TenantSearchDTO, response: Response): Promise { let apiId = APIID.TENANT_SEARCH; - try { + try { const { limit, offset, filters } = tenantSearchDTO; const whereClause: Record = {}; @@ -96,7 +96,7 @@ export class TenantService { case 'name': whereClause[key] = ILike(`%${value}%`); break; - + case 'status': if (Array.isArray(value)) { whereClause[key] = In(value); @@ -104,12 +104,12 @@ export class TenantService { whereClause[key] = value; } break; - + default: if (value !== undefined && value !== null) { whereClause[key] = value; } - break; + break; } }); } @@ -157,9 +157,29 @@ export class TenantService { } } - public async createTenants( tenantCreateDto: TenantCreateDto, response:Response): Promise { + public async createTenants(tenantCreateDto: TenantCreateDto, response: Response): Promise { let apiId = APIID.TENANT_CREATE; try { + + + if (typeof tenantCreateDto.params === 'string') { + try { + tenantCreateDto.params = JSON.parse(tenantCreateDto.params); + } catch { + tenantCreateDto.params = {}; + } + } + + if (typeof tenantCreateDto.contentFilter === 'string') { + try { + tenantCreateDto.contentFilter = JSON.parse(tenantCreateDto.contentFilter); + } catch { + tenantCreateDto.contentFilter = {}; + } + } + + + let checkExitTenants = await this.tenantRepository.find({ where: { "name": tenantCreateDto?.name @@ -232,7 +252,7 @@ export class TenantService { result, HttpStatus.OK, API_RESPONSES.TENANT_DELETE, - ); + ); } } catch (error) { const errorMessage = error.message || API_RESPONSES.INTERNAL_SERVER_ERROR; @@ -293,7 +313,7 @@ export class TenantService { { tenantId, updatedFields: tenantUpdateDto }, // Return updated tenant information HttpStatus.OK, API_RESPONSES.TENANT_UPDATE - ); + ); } } catch (error) { From 43759d29e7f733e1a6a6df7c1845abd6a96c153d Mon Sep 17 00:00:00 2001 From: souravbhowmik1999 Date: Wed, 25 Jun 2025 17:37:03 +0530 Subject: [PATCH 17/53] add --- src/tenant/dto/tenant-update.dto.ts | 4 ++++ src/tenant/tenant.controller.ts | 1 - src/tenant/tenant.service.ts | 36 ++++++++++++++++++++--------- 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/tenant/dto/tenant-update.dto.ts b/src/tenant/dto/tenant-update.dto.ts index 3143f450..9f989274 100644 --- a/src/tenant/dto/tenant-update.dto.ts +++ b/src/tenant/dto/tenant-update.dto.ts @@ -20,6 +20,10 @@ export class TenantUpdateDto { @IsOptional() params?: object; + @ApiPropertyOptional({ type: () => Object }) + @IsOptional() + contentFilter?: object; + //file path @ApiPropertyOptional({ type: () => [String] }) @IsOptional() diff --git a/src/tenant/tenant.controller.ts b/src/tenant/tenant.controller.ts index 5a375edc..340cad2c 100644 --- a/src/tenant/tenant.controller.ts +++ b/src/tenant/tenant.controller.ts @@ -60,7 +60,6 @@ export class TenantController { @GetUserId("userId", ParseUUIDPipe) userId: string ): Promise { const uploadedFiles = []; - // Loop through each file and upload it if (files && files.length > 0) { for (const file of files) { diff --git a/src/tenant/tenant.service.ts b/src/tenant/tenant.service.ts index 2acf3b66..1b3dc53c 100644 --- a/src/tenant/tenant.service.ts +++ b/src/tenant/tenant.service.ts @@ -160,26 +160,23 @@ export class TenantService { public async createTenants(tenantCreateDto: TenantCreateDto, response: Response): Promise { let apiId = APIID.TENANT_CREATE; try { - - - if (typeof tenantCreateDto.params === 'string') { + // Parse JSON strings for params and contentFilter fields + if (tenantCreateDto.params && typeof tenantCreateDto.params === 'string') { try { tenantCreateDto.params = JSON.parse(tenantCreateDto.params); - } catch { - tenantCreateDto.params = {}; + } catch (error) { + LoggerUtil.warn(`Failed to parse params field: ${error.message}`, apiId); } } - - if (typeof tenantCreateDto.contentFilter === 'string') { + + if (tenantCreateDto.contentFilter && typeof tenantCreateDto.contentFilter === 'string') { try { tenantCreateDto.contentFilter = JSON.parse(tenantCreateDto.contentFilter); - } catch { - tenantCreateDto.contentFilter = {}; + } catch (error) { + LoggerUtil.warn(`Failed to parse contentFilter field: ${error.message}`, apiId); } } - - let checkExitTenants = await this.tenantRepository.find({ where: { "name": tenantCreateDto?.name @@ -274,6 +271,23 @@ export class TenantService { public async updateTenants(tenantId: string, tenantUpdateDto: TenantUpdateDto, response: Response) { let apiId = APIID.TENANT_UPDATE; try { + // Parse JSON strings for params and contentFilter fields + if (tenantUpdateDto.params && typeof tenantUpdateDto.params === 'string') { + try { + tenantUpdateDto.params = JSON.parse(tenantUpdateDto.params); + } catch (error) { + LoggerUtil.warn(`Failed to parse params field: ${error.message}`, apiId); + } + } + + if (tenantUpdateDto.contentFilter && typeof tenantUpdateDto.contentFilter === 'string') { + try { + tenantUpdateDto.contentFilter = JSON.parse(tenantUpdateDto.contentFilter); + } catch (error) { + LoggerUtil.warn(`Failed to parse contentFilter field: ${error.message}`, apiId); + } + } + let checkExistingTenant = await this.tenantRepository.findOne({ where: { tenantId } }) From b565521c97adbfdfd38179c41700424f07ec4e81 Mon Sep 17 00:00:00 2001 From: Shubham Date: Sun, 29 Jun 2025 01:30:33 +0530 Subject: [PATCH 18/53] Cohort Changes for Kafka --- src/adapters/postgres/user-adapter.ts | 42 +++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/adapters/postgres/user-adapter.ts b/src/adapters/postgres/user-adapter.ts index a7b147a3..6be257f1 100644 --- a/src/adapters/postgres/user-adapter.ts +++ b/src/adapters/postgres/user-adapter.ts @@ -2772,11 +2772,53 @@ export class PostgresUserService implements IServicelocator { // Get custom fields if any const customFields = await this.fieldsService.getCustomFieldDetails(userId, 'Users'); + // Get cohort information for the user + let cohorts = []; + try { + // Query cohort members with cohort details using a comprehensive query + const cohortQuery = ` + SELECT + cm."cohortId", + cm."createdAt" as "joinedAt", + cm."status" as "cohortMemberStatus", + c."name" as "cohortName", + c."type" as "cohortType", + c."status" as "cohortStatus", + c."tenantId" + FROM public."CohortMembers" cm + JOIN public."Cohort" c ON cm."cohortId" = c."cohortId" + WHERE cm."userId" = $1 + `; + + const cohortResults = await this.usersRepository.query(cohortQuery, [userId]); + + if (cohortResults && cohortResults.length > 0) { + cohorts = cohortResults.map(result => ({ + cohortId: result.cohortId, + joinedAt: result.joinedAt, + tenantId: result.tenantId, + cohortName: result.cohortName, + cohortType: result.cohortType, + cohortStatus: result.cohortStatus, + cohortMemberStatus: result.cohortMemberStatus + })); + } + } catch (cohortError) { + LoggerUtil.error( + `Failed to fetch cohort data for Kafka event`, + `Error: ${cohortError.message}`, + apiId + ); + // Don't fail the entire operation if cohort fetching fails + cohorts = []; + } + // Build the complete data object userData = { ...user, tenantData: tenantRoleData, customFields: customFields || [], + cohorts: cohorts, eventTimestamp: new Date().toISOString() }; } From c62bad32d3e8f271823b49d86b8cb1e086c4404c Mon Sep 17 00:00:00 2001 From: Shubham Date: Mon, 7 Jul 2025 15:45:54 +0530 Subject: [PATCH 19/53] Added Log for Create User an Dynamic Log generation --- src/adapters/postgres/user-adapter.ts | 92 ++++++++++++++++++++++++--- src/common/logger/LoggerUtil.ts | 15 ++++- 2 files changed, 94 insertions(+), 13 deletions(-) diff --git a/src/adapters/postgres/user-adapter.ts b/src/adapters/postgres/user-adapter.ts index 6be257f1..e5971c4b 100644 --- a/src/adapters/postgres/user-adapter.ts +++ b/src/adapters/postgres/user-adapter.ts @@ -1178,7 +1178,19 @@ export class PostgresUserService implements IServicelocator { response: Response ) { const apiId = APIID.USER_CREATE; - // It is considered that if user is not present in keycloak it is not present in database as well + const userContext = { + username: userCreateDto?.username, + email: userCreateDto?.email, + firstName: userCreateDto?.firstName, + lastName: userCreateDto?.lastName + }; + + // Log user creation attempt with context + LoggerUtil.log( + `User creation attempt started for ${userContext.username}`, + apiId, + userContext.username + ); try { if (request.headers.authorization) { @@ -1217,6 +1229,12 @@ export class PostgresUserService implements IServicelocator { Array.isArray(validatedRoles) && validatedRoles.some((item) => item?.code === undefined) ) { + LoggerUtil.error( + `Role validation failed for ${userContext.username}`, + validatedRoles.join("; "), + apiId, + userContext.username + ); return APIResponse.error( response, apiId, @@ -1232,6 +1250,12 @@ export class PostgresUserService implements IServicelocator { //Validaion if try to assign on cohort and automaticMember 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`, + apiId, + userContext.username + ); return APIResponse.error( response, apiId, @@ -1253,6 +1277,12 @@ export class PostgresUserService implements IServicelocator { ); // let checkUserinDb = await this.checkUserinKeyCloakandDb(userCreateDto.username); if (checkUserinKeyCloakandDb) { + LoggerUtil.error( + `User ${userContext.username} already exists`, + `User with username ${userCreateDto.username} or email ${userCreateDto.email} already exists`, + apiId, + userContext.username + ); return APIResponse.error( response, apiId, @@ -1263,11 +1293,22 @@ export class PostgresUserService implements IServicelocator { } // Multi tenant for roles is not currently supported in keycloak + LoggerUtil.log( + `Creating user ${userContext.username} in Keycloak`, + apiId, + userContext.username + ); + resKeycloak = await createUserInKeyCloak(userSchema, token, validatedRoles[0]?.title) if (resKeycloak.statusCode !== 201) { if (resKeycloak.statusCode === 409) { - LoggerUtil.log(API_RESPONSES.EMAIL_EXIST, apiId); + LoggerUtil.error( + `Email already exists in Keycloak for ${userContext.username}`, + `${resKeycloak.message} ${resKeycloak.email}`, + apiId, + userContext.username + ); return APIResponse.error( response, @@ -1277,7 +1318,12 @@ export class PostgresUserService implements IServicelocator { HttpStatus.CONFLICT ); } else { - LoggerUtil.log(API_RESPONSES.SERVER_ERROR, apiId); + LoggerUtil.error( + `Keycloak user creation failed for ${userContext.username}`, + `${resKeycloak.message}`, + apiId, + userContext.username + ); return APIResponse.error( response, apiId, @@ -1288,12 +1334,20 @@ export class PostgresUserService implements IServicelocator { } } - LoggerUtil.log(API_RESPONSES.USER_CREATE_KEYCLOAK, apiId); - + LoggerUtil.log( + `User ${userContext.username} created successfully in Keycloak`, + apiId, + userContext.username + ); userCreateDto.userId = resKeycloak.userId; // if cohort given then check for academic year + LoggerUtil.log( + `Creating user ${userContext.username} in database`, + apiId, + userContext.username + ); const result = await this.createUserInDatabase( request, @@ -1302,7 +1356,11 @@ export class PostgresUserService implements IServicelocator { response ); - LoggerUtil.log(API_RESPONSES.USER_CREATE_IN_DB, apiId); + LoggerUtil.log( + `User ${userContext.username} created successfully in database`, + apiId, + userContext.username + ); const createFailures = []; if ( @@ -1357,7 +1415,12 @@ export class PostgresUserService implements IServicelocator { } } } - LoggerUtil.log(API_RESPONSES.USER_CREATE_SUCCESSFULLY, apiId); + + LoggerUtil.log( + `User ${userContext.username} created successfully with ID: ${result.userId}`, + apiId, + userContext.username + ); // Send response to the client APIResponse.success( @@ -1371,16 +1434,18 @@ 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( - `Failed to publish user created event to Kafka`, + `Failed to publish user created event to Kafka for ${userContext.username}`, `Error: ${error.message}`, - apiId + apiId, + userContext.username )); } catch (e) { LoggerUtil.error( `${API_RESPONSES.SERVER_ERROR}: ${request.url}`, `Error: ${e.message}`, - apiId + apiId, + userContext.username ); const errorMessage = e.message || API_RESPONSES.INTERNAL_SERVER_ERROR; return APIResponse.error( @@ -2054,6 +2119,13 @@ export class PostgresUserService implements IServicelocator { .map((fieldValue) => fieldValue.fieldId); if (invalidFieldIds.length > 0) { + // 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'}`, + apiId, + userCreateDto.username + ); return `The following fields are not valid for this user: ${invalidFieldIds.join( ", " )}.`; diff --git a/src/common/logger/LoggerUtil.ts b/src/common/logger/LoggerUtil.ts index 1d1bbbd1..3362a650 100644 --- a/src/common/logger/LoggerUtil.ts +++ b/src/common/logger/LoggerUtil.ts @@ -1,10 +1,14 @@ import * as winston from 'winston'; +import * as path from 'path'; export class LoggerUtil { private static logger: winston.Logger; static getLogger() { if (!this.logger) { + // Get log directory from environment variable, default to 'logs' + const logDir = process.env.LOG_DIR || 'logs'; + const customFormat = winston.format.printf( ({ timestamp, level, message, context, user, error }) => { return JSON.stringify({ @@ -23,8 +27,13 @@ export class LoggerUtil { format: winston.format.combine(winston.format.timestamp(), customFormat), transports: [ new winston.transports.Console(), - new winston.transports.File({ filename: 'error.log', level: 'error' }), - new winston.transports.File({ filename: 'combined.log' }), + new winston.transports.File({ + filename: path.join(logDir, 'error.log'), + level: 'error' + }), + new winston.transports.File({ + filename: path.join(logDir, 'combined.log') + }), ], }); } @@ -66,7 +75,7 @@ export class LoggerUtil { context: context, timestamp: new Date().toISOString(), }); - } + }internet static debug(message: string, context?: string) { this.getLogger().debug({ From 9107ea25777663168899837eb9538384e0c76baf Mon Sep 17 00:00:00 2001 From: Shubham Date: Mon, 7 Jul 2025 16:29:26 +0530 Subject: [PATCH 20/53] Log File check --- src/common/logger/LoggerUtil.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/common/logger/LoggerUtil.ts b/src/common/logger/LoggerUtil.ts index 3362a650..263e5745 100644 --- a/src/common/logger/LoggerUtil.ts +++ b/src/common/logger/LoggerUtil.ts @@ -1,5 +1,6 @@ import * as winston from 'winston'; import * as path from 'path'; +import * as fs from 'fs'; export class LoggerUtil { private static logger: winston.Logger; @@ -9,6 +10,11 @@ export class LoggerUtil { // Get log directory from environment variable, default to 'logs' const logDir = process.env.LOG_DIR || 'logs'; + // Ensure log directory exists + if (!fs.existsSync(logDir)) { + fs.mkdirSync(logDir, { recursive: true }); + } + const customFormat = winston.format.printf( ({ timestamp, level, message, context, user, error }) => { return JSON.stringify({ @@ -39,6 +45,12 @@ export class LoggerUtil { } return this.logger; } + + // Method to reset logger (useful for testing or when files are deleted) + static resetLogger() { + this.logger = null; + } + static log( message: string, context?: string, @@ -75,7 +87,7 @@ export class LoggerUtil { context: context, timestamp: new Date().toISOString(), }); - }internet + } static debug(message: string, context?: string) { this.getLogger().debug({ From d63bfd742d6d039dc1a56fef6e14ef13e7551906 Mon Sep 17 00:00:00 2001 From: Shubham Date: Tue, 8 Jul 2025 21:57:48 +0530 Subject: [PATCH 21/53] Added Perfomance Checka and Bottleneck removal --- src/adapters/postgres/fields-adapter.ts | 18 +++++ src/adapters/postgres/user-adapter.ts | 87 ++++++++++++++++++------- 2 files changed, 83 insertions(+), 22 deletions(-) diff --git a/src/adapters/postgres/fields-adapter.ts b/src/adapters/postgres/fields-adapter.ts index 5879cd15..6f615135 100644 --- a/src/adapters/postgres/fields-adapter.ts +++ b/src/adapters/postgres/fields-adapter.ts @@ -1690,6 +1690,24 @@ export class PostgresFieldsService implements IServicelocatorfields { return result; } + async updateUserCustomFields(itemId, data, fieldAttributesAndParams) { + // Ensure value is stored as an array + if (!Array.isArray(data.value)) { + data.value = [data.value]; + } + + const result = await this.fieldsValuesRepository.insert({ + itemId, + fieldId: data.fieldId, + value: data.value, + }); + + return { + ...result, + correctValue: true, + }; + } + validateFieldValue(field: any, value: any) { try { const fieldInstance = FieldFactory.createField( diff --git a/src/adapters/postgres/user-adapter.ts b/src/adapters/postgres/user-adapter.ts index e5971c4b..10903e9d 100644 --- a/src/adapters/postgres/user-adapter.ts +++ b/src/adapters/postgres/user-adapter.ts @@ -1178,6 +1178,9 @@ export class PostgresUserService implements IServicelocator { response: Response ) { const apiId = APIID.USER_CREATE; + const startTime = Date.now(); + const stepTimings = {}; + const userContext = { username: userCreateDto?.username, email: userCreateDto?.email, @@ -1193,12 +1196,17 @@ export class PostgresUserService implements IServicelocator { ); try { + // Step 1: Extract user info from JWT token + const jwtStartTime = Date.now(); if (request.headers.authorization) { const decoded: any = jwt_decode(request.headers.authorization); userCreateDto.createdBy = decoded?.sub; userCreateDto.updatedBy = decoded?.sub; } + stepTimings['jwt_extraction'] = Date.now() - jwtStartTime; + // Step 2: Validate custom fields + const customFieldStartTime = Date.now(); let customFieldError; if (userCreateDto.customFields && userCreateDto.customFields.length > 0) { customFieldError = await this.validateCustomField( @@ -1217,8 +1225,10 @@ export class PostgresUserService implements IServicelocator { ); } } + stepTimings['custom_field_validation'] = Date.now() - customFieldStartTime; - // check and validate all fields + // Step 3: Validate request body and roles + const validationStartTime = Date.now(); const validatedRoles: any = await this.validateRequestBody( userCreateDto, academicYearId @@ -1243,12 +1253,10 @@ export class PostgresUserService implements IServicelocator { HttpStatus.BAD_REQUEST ); } + stepTimings['request_validation'] = Date.now() - validationStartTime; - // if(userCreateDto.email){ - // let sendOtp = await this.sendOtpOnMail(userCreateDto.email, userCreateDto.firstName, 'signup'); - // } - - //Validaion if try to assign on cohort and automaticMember + // Step 4: Validate automatic member vs cohort assignment + const businessLogicStartTime = Date.now(); if (userCreateDto.automaticMember?.value === true && userCreateDto.tenantCohortRoleMapping?.[0]?.cohortIds?.length > 0) { LoggerUtil.error( `Invalid operation for ${userContext.username}: Cannot assign automatic member with cohort`, @@ -1264,18 +1272,19 @@ export class PostgresUserService implements IServicelocator { HttpStatus.BAD_REQUEST ); } + stepTimings['business_logic_validation'] = Date.now() - businessLogicStartTime; + // Step 5: Prepare username and check Keycloak + const keycloakCheckStartTime = Date.now(); userCreateDto.username = userCreateDto.username.toLocaleLowerCase(); const userSchema = new UserCreateDto(userCreateDto); - let resKeycloak; - const keycloakResponse = await getKeycloakAdminToken(); const token = keycloakResponse.data.access_token; const checkUserinKeyCloakandDb = await this.checkUserinKeyCloakandDb( userCreateDto ); - // let checkUserinDb = await this.checkUserinKeyCloakandDb(userCreateDto.username); + if (checkUserinKeyCloakandDb) { LoggerUtil.error( `User ${userContext.username} already exists`, @@ -1291,15 +1300,34 @@ export class PostgresUserService implements IServicelocator { HttpStatus.BAD_REQUEST ); } + stepTimings['keycloak_user_check'] = Date.now() - keycloakCheckStartTime; - // Multi tenant for roles is not currently supported in keycloak + // Step 6: Create user in Keycloak + const keycloakCreateStartTime = Date.now(); LoggerUtil.log( `Creating user ${userContext.username} in Keycloak`, apiId, userContext.username ); - resKeycloak = await createUserInKeyCloak(userSchema, token, validatedRoles[0]?.title) + const resKeycloak = await createUserInKeyCloak(userSchema, token, validatedRoles[0]?.title) + + // Handle the case where createUserInKeyCloak returns a string (error) + if (typeof resKeycloak === 'string') { + LoggerUtil.error( + `Keycloak user creation failed for ${userContext.username}`, + resKeycloak, + apiId, + userContext.username + ); + return APIResponse.error( + response, + apiId, + API_RESPONSES.SERVER_ERROR, + resKeycloak, + HttpStatus.INTERNAL_SERVER_ERROR + ); + } if (resKeycloak.statusCode !== 201) { if (resKeycloak.statusCode === 409) { @@ -1342,7 +1370,8 @@ export class PostgresUserService implements IServicelocator { userCreateDto.userId = resKeycloak.userId; - // if cohort given then check for academic year + // Step 7: Create user in database + const dbCreateStartTime = Date.now(); LoggerUtil.log( `Creating user ${userContext.username} in database`, apiId, @@ -1355,6 +1384,7 @@ export class PostgresUserService implements IServicelocator { academicYearId, response ); + stepTimings['database_user_creation'] = Date.now() - dbCreateStartTime; LoggerUtil.log( `User ${userContext.username} created successfully in database`, @@ -1362,6 +1392,8 @@ export class PostgresUserService implements IServicelocator { userContext.username ); + // Step 8: Handle custom fields + const customFieldsStartTime = Date.now(); const createFailures = []; if ( result && @@ -1398,30 +1430,41 @@ export class PostgresUserService implements IServicelocator { value: fieldValues["value"], }; - const res = await this.fieldsService.updateCustomFields( + const res = await this.fieldsService.updateUserCustomFields( userId, fieldData, customFieldAttributes[fieldData.fieldId] ); - - if (res.correctValue) { - if (!result["customFields"]) result["customFields"] = []; - result["customFields"].push(res); - } else { - createFailures.push( - `${fieldData.fieldId}: ${res?.valueIssue} - ${res.fieldName}` - ); - } + console.log(res); + + // if (res.correctValue) { + // if (!result["customFields"]) result["customFields"] = []; + // result["customFields"].push(res); + // } else { + // createFailures.push( + // `${fieldData.fieldId}: ${res?.valueIssue} - ${res.fieldName}` + // ); + // } } } } + stepTimings['custom_fields_processing'] = Date.now() - customFieldsStartTime; + // Step 9: Log performance metrics + const totalTime = Date.now() - startTime; LoggerUtil.log( `User ${userContext.username} created successfully with ID: ${result.userId}`, apiId, userContext.username ); + // 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`, + apiId, + userContext.username + ); + // Send response to the client APIResponse.success( response, From 1088c2a6bfd955b1de480871dc8fd53457901d5e Mon Sep 17 00:00:00 2001 From: Shubham Date: Tue, 8 Jul 2025 22:17:53 +0530 Subject: [PATCH 22/53] Time logging --- src/adapters/postgres/user-adapter.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/adapters/postgres/user-adapter.ts b/src/adapters/postgres/user-adapter.ts index 10903e9d..d9f19667 100644 --- a/src/adapters/postgres/user-adapter.ts +++ b/src/adapters/postgres/user-adapter.ts @@ -1312,6 +1312,9 @@ export class PostgresUserService implements IServicelocator { const resKeycloak = await createUserInKeyCloak(userSchema, token, validatedRoles[0]?.title) + // Capture Keycloak creation timing immediately after the call + stepTimings['keycloak_user_creation'] = Date.now() - keycloakCreateStartTime; + // Handle the case where createUserInKeyCloak returns a string (error) if (typeof resKeycloak === 'string') { LoggerUtil.error( From 08c2939afbf530ff11e91258e75087449ca3bd65 Mon Sep 17 00:00:00 2001 From: Shubham Date: Wed, 9 Jul 2025 12:01:48 +0530 Subject: [PATCH 23/53] DB connection Increased --- src/common/database.module.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/common/database.module.ts b/src/common/database.module.ts index 1649a7c9..e02a2195 100644 --- a/src/common/database.module.ts +++ b/src/common/database.module.ts @@ -17,6 +17,11 @@ import { User } from "src/user/entities/user-entity"; // User // ], autoLoadEntities: true, + extra: { + max: 20, // Number of connections in the pool (default is 10) + idleTimeoutMillis: 30000, // 30 seconds + connectionTimeoutMillis: 2000, // 2 seconds max to wait for a free connection + }, }), inject: [ConfigService], }), From 1c3bd615546c0182b355ae303424b6f449731676 Mon Sep 17 00:00:00 2001 From: Shubham Date: Thu, 10 Jul 2025 18:35:20 +0530 Subject: [PATCH 24/53] Removed Unused Logs and No logs for file --- src/adapters/postgres/user-adapter.ts | 10 ++-------- src/common/logger/LoggerUtil.ts | 17 ----------------- src/common/services/upload-S3.service.ts | 9 +-------- src/common/utils/keycloak.adapter.util.ts | 6 ------ 4 files changed, 3 insertions(+), 39 deletions(-) diff --git a/src/adapters/postgres/user-adapter.ts b/src/adapters/postgres/user-adapter.ts index d9f19667..e9417d32 100644 --- a/src/adapters/postgres/user-adapter.ts +++ b/src/adapters/postgres/user-adapter.ts @@ -1438,7 +1438,6 @@ export class PostgresUserService implements IServicelocator { fieldData, customFieldAttributes[fieldData.fieldId] ); - console.log(res); // if (res.correctValue) { // if (!result["customFields"]) result["customFields"] = []; @@ -2344,15 +2343,12 @@ 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(); - console.log(this.otpDigits, this.otpExpiry,"testing OTP changes"); const { hash, expires, expiresInMinutes } = this.generateOtpHash(mobileWithCode, otp, reason); const replacements = { "{OTP}": otp, "{otpExpiry}": expiresInMinutes }; - console.log(replacements, "replacements"); // Step 2:send SMS notification - console.log(this.msg91TemplateKey,"Key"); const notificationPayload = await this.smsNotification("OTP", "SEND_OTP", replacements, [mobile]); return { notificationPayload, hash, expires, expiresInMinutes }; } @@ -2464,9 +2460,7 @@ export class PostgresUserService implements IServicelocator { // Verify OTP hash const data = `${identifier}.${otp}.${reason}.${expires}`; - console.log(data); const calculatedHash = this.authUtils.calculateHash(data, this.smsKey); - console.log(calculatedHash, hashValue); if (calculatedHash === hashValue) { // For forgot password flow, include the reset token in response const responseData = { success: true }; @@ -2654,7 +2648,7 @@ export class PostgresUserService implements IServicelocator { receipients: emailReceipt, }, }; - console.log("notificationPayload",notificationPayload); + // console.log("notificationPayload",notificationPayload); const mailSend = await this.notificationRequest.sendNotification( notificationPayload @@ -2691,7 +2685,7 @@ export class PostgresUserService implements IServicelocator { "{eventName}": "Shiksha Graha OTP", "{action}": "register" }; - console.log("hii",replacements,email) + // console.log("hii",replacements,email) // Step 4: Send email notification const notificationPayload = await this.sendEmailNotification("OTP", "SendOtpOnMail", replacements, [email]); diff --git a/src/common/logger/LoggerUtil.ts b/src/common/logger/LoggerUtil.ts index 263e5745..55149f95 100644 --- a/src/common/logger/LoggerUtil.ts +++ b/src/common/logger/LoggerUtil.ts @@ -1,20 +1,10 @@ import * as winston from 'winston'; -import * as path from 'path'; -import * as fs from 'fs'; export class LoggerUtil { private static logger: winston.Logger; static getLogger() { if (!this.logger) { - // Get log directory from environment variable, default to 'logs' - const logDir = process.env.LOG_DIR || 'logs'; - - // Ensure log directory exists - if (!fs.existsSync(logDir)) { - fs.mkdirSync(logDir, { recursive: true }); - } - const customFormat = winston.format.printf( ({ timestamp, level, message, context, user, error }) => { return JSON.stringify({ @@ -33,13 +23,6 @@ export class LoggerUtil { format: winston.format.combine(winston.format.timestamp(), customFormat), transports: [ new winston.transports.Console(), - new winston.transports.File({ - filename: path.join(logDir, 'error.log'), - level: 'error' - }), - new winston.transports.File({ - filename: path.join(logDir, 'combined.log') - }), ], }); } diff --git a/src/common/services/upload-S3.service.ts b/src/common/services/upload-S3.service.ts index 2852786c..b1841e83 100644 --- a/src/common/services/upload-S3.service.ts +++ b/src/common/services/upload-S3.service.ts @@ -130,14 +130,7 @@ export class UploadS3Service { API_RESPONSES.SIGNED_URL_SUCCESS ); } catch (error) { - console.error("Presigned URL Error:", error); - return APIResponse.error( - response, - APIID.SIGNED_URL, - API_RESPONSES.BAD_REQUEST, - API_RESPONSES.SIGNED_URL_FAILED, - HttpStatus.BAD_REQUEST - ); + throw new Error(`Failed to generate presigned URL: ${error.message}`); } } diff --git a/src/common/utils/keycloak.adapter.util.ts b/src/common/utils/keycloak.adapter.util.ts index 3cc9f4f0..9a939e04 100644 --- a/src/common/utils/keycloak.adapter.util.ts +++ b/src/common/utils/keycloak.adapter.util.ts @@ -101,24 +101,18 @@ async function createUserInKeyCloak(query, token, role: string) { } catch (error) { // Handle errors and log relevant details if (error.response) { - console.error("Error Response Status:", error.response.status); - console.error("Error Response Data:", error.response.data); - console.error("Error Response Headers:", error.response.headers); - return { statusCode: error.response.status, message: error.response.data.errorMessage || "Error occurred during user creation", email: query.email || "No email provided", }; } else if (error.request) { - console.error("No response received:", error.request); return { statusCode: 500, message: "No response received from Keycloak", email: query.email || "No email provided", }; } else { - console.error("Error setting up request:", error.message); return { statusCode: 500, message: `Error setting up request: ${error.message}`, From f672902cdb347dcb3cdb61defc377cb92c0c9a1f Mon Sep 17 00:00:00 2001 From: souravbhowmik1999 Date: Fri, 11 Jul 2025 12:04:09 +0530 Subject: [PATCH 25/53] Kafka changes --- src/adapters/postgres/user-adapter.ts | 52 +++++++++++++++++++-------- 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/src/adapters/postgres/user-adapter.ts b/src/adapters/postgres/user-adapter.ts index d9f19667..b80512fe 100644 --- a/src/adapters/postgres/user-adapter.ts +++ b/src/adapters/postgres/user-adapter.ts @@ -2893,32 +2893,54 @@ export class PostgresUserService implements IServicelocator { // Get cohort information for the user let cohorts = []; try { - // Query cohort members with cohort details using a comprehensive query + // Enhanced query to fetch batch, parent cohort, and academic year details const cohortQuery = ` + WITH BatchData AS ( + SELECT + cm."cohortId" as "batchId", + cm."createdAt" as "joinedAt", + cm."status" as "cohortMemberStatus", + batch."name" as "batchName", + batch."type" as "batchType", + batch."status" as "batchStatus", + batch."tenantId", + batch."parentId" as "cohortId" + FROM public."CohortMembers" cm + JOIN public."Cohort" batch ON cm."cohortId" = batch."cohortId" + WHERE cm."userId" = $1 AND batch."type" = 'BATCH' + ) SELECT - cm."cohortId", - cm."createdAt" as "joinedAt", - cm."status" as "cohortMemberStatus", - c."name" as "cohortName", - c."type" as "cohortType", - c."status" as "cohortStatus", - c."tenantId" - FROM public."CohortMembers" cm - JOIN public."Cohort" c ON cm."cohortId" = c."cohortId" - WHERE cm."userId" = $1 + bd.*, + cohort."name" as "cohortName", + cohort."type" as "cohortType", + cay."academicYearId", + ay."session" as "academicYearSession" + FROM BatchData bd + LEFT JOIN public."Cohort" cohort ON bd."cohortId" = cohort."cohortId" AND cohort."type" = 'COHORT' + LEFT JOIN public."CohortAcademicYear" cay ON bd."cohortId" = cay."cohortId" + LEFT JOIN public."AcademicYears" ay ON cay."academicYearId" = ay."academicYearId" `; const cohortResults = await this.usersRepository.query(cohortQuery, [userId]); if (cohortResults && cohortResults.length > 0) { cohorts = cohortResults.map(result => ({ - cohortId: result.cohortId, + // Batch details + batchId: result.batchId, + batchName: result.batchName, + batchStatus: result.batchStatus, joinedAt: result.joinedAt, + cohortMemberStatus: result.cohortMemberStatus, tenantId: result.tenantId, + + // Parent Cohort details + cohortId: result.cohortId, cohortName: result.cohortName, cohortType: result.cohortType, - cohortStatus: result.cohortStatus, - cohortMemberStatus: result.cohortMemberStatus + + // Academic Year details + academicYearId: result.academicYearId, + academicYearSession: result.academicYearSession })); } } catch (cohortError) { @@ -2949,7 +2971,7 @@ export class PostgresUserService implements IServicelocator { userData = { userId }; } } - + console.log(userData,"hii"); await this.kafkaService.publishUserEvent(eventType, userData, userId); LoggerUtil.log(`User ${eventType} event published to Kafka for user ${userId}`, apiId); } catch (error) { From 0e10932096de306bd48028bd5289772a6679b964 Mon Sep 17 00:00:00 2001 From: Shubham Date: Mon, 14 Jul 2025 12:26:59 +0530 Subject: [PATCH 26/53] Publish Cohort Events Fixed --- src/adapters/postgres/cohort-adapter.ts | 175 +++++++++++++++++------- 1 file changed, 126 insertions(+), 49 deletions(-) diff --git a/src/adapters/postgres/cohort-adapter.ts b/src/adapters/postgres/cohort-adapter.ts index b8336c01..2f7b2860 100644 --- a/src/adapters/postgres/cohort-adapter.ts +++ b/src/adapters/postgres/cohort-adapter.ts @@ -419,18 +419,6 @@ export class PostgresCohortService { cohortCreateDto.updatedBy ); - // Publish cohort created event to Kafka - try { - await this.kafkaService.publishCohortEvent('created', response, response.cohortId); - } catch (kafkaError) { - LoggerUtil.error( - 'Failed to publish cohort created event to Kafka', - `Error: ${kafkaError.message}`, - apiId - ); - // Don't fail the request if Kafka publishing fails - } - const resBody = new ReturnResponseBody({ ...response, academicYearId: academicYearId, @@ -439,13 +427,24 @@ export class PostgresCohortService { API_RESPONSES.CREATE_COHORT, ) - return APIResponse.success( + // Send response to the client + const apiResponse = APIResponse.success( res, apiId, resBody, HttpStatus.CREATED, API_RESPONSES.CREATE_COHORT ); + + // Publish cohort created event to Kafka asynchronously - after response is sent to client + this.publishCohortEvent('created', response.cohortId, academicYearId, apiId) + .catch(error => LoggerUtil.error( + `Failed to publish cohort created event to Kafka`, + `Error: ${error.message}`, + apiId + )); + + return apiResponse; } catch (error) { LoggerUtil.error( `${API_RESPONSES.SERVER_ERROR}`, @@ -623,32 +622,28 @@ export class PostgresCohortService { } } - // Publish cohort updated event to Kafka - try { - const updatedCohortData = { - ...existingCohorDetails, - ...cohortUpdateDto - }; - await this.kafkaService.publishCohortEvent('updated', updatedCohortData, cohortId); - } catch (kafkaError) { - LoggerUtil.error( - 'Failed to publish cohort updated event to Kafka', - `Error: ${kafkaError.message}`, - apiId - ); - // Don't fail the request if Kafka publishing fails - } - LoggerUtil.log( API_RESPONSES.COHORT_UPDATED_SUCCESSFULLY, ) - return APIResponse.success( + + // Send response to the client + const apiResponse = APIResponse.success( res, apiId, response?.affected, HttpStatus.OK, API_RESPONSES.COHORT_UPDATED_SUCCESSFULLY ); + + // Publish cohort updated event to Kafka asynchronously - after response is sent to client + this.publishCohortEvent('updated', cohortId, null, apiId) + .catch(error => LoggerUtil.error( + `Failed to publish cohort updated event to Kafka`, + `Error: ${error.message}`, + apiId + )); + + return apiResponse; } else { return APIResponse.error( res, @@ -979,31 +974,24 @@ export class PostgresCohortService { await this.cohortMembersRepository.delete({ cohortId: cohortId }); await this.fieldValuesRepository.delete({ itemId: cohortId }); - // Publish cohort deleted event to Kafka - try { - const deletedCohortData = { - cohortId, - status: 'archived', - updatedBy: userId, - deletedAt: new Date().toISOString() - }; - await this.kafkaService.publishCohortEvent('deleted', deletedCohortData, cohortId); - } catch (kafkaError) { - LoggerUtil.error( - 'Failed to publish cohort deleted event to Kafka', - `Error: ${kafkaError.message}`, - apiId - ); - // Don't fail the request if Kafka publishing fails - } - - return APIResponse.success( + // Send response to the client + const apiResponse = APIResponse.success( response, apiId, affectedrows[1], HttpStatus.OK, "Cohort Deleted Successfully." ); + + // Publish cohort deleted event to Kafka asynchronously - after response is sent to client + this.publishCohortEvent('deleted', cohortId, null, apiId) + .catch(error => LoggerUtil.error( + `Failed to publish cohort deleted event to Kafka`, + `Error: ${error.message}`, + apiId + )); + + return apiResponse; } else { return APIResponse.error( response, @@ -1179,4 +1167,93 @@ export class PostgresCohortService { ); } } + + /** + * Publish cohort events to Kafka + * @param eventType Type of event (created, updated, deleted) + * @param cohortId Cohort ID for whom the event is published + * @param apiId API ID for logging + */ + private async publishCohortEvent( + eventType: 'created' | 'updated' | 'deleted', + cohortId: string, + academicYearId: string | null, + apiId: string + ): Promise { + try { + // For delete events, we may want to include just basic information since the cohort might already be removed + let cohortData: any; + + if (eventType === 'deleted') { + cohortData = { + cohortId: cohortId, + deletedAt: new Date().toISOString() + }; + } else { + // For create and update, fetch complete data from DB + try { + // Get basic cohort information + const cohort = await this.cohortRepository.findOne({ + where: { cohortId: cohortId }, + select: [ + "cohortId", + "name", + "type", + "status", + "parentId", + "tenantId", + "createdAt", + "updatedAt", + "createdBy", + "updatedBy" + ] + }); + + if (!cohort) { + LoggerUtil.error(`Failed to fetch cohort data for Kafka event`, `Cohort with ID ${cohortId} not found`); + cohortData = { cohortId }; + } else { + // Get custom fields for the cohort + let customFields = []; + try { + customFields = await this.fieldsService.getCustomFieldDetails(cohortId, 'Cohort'); + } catch (customFieldError) { + LoggerUtil.error( + `Failed to fetch custom fields for Kafka event`, + `Error: ${customFieldError.message}`, + apiId + ); + // Don't fail the entire operation if custom fields fetching fails + customFields = []; + } + + // Build the cohort data object + cohortData = { + ...cohort, + ...(academicYearId && { academicYearId }), + customFields: customFields || [], + eventTimestamp: new Date().toISOString() + }; + } + } catch (error) { + LoggerUtil.error( + `Failed to fetch cohort data for Kafka event`, + `Error: ${error.message}` + ); + // Return at least the cohortId if we can't fetch complete data + cohortData = { cohortId }; + } + } + + await this.kafkaService.publishCohortEvent(eventType, cohortData, cohortId); + LoggerUtil.log(`Cohort ${eventType} event published to Kafka for cohort ${cohortId}`, apiId); + } catch (error) { + LoggerUtil.error( + `Failed to publish cohort ${eventType} event to Kafka`, + `Error: ${error.message}`, + apiId + ); + // Don't throw the error to avoid affecting the main operation + } + } } From 1034ad2718c1d8e44a26022ebd2eff3c438bca40 Mon Sep 17 00:00:00 2001 From: Shubham Date: Mon, 14 Jul 2025 16:52:02 +0530 Subject: [PATCH 27/53] Fixed Cohort Data Query --- src/adapters/postgres/user-adapter.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/adapters/postgres/user-adapter.ts b/src/adapters/postgres/user-adapter.ts index 2d52d80e..516380b5 100644 --- a/src/adapters/postgres/user-adapter.ts +++ b/src/adapters/postgres/user-adapter.ts @@ -2910,13 +2910,12 @@ export class PostgresUserService implements IServicelocator { cay."academicYearId", ay."session" as "academicYearSession" FROM BatchData bd - LEFT JOIN public."Cohort" cohort ON bd."cohortId" = cohort."cohortId" AND cohort."type" = 'COHORT' - LEFT JOIN public."CohortAcademicYear" cay ON bd."cohortId" = cay."cohortId" - LEFT JOIN public."AcademicYears" ay ON cay."academicYearId" = ay."academicYearId" + LEFT JOIN public."Cohort" cohort ON bd."cohortId":: UUID = cohort."cohortId" AND cohort."type" = 'COHORT' + LEFT JOIN public."CohortAcademicYear" cay ON bd."cohortId":: UUID = cay."cohortId" + LEFT JOIN public."AcademicYears" ay ON cay."academicYearId" = ay."id" `; const cohortResults = await this.usersRepository.query(cohortQuery, [userId]); - if (cohortResults && cohortResults.length > 0) { cohorts = cohortResults.map(result => ({ // Batch details @@ -2965,7 +2964,6 @@ export class PostgresUserService implements IServicelocator { userData = { userId }; } } - console.log(userData,"hii"); await this.kafkaService.publishUserEvent(eventType, userData, userId); LoggerUtil.log(`User ${eventType} event published to Kafka for user ${userId}`, apiId); } catch (error) { From 51a3e68304706a2f0fcf5a2d4ba91fbda0c02bff Mon Sep 17 00:00:00 2001 From: souravbhowmik1999 Date: Tue, 15 Jul 2025 15:08:39 +0530 Subject: [PATCH 28/53] update ield option read api defaul limit --- src/adapters/postgres/fields-adapter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/adapters/postgres/fields-adapter.ts b/src/adapters/postgres/fields-adapter.ts index 6f615135..5869d1d6 100644 --- a/src/adapters/postgres/fields-adapter.ts +++ b/src/adapters/postgres/fields-adapter.ts @@ -1194,7 +1194,7 @@ export class PostgresFieldsService implements IServicelocatorfields { } = fieldsOptionsSearchDto; offset = offset || 0; - limit = limit || 200; + limit = limit || 1000; const condition: any = { name: fieldName, From 1a2fd21b4e98c3667e5f3910ed222dfd0ac6e26b Mon Sep 17 00:00:00 2001 From: Shubham4026 <142581532+Shubham4026@users.noreply.github.com> Date: Wed, 16 Jul 2025 16:36:36 +0530 Subject: [PATCH 29/53] Update routeconfig.js --- src/constants/routeconfig.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/constants/routeconfig.js b/src/constants/routeconfig.js index b160e8c2..c9329dc9 100644 --- a/src/constants/routeconfig.js +++ b/src/constants/routeconfig.js @@ -390,6 +390,19 @@ } ] }, + { + "sourceRoute": "/interface/v1/academicyears/create", + "type": "POST", + "priority": "MUST_HAVE", + "inSequence": false, + "orchestrated": false, + "targetPackages": [ + { + "basePackageName": "user", + "packageName": "shiksha-user" + } + ] + }, { "sourceRoute": "/interface/v1/cohortmember/bulkCreate", "type": "POST", From 0e39a847326f45d49cac00bb92add62826683605 Mon Sep 17 00:00:00 2001 From: Shubham Date: Thu, 17 Jul 2025 22:11:19 +0530 Subject: [PATCH 30/53] Structure Change for custom fields --- src/adapters/postgres/user-adapter.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/adapters/postgres/user-adapter.ts b/src/adapters/postgres/user-adapter.ts index 516380b5..cfa80f6c 100644 --- a/src/adapters/postgres/user-adapter.ts +++ b/src/adapters/postgres/user-adapter.ts @@ -2882,7 +2882,17 @@ export class PostgresUserService implements IServicelocator { const tenantRoleData = await this.userTenantRoleData(userId); // Get custom fields if any - const customFields = await this.fieldsService.getCustomFieldDetails(userId, 'Users'); + const customFieldsRows = await this.usersRepository.query(` + SELECT fv."fieldId", f.name, f.type, fv.value + FROM public."FieldValues" fv + JOIN public."Fields" f ON fv."fieldId" = f."fieldId" + WHERE fv."itemId" = $1 + `, [userId]); + const customFields = customFieldsRows.reduce((acc, row) => { + acc[row.name] = { type: row.type, value: row.value }; + return acc; + }, {}); + // Get cohort information for the user let cohorts = []; From 5f666771f8a17ad62d0de41dab686ea483258faf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 29 Jul 2025 12:52:48 +0000 Subject: [PATCH 31/53] Initial plan From b69e6f6f071074efe4de4dfd08cd9a30def99efb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 29 Jul 2025 12:59:02 +0000 Subject: [PATCH 32/53] Initial exploration and plan for /health endpoint implementation Co-authored-by: coolbung <429363+coolbung@users.noreply.github.com> --- package-lock.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/package-lock.json b/package-lock.json index 043af0dc..f05f2b26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,7 @@ "graphql-tag": "^2.12.6", "in-memory-faceted-search": "^1.0.1", "jwt-decode": "^3.1.2", + "kafkajs": "^2.2.4", "moment": "^2.30.1", "multer": "^1.4.4", "node-cron": "^3.0.1", @@ -8916,6 +8917,15 @@ "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" }, + "node_modules/kafkajs": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/kafkajs/-/kafkajs-2.2.4.tgz", + "integrity": "sha512-j/YeapB1vfPT2iOIUn/vxdyKEuhuY2PxMBvf5JWux6iSaukAccrMtXEY/Lb7OvavDhOWME589bpLrEdnVHjfjA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", From 3d62857ce9000e392f081360f5c8ac04f804d1fa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 29 Jul 2025 13:06:11 +0000 Subject: [PATCH 33/53] Implement /health endpoint with PostgreSQL connectivity check Co-authored-by: coolbung <429363+coolbung@users.noreply.github.com> --- package-lock.json | 40 ++++++++++++++++++--- package.json | 6 ++-- src/app.module.ts | 3 +- src/health.controller.spec.ts | 66 +++++++++++++++++++++++++++++++++++ src/health.controller.ts | 43 +++++++++++++++++++++++ 5 files changed, 150 insertions(+), 8 deletions(-) create mode 100644 src/health.controller.spec.ts create mode 100644 src/health.controller.ts diff --git a/package-lock.json b/package-lock.json index f05f2b26..3ce81ff8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,6 +51,7 @@ "swagger-ui-express": "^4.3.0", "templates.js": "^0.3.11", "typeorm": "^0.3.20", + "uuid": "^11.1.0", "winston": "^3.11.0" }, "devDependencies": { @@ -2546,6 +2547,15 @@ "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", "license": "0BSD" }, + "node_modules/@nestjs/common/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@nestjs/config": { "version": "2.3.4", "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-2.3.4.tgz", @@ -2629,6 +2639,15 @@ "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", "license": "0BSD" }, + "node_modules/@nestjs/core/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@nestjs/jwt": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-8.0.1.tgz", @@ -2781,6 +2800,15 @@ "reflect-metadata": "^0.1.12" } }, + "node_modules/@nestjs/schedule/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@nestjs/schematics": { "version": "8.0.8", "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-8.0.8.tgz", @@ -12047,12 +12075,16 @@ } }, "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist/esm/bin/uuid" } }, "node_modules/v8-compile-cache": { diff --git a/package.json b/package.json index 3a49ac9e..d8501ee6 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "graphql-tag": "^2.12.6", "in-memory-faceted-search": "^1.0.1", "jwt-decode": "^3.1.2", + "kafkajs": "^2.2.4", "moment": "^2.30.1", "multer": "^1.4.4", "node-cron": "^3.0.1", @@ -62,9 +63,8 @@ "swagger-ui-express": "^4.3.0", "templates.js": "^0.3.11", "typeorm": "^0.3.20", - "winston": "^3.11.0", - "kafkajs": "^2.2.4" - + "uuid": "^11.1.0", + "winston": "^3.11.0" }, "devDependencies": { "@nestjs/cli": "^8.0.0", diff --git a/src/app.module.ts b/src/app.module.ts index 95d8047e..ec488b49 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -29,6 +29,7 @@ import { RolePermissionModule } from "./permissionRbac/rolePermissionMapping/rol import { LocationModule } from "./location/location.module"; import { KafkaModule } from "./kafka/kafka.module"; import kafkaConfig from "./kafka/kafka.config"; +import { HealthController } from "./health.controller"; @Module({ imports: [ RbacModule, @@ -56,7 +57,7 @@ import kafkaConfig from "./kafka/kafka.config"; LocationModule, KafkaModule, ], - controllers: [AppController], + controllers: [AppController, HealthController], providers: [AppService, HttpService], }) export class AppModule { diff --git a/src/health.controller.spec.ts b/src/health.controller.spec.ts new file mode 100644 index 00000000..0188bd6c --- /dev/null +++ b/src/health.controller.spec.ts @@ -0,0 +1,66 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { HealthController } from './health.controller'; +import { DataSource } from 'typeorm'; + +describe('HealthController', () => { + let controller: HealthController; + let dataSource: DataSource; + + beforeEach(async () => { + const mockDataSource = { + query: jest.fn(), + }; + + const module: TestingModule = await Test.createTestingModule({ + controllers: [HealthController], + providers: [ + { + provide: DataSource, + useValue: mockDataSource, + }, + ], + }).compile(); + + controller = module.get(HealthController); + dataSource = module.get(DataSource); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + describe('getHealth', () => { + it('should return healthy status when database is accessible', async () => { + // Mock successful database query + jest.spyOn(dataSource, 'query').mockResolvedValue([{ '?column?': 1 }]); + + const result = await controller.getHealth(); + + expect(result.id).toBe('api.content.health'); + expect(result.ver).toBe('3.0'); + expect(result.responseCode).toBe('OK'); + expect(result.params.status).toBe('successful'); + expect(result.result.healthy).toBe(true); + expect(result.result.checks).toEqual([ + { name: 'postgres db', healthy: true } + ]); + expect(result.params.resmsgid).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i); + }); + + it('should return unhealthy status when database is not accessible', async () => { + // Mock database query failure + jest.spyOn(dataSource, 'query').mockRejectedValue(new Error('Connection failed')); + + const result = await controller.getHealth(); + + expect(result.id).toBe('api.content.health'); + expect(result.ver).toBe('3.0'); + expect(result.responseCode).toBe('OK'); + expect(result.params.status).toBe('successful'); + expect(result.result.healthy).toBe(false); + expect(result.result.checks).toEqual([ + { name: 'postgres db', healthy: false } + ]); + }); + }); +}); \ No newline at end of file diff --git a/src/health.controller.ts b/src/health.controller.ts new file mode 100644 index 00000000..6f2fee1d --- /dev/null +++ b/src/health.controller.ts @@ -0,0 +1,43 @@ +import { Controller, Get } from '@nestjs/common'; +import { DataSource } from 'typeorm'; +import { v4 as uuidv4 } from 'uuid'; + +@Controller() +export class HealthController { + constructor(private readonly dataSource: DataSource) {} + + @Get('health') + async getHealth() { + let dbHealthy = false; + + try { + // Check database connectivity with a simple query + await this.dataSource.query('SELECT 1'); + dbHealthy = true; + } catch (error) { + dbHealthy = false; + } + + const timestamp = new Date().toISOString().replace(/\.\d{3}Z$/, 'ZZ'); + + return { + id: 'api.content.health', + ver: '3.0', + ts: timestamp, + params: { + resmsgid: uuidv4(), + msgid: null, + err: null, + status: 'successful', + errmsg: null, + }, + responseCode: 'OK', + result: { + checks: [ + { name: 'postgres db', healthy: dbHealthy } + ], + healthy: dbHealthy, + }, + }; + } +} \ No newline at end of file From 4133894be18569a54e35a312d73c71d6f22b9bf5 Mon Sep 17 00:00:00 2001 From: souravbhowmik1999 Date: Wed, 30 Jul 2025 18:26:09 +0530 Subject: [PATCH 34/53] set file format --- src/common/services/upload-file.ts | 24 ++++++++++++++++++++++-- src/common/utils/response.messages.ts | 2 +- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/common/services/upload-file.ts b/src/common/services/upload-file.ts index 834ae6bc..d24d8866 100644 --- a/src/common/services/upload-file.ts +++ b/src/common/services/upload-file.ts @@ -23,11 +23,31 @@ export class FilesUploadService { } async saveFile(file: Express.Multer.File): Promise<{ filePath: string; fileSize: number }> { - const allowedExtensions: string[] = ['.jpg', '.jpeg', '.png', '.gif', '.ico', '.webp']; + const allowedExtensions: string[] = [ + ".jpg", + ".jpeg", + ".png", + ".gif", + ".ico", + ".webp", + ".mp4", + ".mp3", + ".pdf", + ".doc" + ]; const fileExtension = extname(file.originalname).toLowerCase(); if (!allowedExtensions.includes(fileExtension)) { - throw new BadRequestException(`Invalid file type: '${fileExtension}'. Allowed file types are: '.jpg', '.jpeg', '.png', '.gif', '.ico', '.webp'.` + throw new BadRequestException(`Invalid file type: '${fileExtension}'. Allowed file types are: ".jpg", + ".jpeg", + ".png", + ".gif", + ".ico", + ".webp", + ".mp4", + ".mp3", + ".pdf", + ".doc"` ); } diff --git a/src/common/utils/response.messages.ts b/src/common/utils/response.messages.ts index 3d3da252..ec8d5108 100644 --- a/src/common/utils/response.messages.ts +++ b/src/common/utils/response.messages.ts @@ -213,7 +213,7 @@ export const API_RESPONSES = { EMAIL_ERROR: "Email notification failed", SIGNED_URL_SUCCESS: "Signed URL generated successfully", SIGNED_URL_FAILED: "Error while generating signed URL", - INVALID_FILE_TYPE: "Invalid file type. Allowed file types are: '.jpg','.jpeg','.png','.webp','.pdf','.doc','.docx','.mp4','.mov','.txt','.csv'", + INVALID_FILE_TYPE: "Invalid file type. Allowed file types are: '.jpg','.jpeg','.png','.webp','.pdf','.doc','.docx','.mp4','.mov','.txt','.csv','.mp3'", FILE_SIZE_ERROR: "File too large. Maximum allowed file size is 10MB." }; From ec6442f709df303727029ca7d373d033233928c6 Mon Sep 17 00:00:00 2001 From: souravbhowmik1999 Date: Thu, 7 Aug 2025 17:22:55 +0530 Subject: [PATCH 35/53] set file size and format --- package-lock.json | 42 ++++----- package.json | 7 +- src/common/services/upload-S3.service.ts | 113 +++++++++++++++-------- src/fields/fields.module.ts | 2 +- 4 files changed, 98 insertions(+), 66 deletions(-) diff --git a/package-lock.json b/package-lock.json index 043af0dc..1fb2f1a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,6 @@ "@types/multer": "^1.4.12", "aws-sdk": "^2.1692.0", "axios": "^0.26.1", - "cache-manager": "^3.6.1", "class-transformer": "^0.5.1", "class-validator": "^0.13.2", "csv": "^6.3.10", @@ -36,6 +35,7 @@ "graphql-tag": "^2.12.6", "in-memory-faceted-search": "^1.0.1", "jwt-decode": "^3.1.2", + "kafkajs": "^2.2.4", "moment": "^2.30.1", "multer": "^1.4.4", "node-cron": "^3.0.1", @@ -56,7 +56,6 @@ "@nestjs/cli": "^8.0.0", "@nestjs/schematics": "^8.0.0", "@nestjs/testing": "^8.0.0", - "@types/cache-manager": "^3.4.3", "@types/cron": "^1.7.3", "@types/express": "^4.17.13", "@types/jest": "27.4.1", @@ -5276,12 +5275,13 @@ } }, "node_modules/cache-manager": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-3.6.1.tgz", - "integrity": "sha512-jxJvGYhN5dUgpriAdsDnnYbKse4dEXI5i3XpwTfPq5utPtXH1uYXWyGLHGlbSlh9Vq4ytrgAUVwY+IodNeKigA==", + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-3.6.3.tgz", + "integrity": "sha512-dS4DnV6c6cQcVH5OxzIU1XZaACXwvVIiUPkFytnRmLOACuBGv3GQgRQ1RJGRRw4/9DF14ZK2RFlZu1TUgDniMg==", + "license": "MIT", "dependencies": { "async": "3.2.3", - "lodash": "^4.17.21", + "lodash.clonedeep": "^4.5.0", "lru-cache": "6.0.0" } }, @@ -7174,21 +7174,6 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -8916,6 +8901,15 @@ "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" }, + "node_modules/kafkajs": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/kafkajs/-/kafkajs-2.2.4.tgz", + "integrity": "sha512-j/YeapB1vfPT2iOIUn/vxdyKEuhuY2PxMBvf5JWux6iSaukAccrMtXEY/Lb7OvavDhOWME589bpLrEdnVHjfjA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -9011,6 +9005,12 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", diff --git a/package.json b/package.json index 3a49ac9e..2a0b9411 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,6 @@ "@types/multer": "^1.4.12", "aws-sdk": "^2.1692.0", "axios": "^0.26.1", - "cache-manager": "^3.6.1", "class-transformer": "^0.5.1", "class-validator": "^0.13.2", "csv": "^6.3.10", @@ -48,6 +47,7 @@ "graphql-tag": "^2.12.6", "in-memory-faceted-search": "^1.0.1", "jwt-decode": "^3.1.2", + "kafkajs": "^2.2.4", "moment": "^2.30.1", "multer": "^1.4.4", "node-cron": "^3.0.1", @@ -62,15 +62,12 @@ "swagger-ui-express": "^4.3.0", "templates.js": "^0.3.11", "typeorm": "^0.3.20", - "winston": "^3.11.0", - "kafkajs": "^2.2.4" - + "winston": "^3.11.0" }, "devDependencies": { "@nestjs/cli": "^8.0.0", "@nestjs/schematics": "^8.0.0", "@nestjs/testing": "^8.0.0", - "@types/cache-manager": "^3.4.3", "@types/cron": "^1.7.3", "@types/express": "^4.17.13", "@types/jest": "27.4.1", diff --git a/src/common/services/upload-S3.service.ts b/src/common/services/upload-S3.service.ts index b1841e83..e040d37a 100644 --- a/src/common/services/upload-S3.service.ts +++ b/src/common/services/upload-S3.service.ts @@ -65,61 +65,96 @@ export class UploadS3Service { async getPresignedUrl(filename: string, fileType: string, response, foldername?: string): Promise { try { - const allowedFileTypes = [ - '.jpg', '.jpeg', '.png', '.webp', // Images - '.pdf', '.doc', '.docx', // Documents - '.mp4', '.mov', // Videos - '.txt', '.csv' // Text files - ]; - - const mimeTypeMap = { - '.jpg': 'image/jpeg', - '.jpeg': 'image/jpeg', - '.png': 'image/png', - '.webp': 'image/webp', - '.pdf': 'application/pdf', - '.doc': 'application/msword', - '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - '.mp4': 'video/mp4', - '.mov': 'video/quicktime', - '.txt': 'text/plain', - '.csv': 'text/csv', + // Dynamic MIME type detection based on file extension + const getMimeType = (extension: string): string => { + const mimeTypes: { [key: string]: string } = { + // Images + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.png': 'image/png', + '.gif': 'image/gif', + '.bmp': 'image/bmp', + '.webp': 'image/webp', + '.svg': 'image/svg+xml', + '.tiff': 'image/tiff', + '.ico': 'image/x-icon', + + // Documents + '.pdf': 'application/pdf', + '.doc': 'application/msword', + '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + '.xls': 'application/vnd.ms-excel', + '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + '.ppt': 'application/vnd.ms-powerpoint', + '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + '.rtf': 'application/rtf', + + // Text files + '.txt': 'text/plain', + '.csv': 'text/csv', + '.xml': 'text/xml', + '.html': 'text/html', + '.css': 'text/css', + '.js': 'text/javascript', + '.json': 'application/json', + + // Videos + '.mp4': 'video/mp4', + '.avi': 'video/x-msvideo', + '.mov': 'video/quicktime', + '.wmv': 'video/x-ms-wmv', + '.flv': 'video/x-flv', + '.webm': 'video/webm', + '.mkv': 'video/x-matroska', + + // Audio + '.mp3': 'audio/mpeg', + '.wav': 'audio/wav', + '.ogg': 'audio/ogg', + '.m4a': 'audio/mp4', + '.flac': 'audio/flac', + + // Archives + '.zip': 'application/zip', + '.rar': 'application/vnd.rar', + '.7z': 'application/x-7z-compressed', + '.tar': 'application/x-tar', + '.gz': 'application/gzip', + + // Other common formats + '.apk': 'application/vnd.android.package-archive', + '.exe': 'application/octet-stream', + '.dmg': 'application/octet-stream', + '.iso': 'application/octet-stream', + }; + + return mimeTypes[extension.toLowerCase()] || 'application/octet-stream'; }; - // Validate file extension - if (!allowedFileTypes.includes(fileType)) { - return APIResponse.error( - response, - APIID.SIGNED_URL, - API_RESPONSES.BAD_REQUEST, - API_RESPONSES.INVALID_FILE_TYPE, - HttpStatus.BAD_REQUEST, - ); - } - - const contentType = mimeTypeMap[fileType]; + // Get MIME type dynamically + const contentType = getMimeType(fileType); // Construct unique file key - // const newKey = `${filename}-${uuidv4()}${fileType}`; const extension = fileType; const folderPath = foldername ? `${foldername}/` : ''; const newKey = `${folderPath}${filename}-${uuidv4()}${extension}`; - + // Create presigned POST with minimal restrictions const result = await createPresignedPost(this.s3Client, { Bucket: this.bucketName, Key: newKey, Conditions: [ - ['starts-with', '$Content-Type', 'image/'], - ["eq", "$Content-Type", contentType], // ✅ this enforces exact match - ["eq", "$key", newKey], // ✅ makes sure they don't change key - ["content-length-range", 0, 5 * 1024 * 1024], // max 5MB - ]as any[], + // Only enforce the key to prevent tampering + ["eq", "$key", newKey], + // Allow any content type + ["starts-with", "$Content-Type", ""], + // No file size limit (remove content-length-range) + ] as any[], Fields: { key: newKey, "Content-Type": contentType }, - Expires: 300 // 5 minutes + Expires: 24 * 60 * 60 // 24 hours instead of 5 minutes }); return APIResponse.success( diff --git a/src/fields/fields.module.ts b/src/fields/fields.module.ts index fab68345..06e7c45d 100644 --- a/src/fields/fields.module.ts +++ b/src/fields/fields.module.ts @@ -1,4 +1,4 @@ -import { CacheModule, Module } from "@nestjs/common"; +import { Module } from "@nestjs/common"; import { FieldsController } from "./fields.controller"; import { HttpModule } from "@nestjs/axios"; import { FieldsAdapter } from "./fieldsadapter"; From d54eb9e580653996802d7fbff19e031eacde3292 Mon Sep 17 00:00:00 2001 From: Shubham Date: Wed, 13 Aug 2025 12:45:55 +0530 Subject: [PATCH 36/53] Active Check on Geolocation Data --- src/adapters/postgres/fields-adapter.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/adapters/postgres/fields-adapter.ts b/src/adapters/postgres/fields-adapter.ts index 5869d1d6..5d975391 100644 --- a/src/adapters/postgres/fields-adapter.ts +++ b/src/adapters/postgres/fields-adapter.ts @@ -1466,19 +1466,21 @@ export class PostgresFieldsService implements IServicelocatorfields { const orderCond = order || ""; const offsetCond = offset ? `offset ${offset}` : ""; const limitCond = limit ? `limit ${limit}` : ""; - let whereCond = `WHERE `; - whereCond = whereClause ? (whereCond += `${whereClause}`) : ""; + const conditions = []; + + if (whereClause) { + conditions.push(`${whereClause}`); + } + + // Apply default filter to fetch only active records + conditions.push(`is_active=1`); if (optionSelected) { - if (whereCond) { - whereCond += `AND "${tableName}_name" ILike '%${optionSelected}%'`; - } else { - whereCond += `WHERE "${tableName}_name" ILike '%${optionSelected}%'`; - } - } else { - whereCond += ""; + conditions.push(`"${tableName}_name" ILike '%${optionSelected}%'`); } + const whereCond = conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""; + const query = `SELECT *,COUNT(*) OVER() AS total_count FROM public."${tableName}" ${whereCond} ${orderCond} ${offsetCond} ${limitCond}`; const result = await this.fieldsRepository.query(query); From 29cc6f441502e1f0a25a04c443f5985e9d9776f4 Mon Sep 17 00:00:00 2001 From: souravbhowmik1999 Date: Wed, 13 Aug 2025 15:12:23 +0530 Subject: [PATCH 37/53] set tenant type --- src/adapters/postgres/user-adapter.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/adapters/postgres/user-adapter.ts b/src/adapters/postgres/user-adapter.ts index cfa80f6c..432ae6f0 100644 --- a/src/adapters/postgres/user-adapter.ts +++ b/src/adapters/postgres/user-adapter.ts @@ -791,6 +791,7 @@ export class PostgresUserService implements IServicelocator { T."channelId", T.name AS tenantName, T.params, + T."type", UTM."Id" AS userTenantMappingId FROM public."UserTenantMapping" UTM @@ -832,6 +833,7 @@ export class PostgresUserService implements IServicelocator { params: data.params, roleId: roleId, roleName: roleName, + tenantType: data.type, // privileges: privileges, }); } From 0524ba79e2a8ffe8016137fcabf1fd0d68dcb516 Mon Sep 17 00:00:00 2001 From: Shubham Date: Mon, 18 Aug 2025 23:56:21 +0530 Subject: [PATCH 38/53] Kakfa Fix for Centralized Report --- src/adapters/postgres/user-adapter.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/adapters/postgres/user-adapter.ts b/src/adapters/postgres/user-adapter.ts index cfa80f6c..1742f850 100644 --- a/src/adapters/postgres/user-adapter.ts +++ b/src/adapters/postgres/user-adapter.ts @@ -2882,16 +2882,7 @@ export class PostgresUserService implements IServicelocator { const tenantRoleData = await this.userTenantRoleData(userId); // Get custom fields if any - const customFieldsRows = await this.usersRepository.query(` - SELECT fv."fieldId", f.name, f.type, fv.value - FROM public."FieldValues" fv - JOIN public."Fields" f ON fv."fieldId" = f."fieldId" - WHERE fv."itemId" = $1 - `, [userId]); - const customFields = customFieldsRows.reduce((acc, row) => { - acc[row.name] = { type: row.type, value: row.value }; - return acc; - }, {}); + const customFields = await this.fieldsService.getCustomFieldDetails(userId, 'Users'); // Get cohort information for the user From 43581567bca15828bfcb68560a2fdd87fd25522f Mon Sep 17 00:00:00 2001 From: Shubham Date: Fri, 22 Aug 2025 14:06:15 +0530 Subject: [PATCH 39/53] Added default Tenant Id in header for Searh user --- src/adapters/postgres/user-adapter.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/adapters/postgres/user-adapter.ts b/src/adapters/postgres/user-adapter.ts index cfa80f6c..b3846b54 100644 --- a/src/adapters/postgres/user-adapter.ts +++ b/src/adapters/postgres/user-adapter.ts @@ -358,7 +358,7 @@ export class PostgresUserService implements IServicelocator { ) { const apiId = APIID.USER_LIST; try { - const findData = await this.findAllUserDetails(userSearchDto); + const findData = await this.findAllUserDetails(userSearchDto, tenantId); if (findData === false) { LoggerUtil.error( @@ -401,7 +401,7 @@ export class PostgresUserService implements IServicelocator { } - async findAllUserDetails(userSearchDto) { + async findAllUserDetails(userSearchDto, tenantId?: string) { let { limit, offset, filters, exclude, sort } = userSearchDto; let excludeCohortIdes; let excludeUserIdes; @@ -554,6 +554,18 @@ export class PostgresUserService implements IServicelocator { whereCondition = ""; } + // Apply tenant filtering conditionally if tenantId is provided from headers + 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); + } else { + LoggerUtil.warn(`No tenantId provided - returning users from all tenants`, APIID.USER_LIST); + } + //Get user core fields data const query = `SELECT U."userId",U."enrollmentId", U."username",U."email", U."firstName",UTM."tenantId", U."middleName", U."lastName", U."gender", U."dob", R."name" AS role, U."mobile", U."createdBy",U."updatedBy", U."createdAt", U."updatedAt", U."status", COUNT(*) OVER() AS total_count FROM public."Users" U From a30cb265480efe16d34b1cd4d8578d36cb531cee Mon Sep 17 00:00:00 2001 From: Shubham Date: Fri, 22 Aug 2025 15:42:10 +0530 Subject: [PATCH 40/53] Optimzation of Field Value Queries --- src/adapters/postgres/cohort-adapter.ts | 2 +- src/adapters/postgres/fields-adapter.ts | 65 +++++++++++++++++++++++++ src/adapters/postgres/user-adapter.ts | 2 +- 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/src/adapters/postgres/cohort-adapter.ts b/src/adapters/postgres/cohort-adapter.ts index 2f7b2860..3a158da1 100644 --- a/src/adapters/postgres/cohort-adapter.ts +++ b/src/adapters/postgres/cohort-adapter.ts @@ -858,7 +858,7 @@ export class PostgresCohortService { if (Object.keys(searchCustomFields).length > 0) { const context = "COHORT"; getCohortIdUsingCustomFields = - await this.fieldsService.filterUserUsingCustomFields( + await this.fieldsService.filterUserUsingCustomFieldsOptimized( context, searchCustomFields ); diff --git a/src/adapters/postgres/fields-adapter.ts b/src/adapters/postgres/fields-adapter.ts index 5d975391..2b0bf050 100644 --- a/src/adapters/postgres/fields-adapter.ts +++ b/src/adapters/postgres/fields-adapter.ts @@ -1538,6 +1538,71 @@ export class PostgresFieldsService implements IServicelocatorfields { return result; } + // OPTIMIZED VERSION - Much faster alternative to avoid JSON aggregation + async filterUserUsingCustomFieldsOptimized(context: string, stateDistBlockData: any) { + let joinCond = ""; + let targetTable = ""; + + if (context === "COHORT") { + joinCond = `JOIN "Cohort" u ON fv."itemId" = u."cohortId"`; + targetTable = "Cohort"; + } else if (context === "USERS") { + joinCond = `JOIN "Users" u ON fv."itemId" = u."userId"`; + targetTable = "Users"; + } else { + // Generic case - no specific table join + targetTable = "FieldValues"; + } + + // Build EXISTS conditions for each field filter + const conditions = []; + let paramIndex = 1; + const queryParams = []; + + for (const [fieldName, fieldValues] of Object.entries(stateDistBlockData)) { + const values = Array.isArray(fieldValues) ? fieldValues : [fieldValues]; + + // Create placeholders for parameterized query + const valuePlaceholders = values.map(() => `$${paramIndex++}`); + queryParams.push(...values); + + const condition = ` + EXISTS ( + SELECT 1 + FROM "FieldValues" fv_inner + JOIN "Fields" f_inner ON fv_inner."fieldId" = f_inner."fieldId" + WHERE fv_inner."itemId" = ${context === 'COHORT' ? 'c."cohortId"' : 'u."userId"'} + AND f_inner."name" = $${paramIndex} + AND (f_inner.context IN($${paramIndex + 1}, 'NULL', 'null', '') OR f_inner.context IS NULL) + AND fv_inner."value" && ARRAY[${valuePlaceholders.join(',')}] + )`; + + queryParams.push(fieldName, context); + paramIndex += 2; + conditions.push(condition); + } + + let query; + if (context === "COHORT") { + query = ` + SELECT DISTINCT c."cohortId" as "itemId" + FROM "Cohort" c + WHERE ${conditions.join(' AND ')}`; + } else if (context === "USERS") { + query = ` + SELECT DISTINCT u."userId" as "itemId" + FROM "Users" u + WHERE ${conditions.join(' AND ')}`; + } else { + // Fallback to original logic for unknown context + return this.filterUserUsingCustomFields(context, stateDistBlockData); + } + + const queryData = await this.fieldsValuesRepository.query(query, queryParams); + const result = queryData.length > 0 ? queryData.map((item) => item.itemId) : null; + return result; + } + async filterUserUsingCustomFields(context: string, stateDistBlockData: any) { const searchKey = []; let whereCondition = ` WHERE `; diff --git a/src/adapters/postgres/user-adapter.ts b/src/adapters/postgres/user-adapter.ts index 2e1676e5..19ea3170 100644 --- a/src/adapters/postgres/user-adapter.ts +++ b/src/adapters/postgres/user-adapter.ts @@ -513,7 +513,7 @@ export class PostgresUserService implements IServicelocator { const context = "USERS"; getUserIdUsingCustomFields = - await this.fieldsService.filterUserUsingCustomFields( + await this.fieldsService.filterUserUsingCustomFieldsOptimized( context, searchCustomFields ); From 0b5fab9c81b43e89cdf6377de9b647d0effc4095 Mon Sep 17 00:00:00 2001 From: Ishan-ttpl Date: Tue, 26 Aug 2025 10:36:40 +0530 Subject: [PATCH 41/53] Create build.yaml --- .github/workflows/build.yaml | 53 ++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 .github/workflows/build.yaml diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 00000000..ee5ee9f2 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,53 @@ +name: Tag-based Build Image + +on: + push: + tags: + - "v*" + workflow_dispatch: + inputs: + environment: + description: "Target environment (qa or prod)" + required: true + default: "qa" + tag: + description: "Image tag to deploy" + required: true + +jobs: + build: + name: Build and Push Docker Image to AWS ECR + runs-on: ubuntu-latest + environment: ${{ github.event.inputs.environment || 'qa' }} + + steps: + - name: Check out code + uses: actions/checkout@v2 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.AWS_REGION }} + + - name: Extract ECR Registry from Repository + run: | + FULL_REPO="${{ secrets.ECR_REPOSITORY }}" + REGISTRY="$(echo $FULL_REPO | cut -d'/' -f1)" + echo "ECR_REGISTRY=$REGISTRY" >> $GITHUB_ENV + echo "ECR_REPOSITORY=$FULL_REPO" >> $GITHUB_ENV + + - name: Log in to Amazon ECR + run: | + aws ecr get-login-password --region ${{ secrets.AWS_REGION }} \ + | docker login --username AWS --password-stdin ${{ env.ECR_REGISTRY }} + + - name: Build, Tag, and Push Docker Image + run: | + TAG="${{ github.event.inputs.tag || github.ref_name }}" # Use input tag or pushed tag + IMAGE_URI="${{ env.ECR_REPOSITORY }}:$TAG" + echo "Building image: $IMAGE_URI" + docker build -t $IMAGE_URI . + docker push $IMAGE_URI + docker rmi $IMAGE_URI From 760b55cf52185842765e470138fa4492e9ab3b55 Mon Sep 17 00:00:00 2001 From: Ishan-ttpl Date: Tue, 26 Aug 2025 10:40:45 +0530 Subject: [PATCH 42/53] Create qa-prod-deployment.yaml --- .github/workflows/qa-prod-deployment.yaml | 74 +++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 .github/workflows/qa-prod-deployment.yaml diff --git a/.github/workflows/qa-prod-deployment.yaml b/.github/workflows/qa-prod-deployment.yaml new file mode 100644 index 00000000..712cc6d7 --- /dev/null +++ b/.github/workflows/qa-prod-deployment.yaml @@ -0,0 +1,74 @@ +name: BACKEND-TAG-BASED-DEPLOYMENT + +on: + workflow_dispatch: + inputs: + environment: + description: "Target environment (qa or prod)" + required: true + default: "qa" + tag: + description: "Image tag to deploy" + required: true + +jobs: + deploy: + name: Deploy backend Service + runs-on: ubuntu-latest + environment: ${{ github.event.inputs.environment }} + + steps: + # Step 1: Checkout code + - name: Check out repository + uses: actions/checkout@v2 + + # Step 2: Set TAG environment variable + - name: Set TAG environment variable + run: | + TAG="${{ github.event.inputs.tag }}" + echo "TAG=$TAG" >> $GITHUB_ENV + + # Step 3: Debug TAG value + - name: Debug TAG value + run: echo "TAG value:${{ env.TAG }}" + + # Step 4: Configure AWS credentials (from environment-specific secrets) + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.AWS_REGION }} + + # Step 5: Generate ConfigMap YAML safely + - name: Generate ConfigMap YAML + run: | + mkdir -p manifest + cat < manifest/configmap.yaml + ${{ secrets.ENV_FILE_CONTENT }} + EOF + + # Step 6: Update Deployment Manifest + - name: Update Deployment Manifest + env: + IMAGE_TAG: ${{ env.TAG }} + ECR_REPOSITORY: ${{ secrets.ECR_REPOSITORY }} + run: | + mkdir -p manifest + envsubst < manifest/backend-service.yaml > manifest/backend-service-updated.yaml + echo "Updated deployment manifest:" + cat manifest/backend-service-updated.yaml + + # Step 7: Deploy to AWS EKS + - name: Deploy to AWS EKS + env: + EKS_CLUSTER_NAME: ${{ secrets.EKS_CLUSTER_NAME }} + AWS_REGION: ${{ secrets.AWS_REGION }} + NAMESPACE: ${{ github.event.inputs.environment == 'prod' && 'default' || 'microservices-qa' }} + run: | + aws eks update-kubeconfig --name $EKS_CLUSTER_NAME --region $AWS_REGION + kubectl apply -f manifest/backend-service-updated.yaml -n $NAMESPACE + kubectl apply -f manifest/configmap.yaml -n $NAMESPACE + sleep 10 + echo "Pods status:" + kubectl get pods -n $NAMESPACE | grep backend From 556105ef400cc8ed8ba4fd842ab30e21a8dce552 Mon Sep 17 00:00:00 2001 From: Ishan-ttpl Date: Thu, 28 Aug 2025 09:19:22 +0530 Subject: [PATCH 43/53] Update build.yaml --- .github/workflows/build.yaml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index ee5ee9f2..69b5ce5a 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -18,7 +18,10 @@ jobs: build: name: Build and Push Docker Image to AWS ECR runs-on: ubuntu-latest - environment: ${{ github.event.inputs.environment || 'qa' }} + + env: + ENVIRONMENT: ${{ github.event.inputs.environment || 'qa' }} + TAG: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || github.ref_name }} steps: - name: Check out code @@ -33,7 +36,7 @@ jobs: - name: Extract ECR Registry from Repository run: | - FULL_REPO="${{ secrets.ECR_REPOSITORY }}" + FULL_REPO="${{ secrets.ECR_REPOSITORY }}" REGISTRY="$(echo $FULL_REPO | cut -d'/' -f1)" echo "ECR_REGISTRY=$REGISTRY" >> $GITHUB_ENV echo "ECR_REPOSITORY=$FULL_REPO" >> $GITHUB_ENV @@ -45,8 +48,7 @@ jobs: - name: Build, Tag, and Push Docker Image run: | - TAG="${{ github.event.inputs.tag || github.ref_name }}" # Use input tag or pushed tag - IMAGE_URI="${{ env.ECR_REPOSITORY }}:$TAG" + IMAGE_URI="${{ env.ECR_REPOSITORY }}:${{ env.TAG }}" echo "Building image: $IMAGE_URI" docker build -t $IMAGE_URI . docker push $IMAGE_URI From 071f7a51694f9d87c718de59dcded892a2be5ea4 Mon Sep 17 00:00:00 2001 From: Ishan-ttpl Date: Thu, 28 Aug 2025 17:23:24 +0530 Subject: [PATCH 44/53] Update build.yaml --- .github/workflows/build.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 69b5ce5a..9a363c36 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -18,9 +18,10 @@ jobs: build: name: Build and Push Docker Image to AWS ECR runs-on: ubuntu-latest + environment: ${{ github.event.inputs.environment }} env: - ENVIRONMENT: ${{ github.event.inputs.environment || 'qa' }} + ENVIRONMENT: ${{ github.event.inputs.environment }} TAG: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || github.ref_name }} steps: From 0f925f472769885b3f816bd8a1aa8ac31f36789a Mon Sep 17 00:00:00 2001 From: Shubham Date: Fri, 29 Aug 2025 01:36:20 +0530 Subject: [PATCH 45/53] Name Filter added in Search API --- src/adapters/postgres/user-adapter.ts | 11 +++++++---- src/user/entities/user-entity.ts | 3 +++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/adapters/postgres/user-adapter.ts b/src/adapters/postgres/user-adapter.ts index 19ea3170..bc3c1450 100644 --- a/src/adapters/postgres/user-adapter.ts +++ b/src/adapters/postgres/user-adapter.ts @@ -427,7 +427,7 @@ 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']; + 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 @@ -437,6 +437,7 @@ export class PostgresUserService implements IServicelocator { } switch (key) { case "firstName": + case "name": whereCondition += ` U."${key}" ILIKE '%${value}%'`; index++; break; @@ -567,7 +568,7 @@ export class PostgresUserService implements IServicelocator { } //Get user core fields data - const query = `SELECT U."userId",U."enrollmentId", U."username",U."email", U."firstName",UTM."tenantId", U."middleName", U."lastName", U."gender", U."dob", R."name" AS role, U."mobile", U."createdBy",U."updatedBy", U."createdAt", U."updatedAt", U."status", COUNT(*) OVER() AS total_count + const query = `SELECT U."userId",U."enrollmentId", U."username",U."email", U."firstName", U."name",UTM."tenantId", U."middleName", U."lastName", U."gender", U."dob", R."name" AS role, U."mobile", U."createdBy",U."updatedBy", U."createdAt", U."updatedAt", U."status", COUNT(*) OVER() AS total_count FROM public."Users" U LEFT JOIN public."CohortMembers" CM ON CM."userId" = U."userId" @@ -763,6 +764,7 @@ export class PostgresUserService implements IServicelocator { "enrollmentId", "username", "firstName", + "name", "middleName", "lastName", "gender", @@ -2723,7 +2725,7 @@ 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 === 'middleName' || key === 'lastName') { + if (key === 'firstName' || key === 'name' || key === 'middleName' || key === 'lastName') { const sanitizedValue = this.sanitizeInput(value); whereClause[key] = ILike(`%${sanitizedValue}%`); } else { @@ -2735,7 +2737,7 @@ 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', 'middleName', 'lastName','mobile'], // Select only these fields + select: ['username', 'firstName', 'name', 'middleName', 'lastName','mobile'], // Select only these fields }); if (findData.length === 0) { @@ -2876,6 +2878,7 @@ export class PostgresUserService implements IServicelocator { "userId", "username", "firstName", + "name", "middleName", "lastName", "gender", diff --git a/src/user/entities/user-entity.ts b/src/user/entities/user-entity.ts index 25197c40..3655764e 100644 --- a/src/user/entities/user-entity.ts +++ b/src/user/entities/user-entity.ts @@ -31,6 +31,9 @@ export class User { @Column({ type: 'varchar', length: 50, nullable: false }) lastName: string; + @Column({ type: 'varchar', length: 100, nullable: true }) + name: string; + @Column({ type: 'enum', enum: ['male', 'female', 'transgender'], nullable: false }) gender: string; From 6e05dde7b0190062ea8e3dd256ffd6db7ca7a3e4 Mon Sep 17 00:00:00 2001 From: souravbhowmik1999 Date: Wed, 3 Sep 2025 16:01:53 +0530 Subject: [PATCH 46/53] Inactive user in keycloak --- src/adapters/postgres/user-adapter.ts | 105 +++++++++++++++------- src/common/utils/keycloak.adapter.util.ts | 75 ++++++++++++++++ 2 files changed, 147 insertions(+), 33 deletions(-) diff --git a/src/adapters/postgres/user-adapter.ts b/src/adapters/postgres/user-adapter.ts index bc3c1450..c0703698 100644 --- a/src/adapters/postgres/user-adapter.ts +++ b/src/adapters/postgres/user-adapter.ts @@ -10,6 +10,7 @@ import { createUserInKeyCloak, updateUserInKeyCloak, checkIfUsernameExistsInKeycloak, + updateUserEnabledStatusInKeycloak, checkIfEmailExistsInKeycloak, } from "../../common/utils/keycloak.adapter.util"; import { ErrorResponse } from "src/error-response"; @@ -623,7 +624,7 @@ export class PostgresUserService implements IServicelocator { userId: userData.userId, }, }); - + if (checkExistUser.length == 0) { return APIResponse.error( response, @@ -788,7 +789,7 @@ export class PostgresUserService implements IServicelocator { } const tenantData = tenantId ? tenentDetails.filter((item) => item.tenantId === tenantId) - : tenentDetails; + : tenentDetails; userDetails["tenantData"] = tenantData; return userDetails; @@ -817,7 +818,7 @@ export class PostgresUserService implements IServicelocator { UTM."userId" = $1 ORDER BY T."tenantId", UTM."Id";`; - + const result = await this.usersRepository.query(query, [userId]); const combinedResult = []; const roleArray = []; @@ -826,7 +827,7 @@ export class PostgresUserService implements IServicelocator { userId, data.tenantId ); - + if (roleData.length > 0) { roleArray.push(roleData[0].roleid); const roleId = roleData[0].roleid; @@ -944,6 +945,44 @@ export class PostgresUserService implements IServicelocator { userDto?.userId ); + // Synchronize user status with Keycloak + if (userDto.userData?.status) { + const shouldEnable = !['archived', 'inactive'].includes( + userDto.userData.status.toLowerCase() + ); + + try { + const keycloakResponse = await getKeycloakAdminToken(); + const token = keycloakResponse.data.access_token; + + const keycloakStatusUpdateResult = await updateUserEnabledStatusInKeycloak( + { userId: userDto.userId, enabled: shouldEnable }, + token + ); + + if (keycloakStatusUpdateResult.success) { + LoggerUtil.log( + `User status synchronized with Keycloak: ${shouldEnable ? 'enabled' : 'disabled'}`, + apiId, + userDto.userId + ); + } else { + LoggerUtil.error( + `Keycloak user status sync failed`, + `Status: ${keycloakStatusUpdateResult.statusCode}, Message: ${keycloakStatusUpdateResult.message}`, + apiId + ); + } + } catch (error) { + LoggerUtil.error( + `Keycloak user status sync error`, + `Error: ${error.message}`, + apiId + ); + // Continue with the main flow even if Keycloak sync fails + } + } + if (userDto?.customFields?.length > 0) { const getFieldsAttributes = await this.fieldsService.getEditableFieldsAttributes(userDto.userData.tenantId); @@ -1052,7 +1091,7 @@ export class PostgresUserService implements IServicelocator { `Error: ${error.message}`, apiId )); - + return apiResponse; } catch (e) { LoggerUtil.error( @@ -1196,7 +1235,7 @@ export class PostgresUserService implements IServicelocator { const apiId = APIID.USER_CREATE; const startTime = Date.now(); const stepTimings = {}; - + const userContext = { username: userCreateDto?.username, email: userCreateDto?.email, @@ -1300,7 +1339,7 @@ export class PostgresUserService implements IServicelocator { const checkUserinKeyCloakandDb = await this.checkUserinKeyCloakandDb( userCreateDto ); - + if (checkUserinKeyCloakandDb) { LoggerUtil.error( `User ${userContext.username} already exists`, @@ -1475,14 +1514,14 @@ export class PostgresUserService implements IServicelocator { apiId, userContext.username ); - + // 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`, apiId, userContext.username ); - + // Send response to the client APIResponse.success( response, @@ -1491,7 +1530,7 @@ export class PostgresUserService implements IServicelocator { HttpStatus.CREATED, API_RESPONSES.USER_CREATE_SUCCESSFULLY ); - + // Produce user created event to Kafka asynchronously - after response is sent to client this.publishUserEvent('created', result.userId, apiId) .catch(error => LoggerUtil.error( @@ -2083,7 +2122,7 @@ export class PostgresUserService implements IServicelocator { } const fieldAttributes = getFieldDetails?.fieldAttributes || {}; // getFieldDetails["fieldAttributes"] = fieldAttributes[tenantId] || fieldAttributes["default"]; - getFieldDetails["fieldAttributes"] = fieldAttributes; + getFieldDetails["fieldAttributes"] = fieldAttributes; if ( (getFieldDetails.type == "checkbox" || @@ -2377,7 +2416,7 @@ export class PostgresUserService implements IServicelocator { const apiId = APIID.VERIFY_OTP; try { const { mobile, otp, hash, reason, username } = body; - + // Validate required fields for all requests if (!otp || !hash || !reason) { return APIResponse.error( @@ -2388,7 +2427,7 @@ export class PostgresUserService implements IServicelocator { HttpStatus.BAD_REQUEST ); } - + // Validate hash format const [hashValue, expires] = hash.split('.'); if (!hashValue || !expires || isNaN(parseInt(expires))) { @@ -2400,7 +2439,7 @@ export class PostgresUserService implements IServicelocator { HttpStatus.BAD_REQUEST ); } - + // Check for OTP expiration if (Date.now() > parseInt(expires)) { return APIResponse.error( @@ -2411,10 +2450,10 @@ export class PostgresUserService implements IServicelocator { HttpStatus.BAD_REQUEST ); } - + let identifier: string; let resetToken: string | null = null; - + // Process based on reason if (reason === 'signup') { if (!mobile) { @@ -2427,7 +2466,7 @@ export class PostgresUserService implements IServicelocator { ); } identifier = this.formatMobileNumber(mobile); - } + } else if (reason === 'forgot') { if (!username) { return APIResponse.error( @@ -2438,10 +2477,10 @@ export class PostgresUserService implements IServicelocator { HttpStatus.BAD_REQUEST ); } - + identifier = this.formatMobileNumber(mobile); const userData = await this.findUserDetails(null, username); - + if (!userData) { return APIResponse.error( response, @@ -2451,19 +2490,19 @@ export class PostgresUserService implements IServicelocator { HttpStatus.NOT_FOUND ); } - + // Generate reset token for forgot password flow const tokenPayload = { sub: userData.userId, email: userData.email, }; - + resetToken = await this.jwtUtil.generateTokenForForgotPassword( tokenPayload, this.jwt_password_reset_expires_In, this.jwt_secret ); - } + } else { return APIResponse.error( response, @@ -2473,7 +2512,7 @@ export class PostgresUserService implements IServicelocator { HttpStatus.BAD_REQUEST ); } - + // Verify OTP hash const data = `${identifier}.${otp}.${reason}.${expires}`; const calculatedHash = this.authUtils.calculateHash(data, this.smsKey); @@ -2483,7 +2522,7 @@ export class PostgresUserService implements IServicelocator { if (reason === 'forgot' && resetToken) { responseData['token'] = resetToken; } - + return APIResponse.success( response, apiId, @@ -2506,7 +2545,7 @@ export class PostgresUserService implements IServicelocator { `Error during OTP verification: ${error.message}`, apiId ); - + return APIResponse.error( response, apiId, @@ -2665,7 +2704,7 @@ export class PostgresUserService implements IServicelocator { }, }; // console.log("notificationPayload",notificationPayload); - + const mailSend = await this.notificationRequest.sendNotification( notificationPayload ); @@ -2737,7 +2776,7 @@ 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) { @@ -2862,7 +2901,7 @@ export class PostgresUserService implements IServicelocator { try { // For delete events, we may want to include just basic information since the user might already be removed let userData: any; - + if (eventType === 'deleted') { userData = { userId: userId, @@ -2897,10 +2936,10 @@ export class PostgresUserService implements IServicelocator { } 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'); - + // Get cohort information for the user let cohorts = []; @@ -2932,7 +2971,7 @@ export class PostgresUserService implements IServicelocator { LEFT JOIN public."CohortAcademicYear" cay ON bd."cohortId":: UUID = cay."cohortId" LEFT JOIN public."AcademicYears" ay ON cay."academicYearId" = ay."id" `; - + const cohortResults = await this.usersRepository.query(cohortQuery, [userId]); if (cohortResults && cohortResults.length > 0) { cohorts = cohortResults.map(result => ({ @@ -2943,12 +2982,12 @@ export class PostgresUserService implements IServicelocator { joinedAt: result.joinedAt, cohortMemberStatus: result.cohortMemberStatus, tenantId: result.tenantId, - + // Parent Cohort details cohortId: result.cohortId, cohortName: result.cohortName, cohortType: result.cohortType, - + // Academic Year details academicYearId: result.academicYearId, academicYearSession: result.academicYearSession diff --git a/src/common/utils/keycloak.adapter.util.ts b/src/common/utils/keycloak.adapter.util.ts index 9a939e04..91a31e09 100644 --- a/src/common/utils/keycloak.adapter.util.ts +++ b/src/common/utils/keycloak.adapter.util.ts @@ -258,12 +258,87 @@ async function checkIfUsernameExistsInKeycloak(username, token) { return userResponse; } +// Define the structure for user enable/disable operation +interface UpdateUserEnabledQuery { + userId: string; + enabled: boolean; +} + +// Define the structure of the function response +interface UpdateUserEnabledResponse { + success: boolean; + statusCode: number; + message: string; +} + +async function updateUserEnabledStatusInKeycloak( + query: UpdateUserEnabledQuery, + token: string +): Promise { + // Validate required parameters + if (!query.userId) { + return { + success: false, + statusCode: 400, + message: "User status cannot be updated, userId missing", + }; + } + + // Prepare the payload for the update + const data = JSON.stringify({ + enabled: query.enabled, + }); + + // Axios request configuration + const config: AxiosRequestConfig = { + method: "put", + url: `${process.env.KEYCLOAK}${process.env.KEYCLOAK_ADMIN}/${query.userId}`, + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + data: data, + }; + + try { + // Perform the Axios request + const response: AxiosResponse = await axios(config); + + // Handle response status codes + if (response.status === 204) { + return { + success: true, + statusCode: response.status, + message: `User ${query.enabled ? 'enabled' : 'disabled'} successfully in Keycloak`, + }; + } else { + return { + success: false, + statusCode: response.status, + message: `Unexpected response status: ${response.status}`, + }; + } + } catch (error: any) { + // Extract error details + const axiosError: AxiosError = error; + const errorMessage = + axiosError.response?.data?.errorMessage || `Failed to ${query.enabled ? 'enable' : 'disable'} user in Keycloak`; + + return { + success: false, + statusCode: axiosError.response?.status || 500, + message: errorMessage, + }; + } +} + export { getUserGroup, getUserRole, getKeycloakAdminToken, createUserInKeyCloak, updateUserInKeyCloak, + updateUserEnabledStatusInKeycloak, checkIfEmailExistsInKeycloak, checkIfUsernameExistsInKeycloak, }; From 4682f8d19ef5689dd103ecdd997c322523c737d0 Mon Sep 17 00:00:00 2001 From: souravbhowmik1999 Date: Wed, 3 Sep 2025 17:33:55 +0530 Subject: [PATCH 47/53] comment resolved --- src/adapters/postgres/user-adapter.ts | 80 +++++++++++++---------- src/common/utils/keycloak.adapter.util.ts | 24 ++----- src/user/dto/user-update.dto.ts | 2 +- 3 files changed, 55 insertions(+), 51 deletions(-) diff --git a/src/adapters/postgres/user-adapter.ts b/src/adapters/postgres/user-adapter.ts index c0703698..f605dcee 100644 --- a/src/adapters/postgres/user-adapter.ts +++ b/src/adapters/postgres/user-adapter.ts @@ -889,7 +889,6 @@ export class PostgresUserService implements IServicelocator { } } - const { username, firstName, lastName, email } = userDto.userData; const userId = userDto.userId; const keycloakReqBody = { username, firstName, lastName, userId, email }; @@ -945,42 +944,18 @@ export class PostgresUserService implements IServicelocator { userDto?.userId ); + // Synchronize user status with Keycloak if (userDto.userData?.status) { - const shouldEnable = !['archived', 'inactive'].includes( - userDto.userData.status.toLowerCase() - ); - - try { - const keycloakResponse = await getKeycloakAdminToken(); - const token = keycloakResponse.data.access_token; - - const keycloakStatusUpdateResult = await updateUserEnabledStatusInKeycloak( - { userId: userDto.userId, enabled: shouldEnable }, - token - ); - - if (keycloakStatusUpdateResult.success) { - LoggerUtil.log( - `User status synchronized with Keycloak: ${shouldEnable ? 'enabled' : 'disabled'}`, - apiId, - userDto.userId - ); - } else { - LoggerUtil.error( - `Keycloak user status sync failed`, - `Status: ${keycloakStatusUpdateResult.statusCode}, Message: ${keycloakStatusUpdateResult.message}`, - apiId - ); - } - } catch (error) { - LoggerUtil.error( - `Keycloak user status sync error`, + 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', `Error: ${error.message}`, apiId - ); - // Continue with the main flow even if Keycloak sync fails - } + )); } if (userDto?.customFields?.length > 0) { @@ -1181,6 +1156,45 @@ export class PostgresUserService implements IServicelocator { } } + /** + * Synchronizes user status with Keycloak + * @param userId - User ID to update + * @param isActive - Whether user should be active (true) or inactive (false) in Keycloak + * @param apiId - API identifier for logging + */ + private async syncUserStatusWithKeycloak(userId: string, isActive: boolean, apiId: string): Promise { + try { + const keycloakResponse = await getKeycloakAdminToken(); + const token = keycloakResponse.data.access_token; + + const result = await updateUserEnabledStatusInKeycloak( + { userId, enabled: isActive }, + token + ); + + if (result.success) { + LoggerUtil.log( + `Keycloak user status synchronized successfully: ${isActive ? 'enabled' : 'disabled'}`, + apiId, + userId + ); + } else { + LoggerUtil.error( + 'Keycloak user status synchronization failed', + `Status: ${result.statusCode}, Message: ${result.message}`, + apiId + ); + } + } catch (error) { + LoggerUtil.error( + 'Keycloak user status synchronization error', + `Failed to sync user status: ${error.message}`, + apiId + ); + throw error; + } + } + async loginDeviceIdAction(userDeviceId: string, userId: string, existingDeviceId: string[]): Promise { let deviceIds = existingDeviceId || []; // Check if the device ID already exists diff --git a/src/common/utils/keycloak.adapter.util.ts b/src/common/utils/keycloak.adapter.util.ts index 91a31e09..67867e74 100644 --- a/src/common/utils/keycloak.adapter.util.ts +++ b/src/common/utils/keycloak.adapter.util.ts @@ -75,7 +75,7 @@ async function createUserInKeyCloak(query, token, role: string) { value: query.password, }, ], - attributes : { + attributes: { // Multi tenant for roles is not currently supported in keycloak user_roles: [role] // Added in attribute and mappers } @@ -97,7 +97,7 @@ async function createUserInKeyCloak(query, token, role: string) { // Log and return the created user's ID const userId = response.headers.location.split("/").pop(); // Extract user ID from the location header - return { statusCode: response.status, message: "User created successfully", userId : userId }; + return { statusCode: response.status, message: "User created successfully", userId: userId }; } catch (error) { // Handle errors and log relevant details if (error.response) { @@ -303,21 +303,11 @@ async function updateUserEnabledStatusInKeycloak( try { // Perform the Axios request const response: AxiosResponse = await axios(config); - - // Handle response status codes - if (response.status === 204) { - return { - success: true, - statusCode: response.status, - message: `User ${query.enabled ? 'enabled' : 'disabled'} successfully in Keycloak`, - }; - } else { - return { - success: false, - statusCode: response.status, - message: `Unexpected response status: ${response.status}`, - }; - } + return { + success: true, + statusCode: response.status, + message: `User ${query.enabled ? 'enabled' : 'disabled'} successfully in Keycloak`, + }; } catch (error: any) { // Extract error details const axiosError: AxiosError = error; diff --git a/src/user/dto/user-update.dto.ts b/src/user/dto/user-update.dto.ts index 4dc616b3..1230719e 100644 --- a/src/user/dto/user-update.dto.ts +++ b/src/user/dto/user-update.dto.ts @@ -122,7 +122,7 @@ class UserDataDTO { @ApiProperty({ type: () => String }) @IsString() @IsOptional() - @IsEnum(UserStatus) + @IsEnum(UserStatus, { message: 'Invalid status value. Allowed values are: active, inactive, archived.' }) status: UserStatus; @ApiProperty({ type: () => String }) From 46a68f56017a40a4f6b63633498e039357b20055 Mon Sep 17 00:00:00 2001 From: souravbhowmik1999 Date: Wed, 3 Sep 2025 17:37:25 +0530 Subject: [PATCH 48/53] add --- src/adapters/postgres/user-adapter.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/adapters/postgres/user-adapter.ts b/src/adapters/postgres/user-adapter.ts index f605dcee..9be9b90e 100644 --- a/src/adapters/postgres/user-adapter.ts +++ b/src/adapters/postgres/user-adapter.ts @@ -1156,12 +1156,7 @@ export class PostgresUserService implements IServicelocator { } } - /** - * Synchronizes user status with Keycloak - * @param userId - User ID to update - * @param isActive - Whether user should be active (true) or inactive (false) in Keycloak - * @param apiId - API identifier for logging - */ + private async syncUserStatusWithKeycloak(userId: string, isActive: boolean, apiId: string): Promise { try { const keycloakResponse = await getKeycloakAdminToken(); From d130223efa07c9d3525461f9d857c4daa287d66c Mon Sep 17 00:00:00 2001 From: Ishan-ttpl Date: Wed, 3 Sep 2025 17:50:11 +0530 Subject: [PATCH 49/53] Update qa-prod-deployment.yaml --- .github/workflows/qa-prod-deployment.yaml | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/.github/workflows/qa-prod-deployment.yaml b/.github/workflows/qa-prod-deployment.yaml index 712cc6d7..3d0343d6 100644 --- a/.github/workflows/qa-prod-deployment.yaml +++ b/.github/workflows/qa-prod-deployment.yaml @@ -13,7 +13,7 @@ on: jobs: deploy: - name: Deploy backend Service + name: Deploy Backend Service runs-on: ubuntu-latest environment: ${{ github.event.inputs.environment }} @@ -30,9 +30,9 @@ jobs: # Step 3: Debug TAG value - name: Debug TAG value - run: echo "TAG value:${{ env.TAG }}" + run: echo "TAG value: ${{ env.TAG }}" - # Step 4: Configure AWS credentials (from environment-specific secrets) + # Step 4: Configure AWS credentials - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: @@ -40,13 +40,13 @@ jobs: aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: ${{ secrets.AWS_REGION }} - # Step 5: Generate ConfigMap YAML safely - - name: Generate ConfigMap YAML + # Step 5: Decode ConfigMap manifest from secret + - name: Write ConfigMap manifest run: | mkdir -p manifest - cat < manifest/configmap.yaml - ${{ secrets.ENV_FILE_CONTENT }} - EOF + echo "${{ secrets.ENV_FILE_CONTENT_BACKEND }}" | base64 -d > manifest/configmap.yaml + echo "Generated ConfigMap:" + cat manifest/configmap.yaml # Step 6: Update Deployment Manifest - name: Update Deployment Manifest @@ -67,8 +67,10 @@ jobs: NAMESPACE: ${{ github.event.inputs.environment == 'prod' && 'default' || 'microservices-qa' }} run: | aws eks update-kubeconfig --name $EKS_CLUSTER_NAME --region $AWS_REGION - kubectl apply -f manifest/backend-service-updated.yaml -n $NAMESPACE kubectl apply -f manifest/configmap.yaml -n $NAMESPACE + kubectl apply -f manifest/backend-service-updated.yaml -n $NAMESPACE + # Restart pods to pick up new config + kubectl rollout restart deployment backend -n $NAMESPACE sleep 10 echo "Pods status:" kubectl get pods -n $NAMESPACE | grep backend From 01e491395f067cade8b22aea0ae1829d6009d47e Mon Sep 17 00:00:00 2001 From: Ishan-ttpl Date: Wed, 3 Sep 2025 17:50:40 +0530 Subject: [PATCH 50/53] Update qa-prod-deployment.yaml --- .github/workflows/qa-prod-deployment.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/qa-prod-deployment.yaml b/.github/workflows/qa-prod-deployment.yaml index 3d0343d6..1c0644f4 100644 --- a/.github/workflows/qa-prod-deployment.yaml +++ b/.github/workflows/qa-prod-deployment.yaml @@ -30,7 +30,7 @@ jobs: # Step 3: Debug TAG value - name: Debug TAG value - run: echo "TAG value: ${{ env.TAG }}" + run: echo "TAG value:${{ env.TAG }}" # Step 4: Configure AWS credentials - name: Configure AWS credentials From 10670c5435d2d91cb2d2146a291a7d51cc4e9d19 Mon Sep 17 00:00:00 2001 From: Ishan-ttpl Date: Wed, 3 Sep 2025 17:56:27 +0530 Subject: [PATCH 51/53] Update qa-prod-deployment.yaml --- .github/workflows/qa-prod-deployment.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/qa-prod-deployment.yaml b/.github/workflows/qa-prod-deployment.yaml index 1c0644f4..35fe7fe1 100644 --- a/.github/workflows/qa-prod-deployment.yaml +++ b/.github/workflows/qa-prod-deployment.yaml @@ -55,7 +55,7 @@ jobs: ECR_REPOSITORY: ${{ secrets.ECR_REPOSITORY }} run: | mkdir -p manifest - envsubst < manifest/backend-service.yaml > manifest/backend-service-updated.yaml + envsubst < manifest/backend.yaml > manifest/backend-service-updated.yaml echo "Updated deployment manifest:" cat manifest/backend-service-updated.yaml From 2f7b0348230a69f77335b50e26afc532f50b51e6 Mon Sep 17 00:00:00 2001 From: "abhilash.dube" Date: Thu, 4 Sep 2025 14:48:45 +0530 Subject: [PATCH 52/53] Removed unused Pipelines and rename pipelines as per environment --- .github/workflows/Prod-Deployment.yaml | 87 ------------------- .github/workflows/build.yaml | 8 +- ...er-deployment.yaml => dev-deployment.yaml} | 8 +- .../workflows/dev-pratham-eks-deployment.yaml | 87 ------------------- .github/workflows/qa-prod-deployment.yaml | 2 +- 5 files changed, 10 insertions(+), 182 deletions(-) delete mode 100644 .github/workflows/Prod-Deployment.yaml rename .github/workflows/{tekdi-server-deployment.yaml => dev-deployment.yaml} (81%) delete mode 100644 .github/workflows/dev-pratham-eks-deployment.yaml diff --git a/.github/workflows/Prod-Deployment.yaml b/.github/workflows/Prod-Deployment.yaml deleted file mode 100644 index 3b1c2c5a..00000000 --- a/.github/workflows/Prod-Deployment.yaml +++ /dev/null @@ -1,87 +0,0 @@ -name: PROD-BACKEND-TAG-BASE-DEPLOYMENT - -on: - push: - tags: - - 'v*' # This will trigger on tags starting with 'v' - -env: - ECR_REPOSITORY: ${{ secrets.ECR_REPOSITORY }} - EKS_CLUSTER_NAME: ${{ secrets.EKS_CLUSTER_NAME_PROD }} - AWS_REGION: ${{ secrets.AWS_REGION_NAME }} - -jobs: - BACKEND-TAG-BASE-DEPLOYMENT-PROD: - name: Deployment - runs-on: ubuntu-latest - steps: - - name: Check out code - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - - name: Set TAG environment variable - run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV - - - name: Debug TAG value - run: echo "TAG value - ${{ env.TAG }}" - - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v1 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID_PROD }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY_PROD }} - aws-region: ${{ env.AWS_REGION }} - - - name: Setup Node Env - uses: actions/setup-node@v3 - with: - node-version: 21.1.0 - - - name: Copy .env file - env: - ENV_FILE_CONTENT: ${{ secrets.ENV_FILE_CONTENT_PROD }} - run: echo "$ENV_FILE_CONTENT" > manifest/configmap.yaml - - - name: Show PWD and list content and Latest 3 commits - run: | - echo "Fetching all branches to ensure complete history" - git fetch --all - echo "Checking out the current branch" - git checkout ${{ github.ref_name }} - echo "Git Branch cloned" - git branch - echo "Current 3 merge commits are:" - git log --merges -n 3 - pwd - ls -ltra - - - name: Login to Amazon ECR - id: login-ecr - uses: aws-actions/amazon-ecr-login@v1 - - - name: Build, tag, and push image to Amazon ECR - env: - ECR_REGISTRY: ${{ secrets.ECR_REPOSITORY }} - IMAGE_TAG: ${{ env.TAG }} - run: | - docker build -t ${{ secrets.ECR_REPOSITORY }}:${{ env.TAG }} . - docker push ${{ secrets.ECR_REPOSITORY }}:${{ env.TAG }} - - - name: Update kube config - run: aws eks update-kubeconfig --name ${{ secrets.EKS_CLUSTER_NAME_PROD }} --region ${{ secrets.AWS_REGION_NAME }} - - - name: Deploy to EKS - env: - ECR_REGISTRY: ${{ secrets.ECR_REPOSITORY }} - IMAGE_TAG: ${{ env.TAG }} - run: | - export ECR_REPOSITORY=${{ secrets.ECR_REPOSITORY }} - export IMAGE_TAG=${{ env.TAG }} - envsubst < manifest/backend.yaml > manifest/backend-updated.yaml - cat manifest/backend-updated.yaml - kubectl delete deployment backend - kubectl apply -f manifest/backend-updated.yaml - kubectl apply -f manifest/configmap.yaml - sleep 10 - kubectl get pods | grep backend diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 9a363c36..569e16c2 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -1,4 +1,4 @@ -name: Tag-based Build Image +name: Tag-based Image Build for QA and PROD on: push: @@ -18,10 +18,12 @@ jobs: build: name: Build and Push Docker Image to AWS ECR runs-on: ubuntu-latest - environment: ${{ github.event.inputs.environment }} + + # Use qa for tag pushes, or the chosen input for workflow_dispatch + environment: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.environment || 'qa' }} env: - ENVIRONMENT: ${{ github.event.inputs.environment }} + ENVIRONMENT: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.environment || 'qa' }} TAG: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || github.ref_name }} steps: diff --git a/.github/workflows/tekdi-server-deployment.yaml b/.github/workflows/dev-deployment.yaml similarity index 81% rename from .github/workflows/tekdi-server-deployment.yaml rename to .github/workflows/dev-deployment.yaml index badaf466..78fb5d41 100644 --- a/.github/workflows/tekdi-server-deployment.yaml +++ b/.github/workflows/dev-deployment.yaml @@ -1,8 +1,8 @@ -name: Deploy to Tekdi-QA-Server +name: Deploy to Dev Server Dockerised on: push: branches: - - sdbv_rbac_changes + - main jobs: deploy: runs-on: ubuntu-latest @@ -16,12 +16,12 @@ jobs: username: ${{ secrets.USERNAME_TEKDI_QA }} key: ${{ secrets.EC2_SSH_KEY_TEKDI_QA }} port: ${{ secrets.PORT_TEKDI_QA }} - script: | + script: | cd ${{ secrets.TARGET_DIR_TEKDI_QA }} if [ -f .env ]; then rm .env fi - echo '${{ secrets.QA_ENV }}"' > .env + echo '${{ secrets.DEV_ENV }}"' > .env ls -ltra ./deploy.sh #Testing diff --git a/.github/workflows/dev-pratham-eks-deployment.yaml b/.github/workflows/dev-pratham-eks-deployment.yaml deleted file mode 100644 index a27bc03d..00000000 --- a/.github/workflows/dev-pratham-eks-deployment.yaml +++ /dev/null @@ -1,87 +0,0 @@ -name: Deploy to EKS-Pratham -on: - workflow_dispatch: -env: - ECR_REPOSITORY: ${{ secrets.ECR_REPOSITORY }} - EKS_CLUSTER_NAME: ${{ secrets.EKS_CLUSTER_NAME }} - AWS_REGION: ${{ secrets.AWS_REGION_NAME }} -jobs: - build: - - name: Deployment - runs-on: ubuntu-latest - steps: - - name: Set short git commit SHA - id: commit - uses: prompt/actions-commit-hash@v2 - - name: Check out code - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v1 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: ${{env.AWS_REGION}} - - name: Setup Node Env - uses: actions/setup-node@v3 - with: - node-version: 21.1.0 - - name: Copy .env file - env: - ENV_FILE_CONTENT: ${{ secrets.ENV_FILE_CONTENT }} - run: printf "%s" "$ENV_FILE_CONTENT" > manifest/configmap.yaml - #echo "$ENV_FILE_CONTENT" > manifest/configmap.yaml - - name: Show PWD and list content and Latest 3 commits - run: | - echo "Fetching all branches to ensure complete history" - git fetch --all - echo "Checking out the current branch" - git checkout ${{ github.ref_name }} - echo "Git Branch cloned" - git branch - echo "Current 3 merge commits are:" - git log --merges -n 3 - pwd - ls -ltra - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v1 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: ${{ env.AWS_REGION }} - - name: Login to Amazon ECR - id: login-ecr - uses: aws-actions/amazon-ecr-login@v1 - - name: Build, tag, and push image to Amazon ECR - env: - ECR_REGISTRY: ${{ secrets.ECR_REPOSITORY }} - IMAGE_TAG: ${{ secrets.ECR_IMAGE }} - run: | - docker build -t ${{ secrets.ECR_REPOSITORY }}:${{ secrets.IMAGE_TAG }} . - docker push ${{ secrets.ECR_REPOSITORY }}:${{ secrets.IMAGE_TAG }} - - name: Update kube config - run: aws eks update-kubeconfig --name ${{ secrets.EKS_CLUSTER_NAME }} --region ${{ secrets.AWS_REGION_NAME }} - - name: Deploy to EKS - env: - ECR_REGISTRY: ${{ secrets.ECR_REPOSITORY }} - IMAGE_TAG: ${{ secrets.IMAGE_TAG }} - ECR_REPOSITORY: ${{ secrets.ECR_REPOSITORY }} - ECR_IMAGE: ${{ secrets.ECR_IMAGE }} - run: | - export ECR_REPOSITORY=${{ secrets.ECR_REPOSITORY }} - export IMAGE_TAG=${{ secrets.IMAGE_TAG }} - export ECR_IMAGE=${{ secrets.ECR_IMAGE }} - envsubst < manifest/backend.yaml > manifest/backend-updated.yaml - cat manifest/backend-updated.yaml - rm -rf manifest/backend-service.yaml - kubectl delete deployment backend - kubectl delete service backend - kubectl delete cm backend-service-config - kubectl apply -f manifest/backend-updated.yaml - kubectl apply -f manifest/configmap.yaml - sleep 10 - kubectl get pods - kubectl get services - kubectl get deployment diff --git a/.github/workflows/qa-prod-deployment.yaml b/.github/workflows/qa-prod-deployment.yaml index 35fe7fe1..13625bec 100644 --- a/.github/workflows/qa-prod-deployment.yaml +++ b/.github/workflows/qa-prod-deployment.yaml @@ -1,4 +1,4 @@ -name: BACKEND-TAG-BASED-DEPLOYMENT +name: BACKEND-TAG-BASED-DEPLOYMENT-AWS-EKS on: workflow_dispatch: From 7dd466171bf85c3c61e3bead61d8e0e0d873aeec Mon Sep 17 00:00:00 2001 From: apurvaubade Date: Wed, 10 Sep 2025 17:51:24 +0530 Subject: [PATCH 53/53] whatsapp notification --- src/adapters/postgres/user-adapter.ts | 139 ++++++++++++++++++++++++- src/common/utils/notification.axios.ts | 64 ++++++++++++ src/common/utils/response.messages.ts | 6 +- src/user/dto/otpVerify.dto.ts | 12 ++- 4 files changed, 213 insertions(+), 8 deletions(-) diff --git a/src/adapters/postgres/user-adapter.ts b/src/adapters/postgres/user-adapter.ts index 9be9b90e..9e295e66 100644 --- a/src/adapters/postgres/user-adapter.ts +++ b/src/adapters/postgres/user-adapter.ts @@ -2367,14 +2367,47 @@ export class PostgresUserService implements IServicelocator { HttpStatus.BAD_REQUEST ); } - // Step 1: Prepare data for OTP generation and send on Mobile - const { notificationPayload, hash, expires } = await this.sendOTPOnMobile(mobile, reason); + // 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') { + // Send via SMS ONLY for signup/login without triggering WhatsApp + const mobileWithCode = this.formatMobileNumber(mobile); + const otp = this.authUtils.generateOtp(this.otpDigits).toString(); + const generated = this.generateOtpHash(mobileWithCode, otp, reason); + hash = generated.hash; + expires = generated.expires; + const replacements = { + "{OTP}": otp, + "{otpExpiry}": generated.expiresInMinutes + }; + notificationPayload = await this.smsNotification("OTP", "SEND_OTP", replacements, [mobile]); + } else { + // Default: WhatsApp ONLY for signup/login + const otp = this.authUtils.generateOtp(this.otpDigits).toString(); + const waResult = await this.sendOtpOnWhatsApp(mobile, otp, reason); + notificationPayload = waResult.notificationPayload; + hash = waResult.hash; + 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; + } + // Step 2: Send success response const result = { data: { message: `OTP sent to ${mobile}`, hash: `${hash}.${expires}`, - sendStatus: notificationPayload.result?.sms?.data[0] + sendStatus: notificationPayload?.result?.whatsapp?.data?.[0] || notificationPayload?.result?.sms?.data?.[0] // sid: message.sid, // Twilio Message SID } }; @@ -2414,12 +2447,109 @@ export class PostgresUserService implements IServicelocator { }; // Step 2:send SMS notification 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') { + try { + await this.sendOtpOnWhatsApp(mobile, otp, reason); + } catch (waErr: any) { + LoggerUtil.warn(`WhatsApp OTP send failed: ${waErr?.message || waErr}`, APIID.SEND_OTP); + } + } return { notificationPayload, hash, expires, expiresInMinutes }; } catch (error) { throw new Error(`Failed to send OTP: ${error.message}`); } } + + 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); + return { notificationPayload, hash, expires, expiresInMinutes }; + } + catch (error) { + throw new Error(`Failed to send OTP via WhatsApp: ${error.message}`); + } + } + + async whatsappNotificationRaw(whatsapp: string, otp: string, reason: string) { + try { + const formattedWhatsapp = this.formatMobileNumber(whatsapp); + const templateId = this.configService.get("WHATSAPP_TEMPLATE_ID"); + const apiKey = this.configService.get("WHATSAPP_GUPSHUP_API_KEY"); + const gupshupSource = this.configService.get("WHATSAPP_GUPSHUP_SOURCE"); + + if (!templateId || !apiKey || !gupshupSource) { + LoggerUtil.error( + "WhatsApp environment variables not configured", + "WhatsApp OTP sending is disabled. Please configure WHATSAPP_TEMPLATE_ID, WHATSAPP_GUPSHUP_API_KEY, and WHATSAPP_GUPSHUP_SOURCE", + "WHATSAPP_CONFIG" + ); + return { + result: { + whatsapp: { + data: [{ status: "skipped", message: "WhatsApp not configured" }], + }, + }, + }; + } + + const payload = { + whatsapp: { + to: [formattedWhatsapp], + templateId: templateId, + templateParams: [otp], + gupshupSource: gupshupSource, + gupshupApiKey: apiKey, + }, + }; + + 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}`); + } + if (!mailSend || !mailSend.result || !mailSend.result.whatsapp) { + throw new Error("Invalid response from notification service"); + } + return mailSend; + } + catch (error) { + LoggerUtil.error(API_RESPONSES.WHATSAPP_ERROR, error.message); + throw new Error(`${API_RESPONSES.WHATSAPP_NOTIFICATION_ERROR}: ${error.message}`); + } + } + + async whatsappNotification(context: string, key: string, replacements: object, receipients: string[]) { + try { + const notificationPayload = { + isQueue: false, + context: context, + key: key, + replacements: replacements, + whatsapp: { + receipients: receipients.map((recipient) => recipient.toString()), + }, + }; + 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); + const combinedErrorMessage = errorMessages.join(", "); + throw new Error(`${API_RESPONSES.WHATSAPP_ERROR} :${combinedErrorMessage}`); + } + return result; + } + catch (error) { + LoggerUtil.error(API_RESPONSES.WHATSAPP_ERROR, error.message); + throw new Error(`${API_RESPONSES.WHATSAPP_NOTIFICATION_ERROR}: ${error.message}`); + } + } + //verify OTP based on reason [signup , forgot] async verifyOtp(body: OtpVerifyDTO, response: Response) { const apiId = APIID.VERIFY_OTP; @@ -2464,7 +2594,7 @@ export class PostgresUserService implements IServicelocator { let resetToken: string | null = null; // Process based on reason - if (reason === 'signup') { + if (reason === 'signup' || reason === 'login') { if (!mobile) { return APIResponse.error( response, @@ -2474,6 +2604,7 @@ export class PostgresUserService implements IServicelocator { HttpStatus.BAD_REQUEST ); } + // Always use 10-digit mobile from body and format to +91 internally for hash match identifier = this.formatMobileNumber(mobile); } else if (reason === 'forgot') { diff --git a/src/common/utils/notification.axios.ts b/src/common/utils/notification.axios.ts index 789b427c..f29a9749 100644 --- a/src/common/utils/notification.axios.ts +++ b/src/common/utils/notification.axios.ts @@ -40,6 +40,70 @@ export class NotificationRequest { const statusCode = error.response.status; const errorDetails = error.response.data || API_RESPONSES.ERROR; + switch (statusCode) { + case 400: + throw new HttpException( + `Bad Request: ${ + errorDetails.params?.errmsg || API_RESPONSES.BAD_REQUEST + }`, + HttpStatus.BAD_REQUEST + ); + case 404: + throw new HttpException( + `Not Found: ${ + errorDetails.params?.errmsg || API_RESPONSES.NOT_FOUND + }`, + HttpStatus.NOT_FOUND + ); + case 500: + throw new HttpException( + `Internal Server Error: ${ + errorDetails.params?.errmsg || + API_RESPONSES.INTERNAL_SERVER_ERROR + }`, + HttpStatus.INTERNAL_SERVER_ERROR + ); + default: + throw new HttpException( + `Unexpected Error: ${ + errorDetails.params?.errmsg || API_RESPONSES.UNEXPECTED_ERROR + }`, + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } + throw new HttpException( + API_RESPONSES.INTERNAL_SERVER_ERROR, + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } + async sendRawNotification(body) { + const data = JSON.stringify(body); + const config: AxiosRequestConfig = { + method: "POST", + maxBodyLength: Infinity, + url: `${this.url}/notification/send-raw`, + headers: { + "Content-Type": "application/json", + }, + data: data, + }; + try { + const response = await axios.request(config); + return response.data; + } catch (error) { + if (error.code === "ECONNREFUSED") { + throw new HttpException( + API_RESPONSES.SERVICE_UNAVAILABLE, + HttpStatus.SERVICE_UNAVAILABLE + ); + } + if (error.response) { + const statusCode = error.response.status; + const errorDetails = error.response.data || API_RESPONSES.ERROR; + + switch (statusCode) { case 400: throw new HttpException( diff --git a/src/common/utils/response.messages.ts b/src/common/utils/response.messages.ts index ec8d5108..f659c3ef 100644 --- a/src/common/utils/response.messages.ts +++ b/src/common/utils/response.messages.ts @@ -213,7 +213,9 @@ export const API_RESPONSES = { EMAIL_ERROR: "Email notification failed", SIGNED_URL_SUCCESS: "Signed URL generated successfully", SIGNED_URL_FAILED: "Error while generating signed URL", - INVALID_FILE_TYPE: "Invalid file type. Allowed file types are: '.jpg','.jpeg','.png','.webp','.pdf','.doc','.docx','.mp4','.mov','.txt','.csv','.mp3'", - FILE_SIZE_ERROR: "File too large. Maximum allowed file size is 10MB." + INVALID_FILE_TYPE: "Invalid file type. Allowed file types are: '.jpg','.jpeg','.png','.webp','.pdf','.doc','.docx','.mp4','.mov','.txt','.csv','.mp3", + FILE_SIZE_ERROR: "File too large. Maximum allowed file size is 10MB.", + WHATSAPP_ERROR: "WhatsApp notification failed", + WHATSAPP_NOTIFICATION_ERROR: "Failed to send WhatsApp notification:", }; diff --git a/src/user/dto/otpVerify.dto.ts b/src/user/dto/otpVerify.dto.ts index d8e1652f..de65b47f 100644 --- a/src/user/dto/otpVerify.dto.ts +++ b/src/user/dto/otpVerify.dto.ts @@ -1,5 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsIn, IsString, Length, Matches, ValidateIf, registerDecorator, ValidationOptions, ValidationArguments } from 'class-validator'; +import { Transform } from 'class-transformer'; // Custom validator function function IsValidOtp(validationOptions?: ValidationOptions) { @@ -26,7 +27,14 @@ function IsValidOtp(validationOptions?: ValidationOptions) { export class OtpVerifyDTO { @ApiProperty() - @ValidateIf(o => o.reason === 'signup') + @ValidateIf(o => o.reason === 'signup' || o.reason === 'login') + @Transform(({ value }) => { + if (value === undefined || value === null) return value; + const digits = String(value).replace(/\D/g, ''); + if (digits.length === 12 && digits.startsWith('91')) return digits.slice(2); // handle +91xxxxxxxxxx + if (digits.length === 11 && digits.startsWith('0')) return digits.slice(1); // handle 0-prefixed + return digits; + }) @IsString({ message: 'Mobile number must be a string.' }) @Matches(/^\d{10}$/, { message: 'Mobile number must be exactly 10 digits.' }) mobile: string; @@ -43,7 +51,7 @@ export class OtpVerifyDTO { @ApiProperty() @IsString({ message: 'Reason must be a string.' }) - @IsIn(['signup', 'forgot'], { message: 'Reason must be either "signup" or "forgot".' }) + @IsIn(['signup', 'login', 'forgot'], { message: 'Reason must be either "signup", "login" or "forgot".' }) reason: string; @ApiProperty()