From f6b1d41e26ebf40d666d37e832f35317557a810c Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Tue, 7 Oct 2025 01:17:55 +0530 Subject: [PATCH 01/32] Add distribution for organization_registration_codes table --- src/distributionColumns.sql | 1 + 1 file changed, 1 insertion(+) diff --git a/src/distributionColumns.sql b/src/distributionColumns.sql index 0d970a56d..e573ff3a5 100644 --- a/src/distributionColumns.sql +++ b/src/distributionColumns.sql @@ -19,6 +19,7 @@ SELECT create_distributed_table('user_sessions', 'tenant_code'); -- Organization-related tables SELECT create_distributed_table('organization_email_domains', 'tenant_code'); SELECT create_distributed_table('organization_features', 'tenant_code'); +SELECT create_distributed_table('organization_registration_codes', 'tenant_code'); -- Invitation / user-related SELECT create_distributed_table('invitations','tenant_code'); From 0b40f74b5139da71399db421222021449f07c17f Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Tue, 7 Oct 2025 01:18:10 +0530 Subject: [PATCH 02/32] Add 'code' field to organization update blacklist --- src/constants/blacklistConfig.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/constants/blacklistConfig.js b/src/constants/blacklistConfig.js index d4f1346f4..5ad04b304 100644 --- a/src/constants/blacklistConfig.js +++ b/src/constants/blacklistConfig.js @@ -299,6 +299,7 @@ const organization = { update: [ 'id', 'org_admin', + 'code', 'tenant_code', 'parent_id', 'related_orgs', From 463ecb27ce04a8d8f01a36960ede95cc0daec1ba Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Tue, 7 Oct 2025 01:20:20 +0530 Subject: [PATCH 03/32] Refactor eventBodyDTO to use oldValues and newValues for organization updates --- src/dtos/organizationDTO.js | 17 +++---------- src/services/admin.js | 12 ++++++--- src/services/organization.js | 47 +++++++++++++++++++++++++++++------- 3 files changed, 50 insertions(+), 26 deletions(-) diff --git a/src/dtos/organizationDTO.js b/src/dtos/organizationDTO.js index 63cfb1a5a..87a9c76bd 100644 --- a/src/dtos/organizationDTO.js +++ b/src/dtos/organizationDTO.js @@ -11,29 +11,20 @@ class organizationDTO { } } - static eventBodyDTO({ entity, eventType, entityId, changedValues = [], args = {} }) { + static eventBodyDTO({ entity, eventType, entityId, oldValues, newValues, args = {} }) { try { if (!entity || !eventType || !entityId) throw new Error('Entity, EventType & EntityId values are mandatory for an Event') const disallowedArgs = ['deleted_at'] + disallowedArgs.forEach((key) => delete args[key]) - disallowedArgs.forEach((key) => { - delete args[key] - }) - - const changes = changedValues.reduce((result, obj) => { - const { fieldName, oldValue, newValue } = obj - if (!result[fieldName]) result[fieldName] = {} - if (oldValue) result[fieldName].oldValue = oldValue - if (newValue) result[fieldName].newValue = newValue - return result - }, {}) return { entity, eventType, entityId, - changes, + ...(oldValues ? { oldValues } : {}), + ...(newValues ? { newValues } : {}), ...args, } } catch (error) { diff --git a/src/services/admin.js b/src/services/admin.js index 8cf4fe19b..63174651f 100644 --- a/src/services/admin.js +++ b/src/services/admin.js @@ -722,18 +722,22 @@ module.exports = class AdminHelper { //Event body for org update (deactivation) let updatedOrgDetails = orgUpdateResult.updatedRows?.[0] + const eventBodyData = organizationDTO.eventBodyDTO({ entity: 'organization', - eventType: 'update', + eventType: 'deactivate', entityId: updatedOrgDetails.id, args: { - created_by: updatedOrgDetails.created_by, - name: updatedOrgDetails.name, + id: updatedOrgDetails.id, code: updatedOrgDetails.code, + name: updatedOrgDetails.name, + created_by: updatedOrgDetails.created_by, + updated_by: updatedOrgDetails.updated_by, updated_at: updatedOrgDetails?.updated_at || new Date(), + deleted_at: updatedOrgDetails?.deleted_at, status: updatedOrgDetails?.status || common.INACTIVE_STATUS, - id: updatedOrgDetails.id, tenant_code: tenantCode, + deactivated_users_count: userRowsAffected, }, }) diff --git a/src/services/organization.js b/src/services/organization.js index c36a081fa..84ad772e4 100644 --- a/src/services/organization.js +++ b/src/services/organization.js @@ -830,23 +830,53 @@ async function createRoleRequest(bodyData, tokenInformation) { async function orgEventEmitter(orgDetailsBeforeUpdate, updatedOrgDetails, bodyData) { // compute changes from provided body keys - let changes = await utils.extractUpdatedValues(orgDetailsBeforeUpdate, updatedOrgDetails, bodyData) - let related_org_details - if (Object.prototype.hasOwnProperty.call(bodyData, 'related_orgs') && updatedOrgDetails?.related_orgs?.length) { + const newValues = utils.extractDelta(orgDetailsBeforeUpdate, updatedOrgDetails, bodyData) + + // nothing changed → no event + if (Object.keys(newValues).length === 0) return + + // deep-clone old to avoid later mutation bleed + const oldValues = _.cloneDeep(orgDetailsBeforeUpdate) + + let related_org_details_new + let related_org_details_old + + if (Object.prototype.hasOwnProperty.call(bodyData, 'related_orgs')) { const options = { attributes: ['id', 'code'], } - related_org_details = await organizationQueries.findAll( - { id: { [Op.in]: updatedOrgDetails.related_orgs } }, - options - ) + + // Handle new values: fetch if non-empty, else set empty array + if (updatedOrgDetails?.related_orgs?.length > 0) { + related_org_details_new = await organizationQueries.findAll( + { id: { [Op.in]: updatedOrgDetails.related_orgs } }, + options + ) + newValues.related_org_details = related_org_details_new + } else { + newValues.related_org_details = [] + } + + // Handle old values: fetch if non-empty before update, else set empty array + // (This covers additions symmetrically, where old was empty) + if (orgDetailsBeforeUpdate?.related_orgs?.length > 0) { + related_org_details_old = await organizationQueries.findAll( + { id: { [Op.in]: orgDetailsBeforeUpdate.related_orgs } }, + options + ) + oldValues.related_org_details = related_org_details_old + } else { + oldValues.related_org_details = [] + } } + //event Body for org updates const eventBodyData = organizationDTO.eventBodyDTO({ entity: 'organization', eventType: 'update', entityId: orgDetailsBeforeUpdate.id, - changedValues: changes, + oldValues, + newValues, args: { created_by: orgDetailsBeforeUpdate.created_by, name: orgDetailsBeforeUpdate.name, @@ -860,7 +890,6 @@ async function orgEventEmitter(orgDetailsBeforeUpdate, updatedOrgDetails, bodyDa description: orgDetailsBeforeUpdate.description, related_orgs: orgDetailsBeforeUpdate?.related_orgs || [], tenant_code: orgDetailsBeforeUpdate.tenant_code, - ...(related_org_details ? { related_org_details } : {}), }, }) broadcastEvent('organizationEvents', { requestBody: eventBodyData, isInternal: true }) From 22cffa95d09441b36fbf275880c7a8064343de4c Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Tue, 7 Oct 2025 01:20:38 +0530 Subject: [PATCH 04/32] Add extractDelta function and utility logging in eventBroadcasterMain --- src/generics/utils.js | 23 +++++++++++++++++++++++ src/helpers/eventBroadcasterMain.js | 4 ++++ 2 files changed, 27 insertions(+) diff --git a/src/generics/utils.js b/src/generics/utils.js index 0863e05ed..7d4cbaa9d 100644 --- a/src/generics/utils.js +++ b/src/generics/utils.js @@ -1112,6 +1112,28 @@ function extractUpdatedValues(oldData = {}, newData = {}, updateData = {}) { return changes } +function flattenLeafPaths(obj, prefix = '') { + const out = [] + for (const [k, v] of Object.entries(obj || {})) { + const path = prefix ? `${prefix}.${k}` : k + if (v && typeof v === 'object' && !Array.isArray(v)) out.push(...flattenLeafPaths(v, path)) + else out.push(path) + } + return out +} + +function extractDelta(oldData = {}, newData = {}, updateData = {}) { + const delta = {} + const paths = flattenLeafPaths(updateData) // only compare fields the client attempted to update + + for (const p of paths) { + const oldVal = _.get(oldData, p) + const newVal = _.get(newData, p) + if (!_.isEqual(oldVal, newVal)) _.set(delta, p, newVal) + } + return delta +} + module.exports = { generateToken, hashPassword, @@ -1159,4 +1181,5 @@ module.exports = { appendParamsToUrl, parseMetaData, extractUpdatedValues, + extractDelta, } diff --git a/src/helpers/eventBroadcasterMain.js b/src/helpers/eventBroadcasterMain.js index c78751e99..1e832f352 100644 --- a/src/helpers/eventBroadcasterMain.js +++ b/src/helpers/eventBroadcasterMain.js @@ -1,6 +1,7 @@ 'use strict' const requester = require('@utils/requester') const kafkaCommunication = require('@generics/kafka-communication') +const util = require('util') const getEndpoints = (eventGroup) => { switch (eventGroup) { @@ -86,6 +87,9 @@ exports.eventBroadcasterKafka = async (eventGroup, { requestBody }) => { } exports.broadcastEvent = async (eventGroup, { requestBody, headers = {}, isInternal = true }) => { try { + //TODO: Remove this log after testing + console.log(util.inspect(requestBody, { depth: null, colors: true, compact: false })) + // Fire both broadcaster functions concurrently const broadcastPromises = [ exports.eventBroadcasterMain(eventGroup, { requestBody, headers, isInternal }), From ada71ae5545e1819c4d220ae8bff0295d973c57f Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Tue, 7 Oct 2025 01:22:02 +0530 Subject: [PATCH 05/32] Add raw option to tenant update queries for consistent response format --- src/database/queries/tenants.js | 1 + src/services/tenant.js | 107 ++++++++++++-------------------- 2 files changed, 41 insertions(+), 67 deletions(-) diff --git a/src/database/queries/tenants.js b/src/database/queries/tenants.js index 7c7a05524..7c39d8010 100644 --- a/src/database/queries/tenants.js +++ b/src/database/queries/tenants.js @@ -67,6 +67,7 @@ exports.update = async (filter, updatedata) => { return await Tenant.update(updatedata, { where: filter, returning: true, + raw: true, }) } catch (error) { throw error diff --git a/src/services/tenant.js b/src/services/tenant.js index f726c70b0..0cfe0d05c 100644 --- a/src/services/tenant.js +++ b/src/services/tenant.js @@ -432,9 +432,14 @@ module.exports = class tenantHelper { static async update(tenantCode, bodyData, userId) { try { // fetch tenant details - const tenantDetails = await tenantQueries.findOne({ - code: tenantCode, - }) + const tenantDetails = await tenantQueries.findOne( + { + code: tenantCode, + }, + { + raw: true, + } + ) if (!tenantDetails?.code) { return responses.failureResponse({ @@ -443,35 +448,17 @@ module.exports = class tenantHelper { message: 'TENANT_NOT_FOUND', }) } - // list of updatable keys for a tenant - const tenantUpdatableKeys = ['name', 'description', 'logo', 'theming', 'meta'] - // pick relevant data from body data for tenant updation - bodyData = _.pick(bodyData, tenantUpdatableKeys) - - let tenantUpdateBody = {} - // prepare the update body based on comparing with the current data to make sure the change - for (let key of tenantUpdatableKeys) { - if (!_.isEqual(bodyData[key], tenantDetails[key])) { - tenantUpdateBody[key] = bodyData[key] - } - } - let updatedTenantDetails - // update only if the data is changed - if (Object.keys(tenantUpdateBody).length > 0) { - tenantUpdateBody.updated_by = userId - const [rowsAffected, updatedRows] = await tenantQueries.update( - { - code: tenantCode, - }, - tenantUpdateBody, - { - returning: true, - raw: true, - } - ) - updatedTenantDetails = updatedRows?.[0] - } + bodyData.updated_by = userId + const [rowsAffected, updatedRows] = await tenantQueries.update( + { + code: tenantCode, + }, + bodyData + ) + let updatedTenantDetails = updatedRows?.[0] + + console.log({ updatedTenantDetails }) await tenantEventEmitter(tenantDetails, updatedTenantDetails, bodyData) return responses.successResponse({ @@ -554,19 +541,13 @@ module.exports = class tenantHelper { result: {}, }) } - let changes = [ - { - fieldName: 'domains', - oldValue: existingDomains, - newValue: [...existingDomains, ...domainsToCreate], - }, - ] const eventBodyData = TenantDTO.eventBodyDTO({ entity: 'tenant', eventType: 'update', entityId: tenantDetails.code, - changedValues: changes, + oldValue: existingDomains, + newValue: [...existingDomains, ...domainsToCreate], args: { created_by: tenantDetails.created_by, name: tenantDetails.name, @@ -694,19 +675,12 @@ module.exports = class tenantHelper { await Promise.all(domainRemovePromise) - let changes = [ - { - fieldName: 'domains', - oldValue: existingDomains, - newValue: existingDomains.filter((d) => !domainsToRemove.includes(d)), - }, - ] - const eventBodyData = TenantDTO.eventBodyDTO({ entity: 'tenant', eventType: 'update', entityId: tenantDetails.code, - changedValues: changes, + oldValue: existingDomains, + newValue: existingDomains.filter((d) => !domainsToRemove.includes(d)), args: { created_by: tenantDetails.created_by, name: tenantDetails.name, @@ -874,27 +848,26 @@ module.exports = class tenantHelper { } } -async function tenantEventEmitter(tenantDetailsBeforeUpdate, updatedTenantDetails, bodyData) { - // compute changes from provided body keys - let changes = await utils.extractUpdatedValues(tenantDetailsBeforeUpdate, updatedTenantDetails, bodyData) +// tenant events +async function tenantEventEmitter(tenantBefore, tenantAfter, bodyData) { + // compute delta based on attempted update fields + const newValues = utils.extractDelta(tenantBefore, tenantAfter, bodyData) + + // nothing changed → no event + if (Object.keys(newValues).length === 0) return - //event Body for org updates - const eventBodyData = TenantDTO.eventBodyDTO({ + // deep-clone old to avoid later mutation bleed + const oldValues = _.cloneDeep(tenantBefore) + + const eventBodyData = { entity: 'tenant', eventType: 'update', - entityId: tenantDetailsBeforeUpdate.code, - changedValues: changes, - args: { - created_by: tenantDetailsBeforeUpdate.created_by, - name: tenantDetailsBeforeUpdate.name, - code: tenantDetailsBeforeUpdate.code, - created_at: tenantDetailsBeforeUpdate?.created_at || new Date(), - updated_at: updatedTenantDetails?.updated_at || new Date(), - status: tenantDetailsBeforeUpdate?.status || common.ACTIVE_STATUS, - meta: tenantDetailsBeforeUpdate?.meta || {}, - deleted: false, - description: tenantDetailsBeforeUpdate.description, - }, - }) + entityId: tenantBefore.code, + oldValues, + newValues, + created_at: tenantBefore?.created_at || new Date(), + updated_at: tenantAfter?.updated_at || new Date(), + } + broadcastEvent('tenantEvents', { requestBody: eventBodyData, isInternal: true }) } From 65b42620d22f259241ca0f130e40b9996c24ceb8 Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Tue, 7 Oct 2025 16:57:58 +0530 Subject: [PATCH 06/32] Refactor eventBodyDTO to use oldValues and newValues for tenant updates; update tenantQueries to fetch raw data --- src/dtos/tenantDTO.js | 12 +++--------- src/services/tenant.js | 24 ++++++++++++++---------- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/src/dtos/tenantDTO.js b/src/dtos/tenantDTO.js index da37b8ae0..f9b85e748 100644 --- a/src/dtos/tenantDTO.js +++ b/src/dtos/tenantDTO.js @@ -20,7 +20,7 @@ class TenantResponseDTO { } } - static eventBodyDTO({ entity, eventType, entityId, changedValues = [], args = {} }) { + static eventBodyDTO({ entity, eventType, entityId, oldValues, newValues, args = {} }) { try { if (!entity || !eventType || !entityId) throw new Error('Entity, EventType & EntityId values are mandatory for an Event') @@ -31,18 +31,12 @@ class TenantResponseDTO { delete args[key] }) - const changes = changedValues.reduce((result, obj) => { - const { fieldName, oldValue, newValue } = obj - if (!result[fieldName]) result[fieldName] = {} - if (oldValue) result[fieldName].oldValue = oldValue - if (newValue) result[fieldName].newValue = newValue - return result - }, {}) return { entity, eventType, entityId, - changes, + ...(oldValues ? { oldValues } : {}), + ...(newValues ? { newValues } : {}), ...args, } } catch (error) { diff --git a/src/services/tenant.js b/src/services/tenant.js index 0cfe0d05c..eb7203e4d 100644 --- a/src/services/tenant.js +++ b/src/services/tenant.js @@ -483,9 +483,7 @@ module.exports = class tenantHelper { static async addDomain(tenantCode, domains) { try { // fetch tenant details - const tenantDetails = await tenantQueries.findOne({ - code: tenantCode, - }) + const tenantDetails = await tenantQueries.findOne({ code: tenantCode }, { raw: true }) if (!tenantDetails?.code) { return responses.failureResponse({ @@ -541,13 +539,14 @@ module.exports = class tenantHelper { result: {}, }) } + tenantDetails.domains = existingDomains const eventBodyData = TenantDTO.eventBodyDTO({ entity: 'tenant', eventType: 'update', entityId: tenantDetails.code, - oldValue: existingDomains, - newValue: [...existingDomains, ...domainsToCreate], + oldValues: tenantDetails, + newValues: { domains: [...existingDomains, ...domainsToCreate] }, args: { created_by: tenantDetails.created_by, name: tenantDetails.name, @@ -586,9 +585,12 @@ module.exports = class tenantHelper { static async removeDomain(tenantCode, domains) { try { // fetch tenant details - const tenantDetails = await tenantQueries.findOne({ - code: tenantCode, - }) + const tenantDetails = await tenantQueries.findOne( + { + code: tenantCode, + }, + { raw: true } + ) if (!tenantDetails?.code) { return responses.failureResponse({ @@ -675,12 +677,14 @@ module.exports = class tenantHelper { await Promise.all(domainRemovePromise) + tenantDetails.domains = existingDomains + const eventBodyData = TenantDTO.eventBodyDTO({ entity: 'tenant', eventType: 'update', entityId: tenantDetails.code, - oldValue: existingDomains, - newValue: existingDomains.filter((d) => !domainsToRemove.includes(d)), + oldValues: tenantDetails, + newValues: { domains: existingDomains.filter((d) => !domainsToRemove.includes(d)) }, args: { created_by: tenantDetails.created_by, name: tenantDetails.name, From c4cde5bf669cf6953e3744e0399419f0babce266 Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Wed, 8 Oct 2025 00:06:22 +0530 Subject: [PATCH 07/32] Refactor eventBodyDTO to use oldValues and newValues; update updateRoleForApprovedRequest to broadcast user update events --- src/dtos/userDTO.js | 24 ++++++------ src/generics/utils.js | 5 ++- src/services/org-admin.js | 82 +++++++++++++++++++++++---------------- 3 files changed, 63 insertions(+), 48 deletions(-) diff --git a/src/dtos/userDTO.js b/src/dtos/userDTO.js index d04b398b5..cb1f88f41 100644 --- a/src/dtos/userDTO.js +++ b/src/dtos/userDTO.js @@ -77,7 +77,8 @@ class UserDTO { ] return input.filter((key) => !restrictedKeys.includes(key)) } - static eventBodyDTO({ entity, eventType, entityId, changedValues = [], args = {} }) { + + static eventBodyDTO({ entity, eventType, entityId, oldValues = {}, newValues = {}, args = {} }) { try { if (!entity || !eventType || !entityId) throw new Error('Entity, EventType & EntityId values are mandatory for an Event') @@ -91,23 +92,20 @@ class UserDTO { 'custom_entity_text', ] - disallowedArgs.forEach((key) => { - delete args[key] - }) + const sanitize = (obj) => { + if (obj && typeof obj === 'object') { + disallowedArgs.forEach((key) => delete obj[key]) + } + return obj + } - const changes = changedValues.reduce((result, obj) => { - const { fieldName, oldValue, newValue } = obj - if (!result[fieldName]) result[fieldName] = {} - if (oldValue) result[fieldName].oldValue = oldValue - if (newValue) result[fieldName].newValue = newValue - return result - }, {}) return { entity, eventType, entityId, - changes, - ...args, + ...(Object.keys(sanitize(oldValues)).length ? { oldValues } : {}), + ...(Object.keys(sanitize(newValues)).length ? { newValues } : {}), + ...sanitize(args), } } catch (error) { console.error(error) diff --git a/src/generics/utils.js b/src/generics/utils.js index 7d4cbaa9d..e383ae26e 100644 --- a/src/generics/utils.js +++ b/src/generics/utils.js @@ -1122,9 +1122,10 @@ function flattenLeafPaths(obj, prefix = '') { return out } -function extractDelta(oldData = {}, newData = {}, updateData = {}) { +function extractDelta(oldData = {}, newData = {}, updateData) { const delta = {} - const paths = flattenLeafPaths(updateData) // only compare fields the client attempted to update + const paths = + updateData && Object.keys(updateData).length ? flattenLeafPaths(updateData) : flattenLeafPaths(newData) // fallback to full compare for (const p of paths) { const oldVal = _.get(oldData, p) diff --git a/src/services/org-admin.js b/src/services/org-admin.js index 24f862f75..bcd53dd82 100644 --- a/src/services/org-admin.js +++ b/src/services/org-admin.js @@ -28,6 +28,8 @@ const emailEncryption = require('@utils/emailEncryption') const responses = require('@helpers/responses') const notificationUtils = require('@utils/notification') const userHelper = require('@helpers/userHelper') +const { broadcastEvent } = require('@helpers/eventBroadcasterMain') +const { eventBodyDTO } = require('@dtos/userDTO') module.exports = class OrgAdminHelper { /** @@ -628,42 +630,56 @@ module.exports = class OrgAdminHelper { } } -function updateRoleForApprovedRequest(requestDetails, user, tenantCode, orgCode) { - return new Promise(async (resolve, reject) => { - try { - const newRole = await roleQueries.findOne( - { id: requestDetails.role, status: common.ACTIVE_STATUS, tenant_code: tenantCode }, - { attributes: ['title', 'id', 'user_type', 'status'] } - ) +async function updateRoleForApprovedRequest(requestDetails, user, tenantCode, orgCode) { + try { + const newRole = await roleQueries.findOne( + { id: requestDetails.role, status: common.ACTIVE_STATUS, tenant_code: tenantCode }, + { attributes: ['title', 'id', 'user_type', 'status'] } + ) - await userOrganizationRoleQueries.create({ - tenant_code: tenantCode, - user_id: user.id, - organization_code: orgCode, - role_id: newRole.id, - }) + if (!newRole) { + throw new Error('ROLE_NOT_FOUND') + } - eventBroadcaster('roleChange', { - requestBody: { - user_id: requestDetails.requester_id, - new_roles: [newRole.title], - current_roles: _.map(_.find(user.organizations, { code: orgCode })?.roles || [], 'title'), - tenant_code: tenantCode, - organization_code: orgCode, - }, - }) - //delete from cache - const redisUserKey = `${common.redisUserPrefix}${tenantCode}_${requestDetails.requester_id.toString()}` - await utils.redisDel(redisUserKey) + await userOrganizationRoleQueries.create({ + tenant_code: tenantCode, + user_id: user.id, + organization_code: orgCode, + role_id: newRole.id, + }) - return resolve({ - success: true, - }) - } catch (error) { - console.log(error, 'error') - return error - } - }) + const updatedUser = await userQueries.findUserWithOrganization({ + id: requestDetails.requester_id, + tenant_code: tenantCode, + }) + + const newValues = utils.extractDelta(user, updatedUser) + + // nothing changed → no event + if (Object.keys(newValues).length === 0) return + + const eventBody = eventBodyDTO({ + entity: 'user', + eventType: 'update', + entityId: requestDetails.requester_id, + oldValues: user, + newValues: newValues, + args: { + created_at: updatedUser.created_at, + updated_at: updatedUser.updated_at, + }, + }) + + broadcastEvent('userEvents', { requestBody: eventBody, isInternal: true }) + + const redisUserKey = `${common.redisUserPrefix}${tenantCode}_${requestDetails.requester_id.toString()}` + await utils.redisDel(redisUserKey) + + return { success: true } + } catch (error) { + console.log(error, 'error') + throw error + } } async function sendRoleRequestStatusEmail(userDetails, status, organizationCode, tenantCode) { From 1503ff2579c53a04fd2552ab454bd1dceda6a24b Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Wed, 8 Oct 2025 00:22:57 +0530 Subject: [PATCH 08/32] Add error handling for tenant update failures in update method --- src/services/tenant.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/services/tenant.js b/src/services/tenant.js index eb7203e4d..e2dd11f19 100644 --- a/src/services/tenant.js +++ b/src/services/tenant.js @@ -456,9 +456,17 @@ module.exports = class tenantHelper { }, bodyData ) + + if (!updatedRows || updatedRows.length === 0) { + console.error('Update query did not return updated tenant data') + return responses.failureResponse({ + statusCode: httpStatusCode.internal_server_error, + message: 'TENANT_UPDATE_FAILED', + }) + } + let updatedTenantDetails = updatedRows?.[0] - console.log({ updatedTenantDetails }) await tenantEventEmitter(tenantDetails, updatedTenantDetails, bodyData) return responses.successResponse({ From 2b1d7c07a994d6373f2054c772977d88342e6d0a Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Thu, 9 Oct 2025 20:01:30 +0530 Subject: [PATCH 09/32] feat(auth): allow admin role to override tenant and org --- src/middlewares/authenticator.js | 55 +++++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/src/middlewares/authenticator.js b/src/middlewares/authenticator.js index 5ce40ccc9..ab4a494d0 100644 --- a/src/middlewares/authenticator.js +++ b/src/middlewares/authenticator.js @@ -10,7 +10,7 @@ const jwt = require('jsonwebtoken') const httpStatusCode = require('@generics/http-status') const common = require('@constants/common') const userQueries = require('@database/queries/users') -const roleQueries = require('@database/queries/user-role') + const rolePermissionMappingQueries = require('@database/queries/role-permission-mapping') const { Op } = require('sequelize') const responses = require('@helpers/responses') @@ -159,6 +159,7 @@ module.exports = async function (req, res, next) { token = extractedToken.trim() } else token = authHeader.trim() + let decodedToken let org try { decodedToken = jwt.verify(token, process.env.ACCESS_TOKEN_SECRET) @@ -210,10 +211,56 @@ module.exports = async function (req, res, next) { let isAdmin = false if (decodedToken.data.roles) { isAdmin = decodedToken.data.roles.some((role) => role.title == common.ADMIN_ROLE) - if (isAdmin) { - req.decodedToken = decodedToken.data - return next() + } + + if (isAdmin) { + // For admin users, allow overriding tenant_code, organization_id, and organization_code via headers + // Header names are configurable via environment variables with sensible defaults + const orgIdHeaderName = process.env.ORG_ID_HEADER_NAME + const orgCodeHeaderName = process.env.ORG_CODE_HEADER_NAME + const tenantCodeHeaderName = process.env.TENANT_CODE_HEADER_NAME + + // Extract and sanitize header values (trim whitespace, case-insensitive header lookup) + const orgId = (req.headers[orgIdHeaderName.toLowerCase()] || '').trim() + const orgCode = (req.headers[orgCodeHeaderName.toLowerCase()] || '').trim() + const tenantCode = (req.headers[tenantCodeHeaderName.toLowerCase()] || '').trim() + + // If any override header is provided (non-empty after trim), all three must be present and non-empty + const hasAnyOverrideHeader = orgId || orgCode || tenantCode + if (hasAnyOverrideHeader) { + if (!orgId || !orgCode || !tenantCode) { + throw responses.failureResponse({ + message: { + key: 'ADD_ORG_HEADER', + interpolation: { + orgIdHeader: orgIdHeaderName, + orgCodeHeader: orgCodeHeaderName, + tenantCodeHeader: tenantCodeHeaderName, + }, + }, + statusCode: httpStatusCode.bad_request, + responseCode: 'CLIENT_ERROR', + }) + } + + // Validate orgId is a valid positive integer + const parsedOrgId = parseInt(orgId, 10) + if (isNaN(parsedOrgId) || parsedOrgId <= 0) { + throw responses.failureResponse({ + message: 'INVALID_ORG_ID', + statusCode: httpStatusCode.bad_request, + responseCode: 'CLIENT_ERROR', + }) + } + + // Override the values from the token with sanitized header values + decodedToken.data.tenant_code = tenantCode + decodedToken.data.organization_id = orgId + decodedToken.data.organization_code = orgCode } + + req.decodedToken = decodedToken.data + //return next() } if (roleValidation) { From 738816541e2022a5aafef770de3c0af177b804b3 Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Thu, 9 Oct 2025 20:01:50 +0530 Subject: [PATCH 10/32] feat(env): add optional headers for ORG_ID, ORG_CODE, and TENANT_CODE --- src/envVariables.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/envVariables.js b/src/envVariables.js index e640e1c0a..8be773a91 100644 --- a/src/envVariables.js +++ b/src/envVariables.js @@ -458,6 +458,21 @@ let enviromentVariables = { value: 'true', }, }, + ORG_ID_HEADER_NAME: { + message: 'Required ORG_ID_HEADER_NAME', + optional: true, + default: 'X-Org-Id', + }, + ORG_CODE_HEADER_NAME: { + message: 'Required ORG_CODE_HEADER_NAME', + optional: true, + default: 'X-Org-Code', + }, + TENANT_CODE_HEADER_NAME: { + message: 'Required TENANT_CODE_HEADER_NAME', + optional: true, + default: 'X-Tenant-Code', + }, } let success = true From 490a558b76f72d9427fb9e965c8f630add1de098 Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Thu, 9 Oct 2025 20:02:39 +0530 Subject: [PATCH 11/32] feat(locales): add organization header messages and validation for org ID --- src/locales/en.json | 4 +++- src/middlewares/authenticator.js | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/locales/en.json b/src/locales/en.json index 64d5f726a..0aca71153 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -182,5 +182,7 @@ "REG_CODE_ERROR": "registration_code is not valid or unique.", "INVALID_REG_CODE_ERROR": "registration_codes {{errorMessage}}. Invalid Code(s) : {{errorValues}}", "UNIQUE_CONSTRAINT_ERROR": "{{fields}} is Invalid.", - "USER_PROFILE_FETCHED_SUCCESSFULLY": "User profile fetched successfully!" + "USER_PROFILE_FETCHED_SUCCESSFULLY": "User profile fetched successfully!", + "ADD_ORG_HEADER": "Please provide all required organization headers: {orgIdHeader}, {orgCodeHeader}, and {tenantCodeHeader} for admin override.", + "INVALID_ORG_ID": "Organization ID must be a valid positive integer." } diff --git a/src/middlewares/authenticator.js b/src/middlewares/authenticator.js index ab4a494d0..cd0a95024 100644 --- a/src/middlewares/authenticator.js +++ b/src/middlewares/authenticator.js @@ -260,7 +260,6 @@ module.exports = async function (req, res, next) { } req.decodedToken = decodedToken.data - //return next() } if (roleValidation) { From c09c7187176d0c84400f1b5eda1f56430a1c75d1 Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Thu, 9 Oct 2025 20:13:26 +0530 Subject: [PATCH 12/32] fix(locales): correct placeholder syntax in organization header message --- src/locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/locales/en.json b/src/locales/en.json index 0aca71153..4929b788e 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -183,6 +183,6 @@ "INVALID_REG_CODE_ERROR": "registration_codes {{errorMessage}}. Invalid Code(s) : {{errorValues}}", "UNIQUE_CONSTRAINT_ERROR": "{{fields}} is Invalid.", "USER_PROFILE_FETCHED_SUCCESSFULLY": "User profile fetched successfully!", - "ADD_ORG_HEADER": "Please provide all required organization headers: {orgIdHeader}, {orgCodeHeader}, and {tenantCodeHeader} for admin override.", + "ADD_ORG_HEADER": "Please provide all required organization headers: {{orgIdHeader}}, {{orgCodeHeader}}, and {{tenantCodeHeader}} for admin override.", "INVALID_ORG_ID": "Organization ID must be a valid positive integer." } From 65f352b359c3b19669cdda7b62367a6b1f06f1af Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Thu, 9 Oct 2025 20:20:16 +0530 Subject: [PATCH 13/32] feat(constants): update ORG_CODE_HEADER and TENANT_CODE_HEADER to use environment variables --- src/constants/common.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/constants/common.js b/src/constants/common.js index 37791b56d..64d83dbd7 100644 --- a/src/constants/common.js +++ b/src/constants/common.js @@ -102,8 +102,8 @@ module.exports = { READ_ACCESS: 'r', TYPE_ALL: 'all', ENGLISH_LANGUGE_CODE: 'en', - ORG_CODE_HEADER: 'organizationcode', - TENANT_CODE_HEADER: 'tenantcode', + ORG_CODE_HEADER: process.env.ORG_CODE_HEADER_NAME, + TENANT_CODE_HEADER: process.env.TENANT_CODE_HEADER_NAME, DELETE_METHOD: 'DELETE', SEQUELIZE_FOREIGN_KEY_CONSTRAINT_ERROR: 'SequelizeForeignKeyConstraintError', BULK_INVITATION_VALIDITY: '604800000', //SET to one week by default if not set by tenant (In Sec), From e72da9e7c9edfe406b63c3dd91e6425492af0768 Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Thu, 9 Oct 2025 20:23:21 +0530 Subject: [PATCH 14/32] fix(auth): replace organization_id with parsedOrgId in token decoding --- src/middlewares/authenticator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/middlewares/authenticator.js b/src/middlewares/authenticator.js index cd0a95024..c64064e32 100644 --- a/src/middlewares/authenticator.js +++ b/src/middlewares/authenticator.js @@ -255,7 +255,7 @@ module.exports = async function (req, res, next) { // Override the values from the token with sanitized header values decodedToken.data.tenant_code = tenantCode - decodedToken.data.organization_id = orgId + decodedToken.data.organization_id = parsedOrgId decodedToken.data.organization_code = orgCode } From 4095b0f743dc0e003c156cb7e06dbfee6fb1ec42 Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Fri, 10 Oct 2025 00:29:01 +0530 Subject: [PATCH 15/32] fix(auth): use strict equality for admin role check --- src/middlewares/authenticator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/middlewares/authenticator.js b/src/middlewares/authenticator.js index c64064e32..8b119db09 100644 --- a/src/middlewares/authenticator.js +++ b/src/middlewares/authenticator.js @@ -210,7 +210,7 @@ module.exports = async function (req, res, next) { //check for admin user let isAdmin = false if (decodedToken.data.roles) { - isAdmin = decodedToken.data.roles.some((role) => role.title == common.ADMIN_ROLE) + isAdmin = decodedToken.data.roles.some((role) => role.title === common.ADMIN_ROLE) } if (isAdmin) { From 43dde738d058d5edef04ce0c51f3952ee0d86713 Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Fri, 10 Oct 2025 00:30:39 +0530 Subject: [PATCH 16/32] fix(auth): remove redundant assignment of decodedToken to request object --- src/middlewares/authenticator.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/middlewares/authenticator.js b/src/middlewares/authenticator.js index 8b119db09..ca9be5b77 100644 --- a/src/middlewares/authenticator.js +++ b/src/middlewares/authenticator.js @@ -258,8 +258,6 @@ module.exports = async function (req, res, next) { decodedToken.data.organization_id = parsedOrgId decodedToken.data.organization_code = orgCode } - - req.decodedToken = decodedToken.data } if (roleValidation) { From d909cf2e0507ff0394dfd58920ed4fad5865a63f Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Tue, 14 Oct 2025 01:42:32 +0530 Subject: [PATCH 17/32] refactor(headers): use env header names and lowercase defaults --- src/controllers/v1/admin.js | 4 ++-- src/controllers/v1/form.js | 2 +- src/controllers/v1/public.js | 2 +- src/controllers/v1/tenant.js | 4 ++-- src/envVariables.js | 6 +++--- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/controllers/v1/admin.js b/src/controllers/v1/admin.js index b18b4c3ff..45ca01839 100644 --- a/src/controllers/v1/admin.js +++ b/src/controllers/v1/admin.js @@ -110,7 +110,7 @@ module.exports = class Admin { req.body.organization_id, req.decodedToken.id, req.body?.identifier, - req.headers?.['tenant-id'], + req.headers?.[process.env.TENANT_CODE_HEADER_NAME], req.body?.phone_code ) return orgAdminCreation @@ -154,7 +154,7 @@ module.exports = class Admin { const result = await adminService.deactivateOrg( req.params.id, - req.headers?.['tenant-id'], + req.headers?.[process.env.TENANT_CODE_HEADER_NAME], req.decodedToken.id ) return result diff --git a/src/controllers/v1/form.js b/src/controllers/v1/form.js index c0c0ba0e7..3bcf4b9f7 100644 --- a/src/controllers/v1/form.js +++ b/src/controllers/v1/form.js @@ -100,7 +100,7 @@ module.exports = class Form { req.params.id, params, req?.decodedToken?.organization_id || null, - req?.decodedToken?.tenant_code || req?.headers?.tenant_code || null, + req?.decodedToken?.tenant_code || req?.headers?.[process.env.TENANT_CODE_HEADER_NAME] || null, domain ) return form diff --git a/src/controllers/v1/public.js b/src/controllers/v1/public.js index adf53139e..64445a730 100644 --- a/src/controllers/v1/public.js +++ b/src/controllers/v1/public.js @@ -4,7 +4,7 @@ const { getDomainFromRequest } = require('@utils/domain') module.exports = class Public { async branding(req) { let domain = '' - let tenantCode = req?.headers?.tenantid || null + let tenantCode = req?.headers?.[process.env.TENANT_CODE_HEADER_NAME] || null if (!tenantCode) { domain = getDomainFromRequest(req) } diff --git a/src/controllers/v1/tenant.js b/src/controllers/v1/tenant.js index 8f9822bcc..88e2666ee 100644 --- a/src/controllers/v1/tenant.js +++ b/src/controllers/v1/tenant.js @@ -146,8 +146,8 @@ module.exports = class Tenant { const tenant = await tenantService.userBulkUpload( req.body.file_path, req.decodedToken.id, - req.headers.organization, - req.headers.tenant, + req.headers?.[process.env.ORG_CODE_HEADER_NAME], + req.headers?.[process.env.TENANT_CODE_HEADER_NAME], req?.body?.editable_fields, req?.body?.upload_type.toUpperCase() ) diff --git a/src/envVariables.js b/src/envVariables.js index 8be773a91..692b6dba2 100644 --- a/src/envVariables.js +++ b/src/envVariables.js @@ -461,17 +461,17 @@ let enviromentVariables = { ORG_ID_HEADER_NAME: { message: 'Required ORG_ID_HEADER_NAME', optional: true, - default: 'X-Org-Id', + default: 'x-org-id', }, ORG_CODE_HEADER_NAME: { message: 'Required ORG_CODE_HEADER_NAME', optional: true, - default: 'X-Org-Code', + default: 'x-org-code', }, TENANT_CODE_HEADER_NAME: { message: 'Required TENANT_CODE_HEADER_NAME', optional: true, - default: 'X-Tenant-Code', + default: 'x-tenant-code', }, } let success = true From 15ad6de68b3880dbf27f5973ddfda33e84f5318e Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Tue, 14 Oct 2025 02:10:47 +0530 Subject: [PATCH 18/32] feat(admin): add raw SELECT query execution with pagination --- src/controllers/v1/admin.js | 31 +++++++++++++++++ src/locales/en.json | 6 +++- src/services/admin.js | 67 +++++++++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 1 deletion(-) diff --git a/src/controllers/v1/admin.js b/src/controllers/v1/admin.js index b18b4c3ff..27d3575a7 100644 --- a/src/controllers/v1/admin.js +++ b/src/controllers/v1/admin.js @@ -196,6 +196,37 @@ module.exports = class Admin { } } + /** + * Execute raw SELECT query with pagination + * @method + * @name executeRawQuery + * @param {Object} req - Request object containing query in body and pagination params + * @param {String} req.body.query - Raw SQL SELECT query + * @param {Number} req.pageNo - Page number + * @param {Number} req.pageSize - Page size limit + * @returns {JSON} - Paginated query results + */ + async executeRawQuery(req) { + try { + // Validate admin role + if (!utilsHelper.validateRoleAccess(req.decodedToken.roles, common.ADMIN_ROLE)) { + return responses.failureResponse({ + message: 'USER_IS_NOT_A_ADMIN', + statusCode: httpStatusCode.bad_request, + responseCode: 'CLIENT_ERROR', + }) + } + + const { query } = req.body + + const pageNo = req.pageNo + const pageSize = req.pageSize + + return await adminService.executeRawQuery(query, req.decodedToken.id, pageNo, pageSize) + } catch (error) { + return error + } + } async triggerViewRebuild(req) { try { if (!req.decodedToken.roles.some((role) => role.title === common.ADMIN_ROLE)) { diff --git a/src/locales/en.json b/src/locales/en.json index 64d5f726a..2f8060620 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -182,5 +182,9 @@ "REG_CODE_ERROR": "registration_code is not valid or unique.", "INVALID_REG_CODE_ERROR": "registration_codes {{errorMessage}}. Invalid Code(s) : {{errorValues}}", "UNIQUE_CONSTRAINT_ERROR": "{{fields}} is Invalid.", - "USER_PROFILE_FETCHED_SUCCESSFULLY": "User profile fetched successfully!" + "USER_PROFILE_FETCHED_SUCCESSFULLY": "User profile fetched successfully!", + "RAW_QUERY_EXECUTED_SUCCESSFULLY": "Raw query executed successfully!", + "INVALID_QUERY_INPUT": "Invalid query input provided.", + "ONLY_SELECT_QUERIES_ALLOWED": "Only SELECT queries are allowed.", + "QUERY_EXECUTION_FAILED": "Failed to execute query." } diff --git a/src/services/admin.js b/src/services/admin.js index 8cf4fe19b..411a0a55d 100644 --- a/src/services/admin.js +++ b/src/services/admin.js @@ -816,6 +816,73 @@ module.exports = class AdminHelper { throw error } } + /** + * Execute raw SELECT query with pagination + * @method + * @name executeRawQuery + * @param {String} query - Raw SQL SELECT query + * @param {String} adminUserId - ID of the admin executing the query + * @param {Number} pageNo - Page number + * @param {Number} pageSize - Page size limit + * @returns {JSON} - Paginated query results with count + */ + static async executeRawQuery(query, adminUserId, pageNo, pageSize) { + try { + if (!query || typeof query !== 'string') { + return responses.failureResponse({ + message: 'INVALID_QUERY_INPUT', + statusCode: httpStatusCode.bad_request, + responseCode: 'CLIENT_ERROR', + }) + } + // Validate that the query is a SELECT statement + const normalizedQuery = query.trim().toLowerCase() + if (!normalizedQuery.startsWith('select')) { + return responses.failureResponse({ + message: 'ONLY_SELECT_QUERIES_ALLOWED', + statusCode: httpStatusCode.bad_request, + responseCode: 'CLIENT_ERROR', + }) + } + + // Log the query for auditing + console.log(`Admin ${adminUserId} executed query: ${query} (Page: ${pageNo}, Size: ${pageSize})`) + + // Calculate offset + const offset = (pageNo - 1) * pageSize + + // Modify query for pagination + const paginatedQuery = `${query} LIMIT ${pageSize} OFFSET ${offset}` + + // Execute the paginated query + const data = await sequelize.query(paginatedQuery, { + type: sequelize.QueryTypes.SELECT, + }) + // Get total count (construct COUNT query) + const countQuery = `SELECT COUNT(*) as count FROM (${query}) AS subquery` + const [countResult] = await sequelize.query(countQuery, { + type: sequelize.QueryTypes.SELECT, + }) + const count = countResult.count + + return responses.successResponse({ + statusCode: httpStatusCode.ok, + message: 'QUERY_EXECUTED_SUCCESSFULLY', + result: { + data, + count, + }, + }) + } catch (error) { + console.error('Error executing raw query:', error) + return responses.failureResponse({ + message: 'QUERY_EXECUTION_FAILED', + statusCode: httpStatusCode.internal_server_error, + responseCode: 'SERVER_ERROR', + error: error.message, + }) + } + } static async triggerViewRebuild(decodedToken) { try { From 1d8fbae155a9f82be2a367f8fc0e2a86f6a011ed Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Tue, 14 Oct 2025 02:22:19 +0530 Subject: [PATCH 19/32] refactor(headers): use common constants for header names --- src/constants/common.js | 5 +++-- src/controllers/v1/admin.js | 4 ++-- src/controllers/v1/form.js | 3 ++- src/controllers/v1/public.js | 3 ++- src/controllers/v1/tenant.js | 4 ++-- 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/constants/common.js b/src/constants/common.js index 64d83dbd7..68580fbe8 100644 --- a/src/constants/common.js +++ b/src/constants/common.js @@ -102,8 +102,9 @@ module.exports = { READ_ACCESS: 'r', TYPE_ALL: 'all', ENGLISH_LANGUGE_CODE: 'en', - ORG_CODE_HEADER: process.env.ORG_CODE_HEADER_NAME, - TENANT_CODE_HEADER: process.env.TENANT_CODE_HEADER_NAME, + ORG_CODE_HEADER: process.env.ORG_CODE_HEADER_NAME.toLowerCase(), + ORG_ID_HEADER: process.env.ORG_ID_HEADER_NAME.toLowerCase(), + TENANT_CODE_HEADER: process.env.TENANT_CODE_HEADER_NAME.toLowerCase(), DELETE_METHOD: 'DELETE', SEQUELIZE_FOREIGN_KEY_CONSTRAINT_ERROR: 'SequelizeForeignKeyConstraintError', BULK_INVITATION_VALIDITY: '604800000', //SET to one week by default if not set by tenant (In Sec), diff --git a/src/controllers/v1/admin.js b/src/controllers/v1/admin.js index 45ca01839..6ca70e088 100644 --- a/src/controllers/v1/admin.js +++ b/src/controllers/v1/admin.js @@ -110,7 +110,7 @@ module.exports = class Admin { req.body.organization_id, req.decodedToken.id, req.body?.identifier, - req.headers?.[process.env.TENANT_CODE_HEADER_NAME], + req.headers?.[common.TENANT_CODE_HEADER], req.body?.phone_code ) return orgAdminCreation @@ -154,7 +154,7 @@ module.exports = class Admin { const result = await adminService.deactivateOrg( req.params.id, - req.headers?.[process.env.TENANT_CODE_HEADER_NAME], + req.headers?.[common.TENANT_CODE_HEADER], req.decodedToken.id ) return result diff --git a/src/controllers/v1/form.js b/src/controllers/v1/form.js index 3bcf4b9f7..31becc9b3 100644 --- a/src/controllers/v1/form.js +++ b/src/controllers/v1/form.js @@ -7,6 +7,7 @@ // Dependencies const formsService = require('@services/form') +const common = require('@constants/common') module.exports = class Form { /** @@ -100,7 +101,7 @@ module.exports = class Form { req.params.id, params, req?.decodedToken?.organization_id || null, - req?.decodedToken?.tenant_code || req?.headers?.[process.env.TENANT_CODE_HEADER_NAME] || null, + req?.decodedToken?.tenant_code || req?.headers?.[common.TENANT_CODE_HEADER] || null, domain ) return form diff --git a/src/controllers/v1/public.js b/src/controllers/v1/public.js index 64445a730..5df953a7c 100644 --- a/src/controllers/v1/public.js +++ b/src/controllers/v1/public.js @@ -1,10 +1,11 @@ const publicService = require('@services/public') const { getDomainFromRequest } = require('@utils/domain') +const common = require('@constants/common') module.exports = class Public { async branding(req) { let domain = '' - let tenantCode = req?.headers?.[process.env.TENANT_CODE_HEADER_NAME] || null + let tenantCode = req?.headers?.[common.TENANT_CODE_HEADER] || null if (!tenantCode) { domain = getDomainFromRequest(req) } diff --git a/src/controllers/v1/tenant.js b/src/controllers/v1/tenant.js index 88e2666ee..0e628ed79 100644 --- a/src/controllers/v1/tenant.js +++ b/src/controllers/v1/tenant.js @@ -146,8 +146,8 @@ module.exports = class Tenant { const tenant = await tenantService.userBulkUpload( req.body.file_path, req.decodedToken.id, - req.headers?.[process.env.ORG_CODE_HEADER_NAME], - req.headers?.[process.env.TENANT_CODE_HEADER_NAME], + req.headers?.[common.ORG_CODE_HEADER], + req.headers?.[common.TENANT_CODE_HEADER], req?.body?.editable_fields, req?.body?.upload_type.toUpperCase() ) From 78168ee6c6f5c67a1c3f6e0f061bd19f93ca80a2 Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Tue, 14 Oct 2025 02:36:44 +0530 Subject: [PATCH 20/32] feat(auth): validate org and tenant code in authenticator --- src/locales/en.json | 3 ++- src/middlewares/authenticator.js | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/locales/en.json b/src/locales/en.json index 4929b788e..9eeb4e97e 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -184,5 +184,6 @@ "UNIQUE_CONSTRAINT_ERROR": "{{fields}} is Invalid.", "USER_PROFILE_FETCHED_SUCCESSFULLY": "User profile fetched successfully!", "ADD_ORG_HEADER": "Please provide all required organization headers: {{orgIdHeader}}, {{orgCodeHeader}}, and {{tenantCodeHeader}} for admin override.", - "INVALID_ORG_ID": "Organization ID must be a valid positive integer." + "INVALID_ORG_ID": "Organization ID must be a valid positive integer.", + "INVALID_ORG_OR_TENANT_CODE": "The provided organization or tenant code is invalid or does not match." } diff --git a/src/middlewares/authenticator.js b/src/middlewares/authenticator.js index ca9be5b77..44478a5db 100644 --- a/src/middlewares/authenticator.js +++ b/src/middlewares/authenticator.js @@ -18,6 +18,7 @@ const utilsHelper = require('@generics/utils') const { verifyCaptchaToken } = require('@utils/captcha') const { getDomainFromRequest } = require('@utils/domain') const tenantDomainQueries = require('@database/queries/tenantDomain') +const organizationQueries = require('@database/queries/organization') async function checkPermissions(roleTitle, requestPath, requestMethod) { const parts = requestPath.match(/[^/]+/g) @@ -252,7 +253,21 @@ module.exports = async function (req, res, next) { responseCode: 'CLIENT_ERROR', }) } + const org = await organizationQueries.findOne({ + id: parsedOrgId, + code: orgCode, + tenant_code: tenantCode, + status: common.ACTIVE_STATUS, + deleted_at: null, + }) + if (!org) { + throw responses.failureResponse({ + message: 'INVALID_ORG_OR_TENANT_CODE', + statusCode: httpStatusCode.bad_request, + responseCode: 'CLIENT_ERROR', + }) + } // Override the values from the token with sanitized header values decodedToken.data.tenant_code = tenantCode decodedToken.data.organization_id = parsedOrgId From 4299978183492fc7ab538506b711b509720bc144 Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Tue, 14 Oct 2025 16:56:55 +0530 Subject: [PATCH 21/32] refactor(auth): remove orgId header requirement for admin override --- src/locales/en.json | 2 +- src/middlewares/authenticator.js | 31 ++++++++++--------------------- 2 files changed, 11 insertions(+), 22 deletions(-) diff --git a/src/locales/en.json b/src/locales/en.json index 9eeb4e97e..9bb0110be 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -183,7 +183,7 @@ "INVALID_REG_CODE_ERROR": "registration_codes {{errorMessage}}. Invalid Code(s) : {{errorValues}}", "UNIQUE_CONSTRAINT_ERROR": "{{fields}} is Invalid.", "USER_PROFILE_FETCHED_SUCCESSFULLY": "User profile fetched successfully!", - "ADD_ORG_HEADER": "Please provide all required organization headers: {{orgIdHeader}}, {{orgCodeHeader}}, and {{tenantCodeHeader}} for admin override.", + "ADD_ORG_HEADER": "Please provide all required organization headers: {{orgCodeHeader}}, and {{tenantCodeHeader}} for admin override.", "INVALID_ORG_ID": "Organization ID must be a valid positive integer.", "INVALID_ORG_OR_TENANT_CODE": "The provided organization or tenant code is invalid or does not match." } diff --git a/src/middlewares/authenticator.js b/src/middlewares/authenticator.js index 44478a5db..5afd4dcba 100644 --- a/src/middlewares/authenticator.js +++ b/src/middlewares/authenticator.js @@ -215,26 +215,23 @@ module.exports = async function (req, res, next) { } if (isAdmin) { - // For admin users, allow overriding tenant_code, organization_id, and organization_code via headers + // For admin users, allow overriding tenant_code and organization_code via headers // Header names are configurable via environment variables with sensible defaults - const orgIdHeaderName = process.env.ORG_ID_HEADER_NAME - const orgCodeHeaderName = process.env.ORG_CODE_HEADER_NAME - const tenantCodeHeaderName = process.env.TENANT_CODE_HEADER_NAME + const orgCodeHeaderName = common.ORG_CODE_HEADER + const tenantCodeHeaderName = common.TENANT_CODE_HEADER // Extract and sanitize header values (trim whitespace, case-insensitive header lookup) - const orgId = (req.headers[orgIdHeaderName.toLowerCase()] || '').trim() const orgCode = (req.headers[orgCodeHeaderName.toLowerCase()] || '').trim() const tenantCode = (req.headers[tenantCodeHeaderName.toLowerCase()] || '').trim() - // If any override header is provided (non-empty after trim), all three must be present and non-empty - const hasAnyOverrideHeader = orgId || orgCode || tenantCode + // If any override header is provided (non-empty after trim), both must be present and non-empty + const hasAnyOverrideHeader = orgCode || tenantCode if (hasAnyOverrideHeader) { - if (!orgId || !orgCode || !tenantCode) { + if (!orgCode || !tenantCode) { throw responses.failureResponse({ message: { key: 'ADD_ORG_HEADER', interpolation: { - orgIdHeader: orgIdHeaderName, orgCodeHeader: orgCodeHeaderName, tenantCodeHeader: tenantCodeHeaderName, }, @@ -244,17 +241,8 @@ module.exports = async function (req, res, next) { }) } - // Validate orgId is a valid positive integer - const parsedOrgId = parseInt(orgId, 10) - if (isNaN(parsedOrgId) || parsedOrgId <= 0) { - throw responses.failureResponse({ - message: 'INVALID_ORG_ID', - statusCode: httpStatusCode.bad_request, - responseCode: 'CLIENT_ERROR', - }) - } + // Query the database to find the organization based on orgCode and tenantCode const org = await organizationQueries.findOne({ - id: parsedOrgId, code: orgCode, tenant_code: tenantCode, status: common.ACTIVE_STATUS, @@ -268,9 +256,10 @@ module.exports = async function (req, res, next) { responseCode: 'CLIENT_ERROR', }) } - // Override the values from the token with sanitized header values + + // Override the values from the token with sanitized header values and fetched orgId decodedToken.data.tenant_code = tenantCode - decodedToken.data.organization_id = parsedOrgId + decodedToken.data.organization_id = org.id // Use the ID from the database decodedToken.data.organization_code = orgCode } } From 091cf7b9d27e239169e88e2f957b79b2271ce777 Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Tue, 14 Oct 2025 18:50:19 +0530 Subject: [PATCH 22/32] updated: response message --- src/locales/en.json | 2 +- src/services/admin.js | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/locales/en.json b/src/locales/en.json index 2f8060620..be1fe8ae6 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -183,7 +183,7 @@ "INVALID_REG_CODE_ERROR": "registration_codes {{errorMessage}}. Invalid Code(s) : {{errorValues}}", "UNIQUE_CONSTRAINT_ERROR": "{{fields}} is Invalid.", "USER_PROFILE_FETCHED_SUCCESSFULLY": "User profile fetched successfully!", - "RAW_QUERY_EXECUTED_SUCCESSFULLY": "Raw query executed successfully!", + "QUERY_EXECUTED_SUCCESSFULLY": "Raw query executed successfully!", "INVALID_QUERY_INPUT": "Invalid query input provided.", "ONLY_SELECT_QUERIES_ALLOWED": "Only SELECT queries are allowed.", "QUERY_EXECUTION_FAILED": "Failed to execute query." diff --git a/src/services/admin.js b/src/services/admin.js index 411a0a55d..fad6a0305 100644 --- a/src/services/admin.js +++ b/src/services/admin.js @@ -836,23 +836,27 @@ module.exports = class AdminHelper { }) } // Validate that the query is a SELECT statement - const normalizedQuery = query.trim().toLowerCase() - if (!normalizedQuery.startsWith('select')) { + const rawQuery = query.trim() + // Allow SELECT or WITH; block multi-statements and DDL/DML + const isSelectLike = /^\s*(with|select)\b/i.test(rawQuery) + const hasForbidden = /[;]|(--|\/\*)|\b(drop|alter|truncate|insert|update|delete)\b/i.test(rawQuery) + if (!isSelectLike || hasForbidden) { return responses.failureResponse({ message: 'ONLY_SELECT_QUERIES_ALLOWED', statusCode: httpStatusCode.bad_request, responseCode: 'CLIENT_ERROR', }) } + const sanitizedQuery = rawQuery.replace(/;+\s*$/, '') // Log the query for auditing - console.log(`Admin ${adminUserId} executed query: ${query} (Page: ${pageNo}, Size: ${pageSize})`) + console.log(`Admin ${adminUserId} executed query: ${sanitizedQuery} (Page: ${pageNo}, Size: ${pageSize})`) // Calculate offset const offset = (pageNo - 1) * pageSize // Modify query for pagination - const paginatedQuery = `${query} LIMIT ${pageSize} OFFSET ${offset}` + const paginatedQuery = `${sanitizedQuery} LIMIT ${pageSize} OFFSET ${offset}` // Execute the paginated query const data = await sequelize.query(paginatedQuery, { @@ -879,7 +883,6 @@ module.exports = class AdminHelper { message: 'QUERY_EXECUTION_FAILED', statusCode: httpStatusCode.internal_server_error, responseCode: 'SERVER_ERROR', - error: error.message, }) } } From 40ac766984d21bd1d7c619e35c2199d6113ba4a8 Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Wed, 15 Oct 2025 11:12:35 +0530 Subject: [PATCH 23/32] refactor(auth): simplify tenant filter construction logic --- src/middlewares/authenticator.js | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/middlewares/authenticator.js b/src/middlewares/authenticator.js index 5afd4dcba..a6daab704 100644 --- a/src/middlewares/authenticator.js +++ b/src/middlewares/authenticator.js @@ -111,18 +111,13 @@ module.exports = async function (req, res, next) { }) } - const domain = getDomainFromRequest(req) || null - const tenant_code = - req?.headers?.tenantId || - req?.headers?.tenantid || - req?.headers?.tenant_Id || - req?.headers?.tenant_id || - req?.headers?.tenant || - req?.headers?.tenant_code || - req.headers.tenantCode || - null - - const tenantFilter = domain ? { domain } : tenant_code ? { tenant_code } : null || {} + const tenantFilter = {} + const domain = getDomainFromRequest(req) + + const tenant_code = req?.headers?.[common.TENANT_CODE_HEADER] ?? null + + if (domain) tenantFilter.domain = domain + else if (tenant_code) tenantFilter.tenant_code = tenant_code if (Object.keys(tenantFilter).length > 0) { const tenantDomain = await tenantDomainQueries.findOne(tenantFilter, { From 68ce70bb339207e093b56830c751f446abe65b61 Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Wed, 15 Oct 2025 18:24:59 +0530 Subject: [PATCH 24/32] feat(admin): add rawQueryUtils for secure query validation --- src/services/admin.js | 49 +++++++-------- src/utils/rawQueryUtils.js | 125 +++++++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+), 25 deletions(-) create mode 100644 src/utils/rawQueryUtils.js diff --git a/src/services/admin.js b/src/services/admin.js index fad6a0305..5cfa5d7b7 100644 --- a/src/services/admin.js +++ b/src/services/admin.js @@ -15,6 +15,7 @@ const bcryptJs = require('bcryptjs') const common = require('@constants/common') const httpStatusCode = require('@generics/http-status') const utils = require('@generics/utils') +const rawQueryUtils = require('@utils/rawQueryUtils') // Database queries const organizationQueries = require('@database/queries/organization') @@ -828,6 +829,7 @@ module.exports = class AdminHelper { */ static async executeRawQuery(query, adminUserId, pageNo, pageSize) { try { + // Basic input validation if (!query || typeof query !== 'string') { return responses.failureResponse({ message: 'INVALID_QUERY_INPUT', @@ -835,39 +837,34 @@ module.exports = class AdminHelper { responseCode: 'CLIENT_ERROR', }) } - // Validate that the query is a SELECT statement - const rawQuery = query.trim() - // Allow SELECT or WITH; block multi-statements and DDL/DML - const isSelectLike = /^\s*(with|select)\b/i.test(rawQuery) - const hasForbidden = /[;]|(--|\/\*)|\b(drop|alter|truncate|insert|update|delete)\b/i.test(rawQuery) - if (!isSelectLike || hasForbidden) { - return responses.failureResponse({ - message: 'ONLY_SELECT_QUERIES_ALLOWED', - statusCode: httpStatusCode.bad_request, - responseCode: 'CLIENT_ERROR', - }) - } - const sanitizedQuery = rawQuery.replace(/;+\s*$/, '') + + // Trim and strip trailing semicolon(s) + let sanitizedQuery = query.trim().replace(/;+\s*$/, '') + + // Security validation + rawQueryUtils.validateQuerySecurity(sanitizedQuery) // Log the query for auditing console.log(`Admin ${adminUserId} executed query: ${sanitizedQuery} (Page: ${pageNo}, Size: ${pageSize})`) - // Calculate offset - const offset = (pageNo - 1) * pageSize + // Get pagination parameters + const { limit, offset } = rawQueryUtils.getPaginationParams(pageNo, pageSize) - // Modify query for pagination - const paginatedQuery = `${sanitizedQuery} LIMIT ${pageSize} OFFSET ${offset}` - - // Execute the paginated query + // Execute the paginated query with bindings + const paginatedQuery = `${sanitizedQuery} LIMIT :limit OFFSET :offset` const data = await sequelize.query(paginatedQuery, { + replacements: { limit, offset }, type: sequelize.QueryTypes.SELECT, + timeout: 30000, // Prevent long-running queries }) - // Get total count (construct COUNT query) - const countQuery = `SELECT COUNT(*) as count FROM (${query}) AS subquery` + + // Get total count (use sanitizedQuery as subquery) + const countQuery = `SELECT COUNT(*) AS count FROM (${sanitizedQuery}) AS subquery` const [countResult] = await sequelize.query(countQuery, { type: sequelize.QueryTypes.SELECT, + timeout: 30000, }) - const count = countResult.count + const count = Number(countResult.count) // Coerce to number return responses.successResponse({ statusCode: httpStatusCode.ok, @@ -880,9 +877,11 @@ module.exports = class AdminHelper { } catch (error) { console.error('Error executing raw query:', error) return responses.failureResponse({ - message: 'QUERY_EXECUTION_FAILED', - statusCode: httpStatusCode.internal_server_error, - responseCode: 'SERVER_ERROR', + message: error.message || 'QUERY_EXECUTION_FAILED', + statusCode: error.message.includes('QUERY_') + ? httpStatusCode.bad_request + : httpStatusCode.internal_server_error, + responseCode: error.message.includes('QUERY_') ? 'CLIENT_ERROR' : 'SERVER_ERROR', }) } } diff --git a/src/utils/rawQueryUtils.js b/src/utils/rawQueryUtils.js new file mode 100644 index 000000000..e19f4e802 --- /dev/null +++ b/src/utils/rawQueryUtils.js @@ -0,0 +1,125 @@ +/** + * Utilities for validating and processing raw SQL SELECT queries + * @module rawQueryUtils + */ + +/** + * Comprehensive security validation for SQL queries + * @param {string} query - The raw SQL query to validate + * @throws {Error} - If validation fails + */ +function validateQuerySecurity(query) { + const lowerQuery = query.toLowerCase() + + // Check starts with SELECT or WITH + if (!/^\s*(with|select)\b/.test(lowerQuery)) { + throw new Error('ONLY_SELECT_QUERIES_ALLOWED') + } + + // Check for unbalanced quotes + if (!hasValidQuotes(query)) { + throw new Error('QUERY_INVALID_OR_UNBALANCED_QUOTES') + } + + // Check for critical injection patterns + if (hasCriticalInjectionPatterns(lowerQuery)) { + throw new Error('QUERY_FORBIDDEN_INJECTION_PATTERNS') + } + + // Normalize and check for forbidden patterns + const normalizedQuery = normalizeQuery(query) + if (hasForbiddenPatterns(normalizedQuery)) { + throw new Error('QUERY_FORBIDDEN_PATTERNS') + } +} + +/** + * Calculates pagination parameters with validation + * @param {number} pageNo - Page number + * @param {number} pageSize - Page size + * @returns {Object} - { limit, offset } + */ +function getPaginationParams(pageNo, pageSize) { + const validPageNo = Math.max(1, parseInt(pageNo) || 1) + const validPageSize = Math.min(Math.max(1, parseInt(pageSize) || 100), 1000) // Cap at 1000 + const offset = (validPageNo - 1) * validPageSize + + return { limit: validPageSize, offset } +} + +/** + * Checks for critical injection patterns + * @param {string} lowerQuery - Lowercased query + * @returns {boolean} - True if critical patterns are found + */ +function hasCriticalInjectionPatterns(lowerQuery) { + const criticalPatterns = [';', '--', '/*', '*/', '#', '\\'] + return criticalPatterns.some((pattern) => lowerQuery.includes(pattern)) +} + +/** + * Removes comments and string literals for pattern analysis + * @param {string} query - The raw query + * @returns {string} - Normalized query + */ +function normalizeQuery(query) { + return query + .toLowerCase() + .replace(/\/\*[\s\S]*?\*\//g, ' ') // Remove /* */ comments + .replace(/--.*$/gm, ' ') // Remove -- comments + .replace(/#[^\n]*/g, ' ') // Remove # comments + .replace(/'(?:''|[^'])*'/g, ' ') // Remove single-quoted strings + .replace(/"(?:""|[^"])*"/g, ' ') // Remove double-quoted strings + .replace(/\s+/g, ' ') // Normalize whitespace + .trim() +} + +/** + * Checks if normalized query contains forbidden operations + * @param {string} normalizedQuery - Normalized query + * @returns {boolean} - True if forbidden patterns are found + */ +function hasForbiddenPatterns(normalizedQuery) { + const forbiddenVerbs = /\b(drop|alter|truncate|insert|update|delete)\b/i + return forbiddenVerbs.test(normalizedQuery) +} + +/** + * Validates balanced quotes with escaping + * @param {string} query - The raw query + * @returns {boolean} - True if quotes are balanced + */ +function hasValidQuotes(query) { + let inSingleQuote = false + let inDoubleQuote = false + + for (let i = 0; i < query.length; i++) { + const char = query[i] + const nextChar = query[i + 1] + + if (!inDoubleQuote && char === "'") { + if (inSingleQuote && nextChar === "'") { + i++ // Skip escaped '' + continue + } + inSingleQuote = !inSingleQuote + } else if (!inSingleQuote && char === '"') { + if (inDoubleQuote && nextChar === '"') { + i++ // Skip escaped "" + continue + } + inDoubleQuote = !inDoubleQuote + } + } + + return !inSingleQuote && !inDoubleQuote +} + +module.exports = { + validateQuerySecurity, + getPaginationParams, + hasCriticalInjectionPatterns, + normalizeQuery, + hasForbiddenPatterns, + hasValidQuotes, +} From d32a714c630ad354e6812542aa0a758cb4482e90 Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Wed, 15 Oct 2025 18:33:31 +0530 Subject: [PATCH 25/32] updated: hasForbiddenPatterns --- src/locales/en.json | 5 ++++- src/utils/rawQueryUtils.js | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/locales/en.json b/src/locales/en.json index be1fe8ae6..0370d7397 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -186,5 +186,8 @@ "QUERY_EXECUTED_SUCCESSFULLY": "Raw query executed successfully!", "INVALID_QUERY_INPUT": "Invalid query input provided.", "ONLY_SELECT_QUERIES_ALLOWED": "Only SELECT queries are allowed.", - "QUERY_EXECUTION_FAILED": "Failed to execute query." + "QUERY_EXECUTION_FAILED": "Failed to execute query.", + "QUERY_INVALID_OR_UNBALANCED_QUOTES": "Query contains unbalanced or invalid quotes.", + "QUERY_FORBIDDEN_INJECTION_PATTERNS": "Query contains forbidden injection patterns.", + "QUERY_FORBIDDEN_PATTERNS": "Query contains forbidden SQL operations." } diff --git a/src/utils/rawQueryUtils.js b/src/utils/rawQueryUtils.js index e19f4e802..a974c8d8d 100644 --- a/src/utils/rawQueryUtils.js +++ b/src/utils/rawQueryUtils.js @@ -80,8 +80,8 @@ function normalizeQuery(query) { * @returns {boolean} - True if forbidden patterns are found */ function hasForbiddenPatterns(normalizedQuery) { - const forbiddenVerbs = /\b(drop|alter|truncate|insert|update|delete)\b/i - return forbiddenVerbs.test(normalizedQuery) + const forbiddenPatterns = /\b(drop|alter|truncate|insert|update|delete|union|concat|\|\|)\b/i + return forbiddenPatterns.test(normalizedQuery) } /** From 88d93330f8c41037efbdd76a17e410bef05a3af6 Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Wed, 15 Oct 2025 18:46:32 +0530 Subject: [PATCH 26/32] fix(auth): ensure decodedToken set and next() called properly --- src/controllers/v1/admin.js | 9 --------- src/middlewares/authenticator.js | 3 +++ 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/controllers/v1/admin.js b/src/controllers/v1/admin.js index 66fe18e3c..71e553106 100644 --- a/src/controllers/v1/admin.js +++ b/src/controllers/v1/admin.js @@ -208,15 +208,6 @@ module.exports = class Admin { */ async executeRawQuery(req) { try { - // Validate admin role - if (!utilsHelper.validateRoleAccess(req.decodedToken.roles, common.ADMIN_ROLE)) { - return responses.failureResponse({ - message: 'USER_IS_NOT_A_ADMIN', - statusCode: httpStatusCode.bad_request, - responseCode: 'CLIENT_ERROR', - }) - } - const { query } = req.body const pageNo = req.pageNo diff --git a/src/middlewares/authenticator.js b/src/middlewares/authenticator.js index a6daab704..878b77a40 100644 --- a/src/middlewares/authenticator.js +++ b/src/middlewares/authenticator.js @@ -256,7 +256,10 @@ module.exports = async function (req, res, next) { decodedToken.data.tenant_code = tenantCode decodedToken.data.organization_id = org.id // Use the ID from the database decodedToken.data.organization_code = orgCode + } else { + req.decodedToken = decodedToken.data } + return next() } if (roleValidation) { From 65b17396f6403f63bae0cc438f5207780e1f54cc Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Wed, 15 Oct 2025 18:56:17 +0530 Subject: [PATCH 27/32] updated: getPaginationParams to handle default values --- src/utils/rawQueryUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/rawQueryUtils.js b/src/utils/rawQueryUtils.js index a974c8d8d..f7afc6182 100644 --- a/src/utils/rawQueryUtils.js +++ b/src/utils/rawQueryUtils.js @@ -41,7 +41,7 @@ function validateQuerySecurity(query) { */ function getPaginationParams(pageNo, pageSize) { const validPageNo = Math.max(1, parseInt(pageNo) || 1) - const validPageSize = Math.min(Math.max(1, parseInt(pageSize) || 100), 1000) // Cap at 1000 + const validPageSize = Math.min(Math.max(1, parseInt(pageSize) || 100), 100) // Cap at 100 const offset = (validPageNo - 1) * validPageSize return { limit: validPageSize, offset } From f01b6dfd7d59564f78459d0b71528e97430185da Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Wed, 15 Oct 2025 19:01:50 +0530 Subject: [PATCH 28/32] updated: error response message --- src/services/admin.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/services/admin.js b/src/services/admin.js index b7fff1aa4..133c8dbbc 100644 --- a/src/services/admin.js +++ b/src/services/admin.js @@ -861,7 +861,6 @@ module.exports = class AdminHelper { type: sequelize.QueryTypes.SELECT, timeout: 30000, // Prevent long-running queries }) - // Get total count (use sanitizedQuery as subquery) const countQuery = `SELECT COUNT(*) AS count FROM (${sanitizedQuery}) AS subquery` const [countResult] = await sequelize.query(countQuery, { @@ -880,12 +879,15 @@ module.exports = class AdminHelper { }) } catch (error) { console.error('Error executing raw query:', error) + const isValidationError = + error.message && + (error.message.startsWith('QUERY_') || + error.message === 'INVALID_QUERY_INPUT' || + error.message === 'ONLY_SELECT_QUERIES_ALLOWED') return responses.failureResponse({ - message: error.message || 'QUERY_EXECUTION_FAILED', - statusCode: error.message.includes('QUERY_') - ? httpStatusCode.bad_request - : httpStatusCode.internal_server_error, - responseCode: error.message.includes('QUERY_') ? 'CLIENT_ERROR' : 'SERVER_ERROR', + message: isValidationError ? error.message : 'QUERY_EXECUTION_FAILED', + statusCode: isValidationError ? httpStatusCode.bad_request : httpStatusCode.internal_server_error, + responseCode: isValidationError ? 'CLIENT_ERROR' : 'SERVER_ERROR', }) } } From debdaa4a2a3550297e3ea09c56e013602e8d6119 Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Wed, 15 Oct 2025 19:11:24 +0530 Subject: [PATCH 29/32] add: comment for early return for admin users. --- src/middlewares/authenticator.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/middlewares/authenticator.js b/src/middlewares/authenticator.js index 878b77a40..9de389f1e 100644 --- a/src/middlewares/authenticator.js +++ b/src/middlewares/authenticator.js @@ -259,6 +259,10 @@ module.exports = async function (req, res, next) { } else { req.decodedToken = decodedToken.data } + // Admin users intentionally bypass role and permission validation below. + // This early return ensures admins proceed without further checks. + // If admin access rules change, remove or adjust this bypass accordingly. + return next() } From 3a77d3db6b5ea21776746c46372f25761a4ad0c6 Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Wed, 15 Oct 2025 19:17:11 +0530 Subject: [PATCH 30/32] add: explicit role validation --- src/controllers/v1/admin.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/controllers/v1/admin.js b/src/controllers/v1/admin.js index 71e553106..1067e2b0e 100644 --- a/src/controllers/v1/admin.js +++ b/src/controllers/v1/admin.js @@ -208,6 +208,14 @@ module.exports = class Admin { */ async executeRawQuery(req) { try { + if (!utilsHelper.validateRoleAccess(req.decodedToken.roles, common.ADMIN_ROLE)) { + throw responses.failureResponse({ + message: 'USER_IS_NOT_A_ADMIN', + statusCode: httpStatusCode.bad_request, + responseCode: 'CLIENT_ERROR', + }) + } + const { query } = req.body const pageNo = req.pageNo From 3a29ad6b20d20cd918256b509a6b47818091602f Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Wed, 15 Oct 2025 19:31:56 +0530 Subject: [PATCH 31/32] add: validation for executeRawQuery --- src/validators/v1/admin.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/validators/v1/admin.js b/src/validators/v1/admin.js index 7538047f9..26f815777 100644 --- a/src/validators/v1/admin.js +++ b/src/validators/v1/admin.js @@ -177,4 +177,7 @@ module.exports = { .matches(/^[a-z0-9_]+$/) .withMessage('tenant-id must be lowercase alphanumeric with underscores.') }, + executeRawQuery: (req) => { + req.checkBody('query').trim().notEmpty().withMessage('query is required for phone number login') + }, } From 04696613a8e415572f5da497da24cf6f8d81545a Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Wed, 15 Oct 2025 19:43:59 +0530 Subject: [PATCH 32/32] fix: message --- src/validators/v1/admin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/validators/v1/admin.js b/src/validators/v1/admin.js index 26f815777..f7741b749 100644 --- a/src/validators/v1/admin.js +++ b/src/validators/v1/admin.js @@ -178,6 +178,6 @@ module.exports = { .withMessage('tenant-id must be lowercase alphanumeric with underscores.') }, executeRawQuery: (req) => { - req.checkBody('query').trim().notEmpty().withMessage('query is required for phone number login') + req.checkBody('query').trim().notEmpty().withMessage('query field is empty') }, }