From b87fcf60b39c3f9ce5a48acaf37c38d99aa9bfec Mon Sep 17 00:00:00 2001 From: priyanka-TL Date: Mon, 8 Dec 2025 23:19:25 +0530 Subject: [PATCH 1/9] feat: add tenant association to TenantDomain model and update queries for tenant validation --- src/database/models/TenantDomain.js | 8 ++++++ src/database/queries/tenantDomain.js | 38 +++++++++++++++++++++++++++- src/services/account.js | 18 ++++++------- 3 files changed, 53 insertions(+), 11 deletions(-) diff --git a/src/database/models/TenantDomain.js b/src/database/models/TenantDomain.js index b1e5ed6ee..c675b3191 100644 --- a/src/database/models/TenantDomain.js +++ b/src/database/models/TenantDomain.js @@ -45,5 +45,13 @@ module.exports = (sequelize, DataTypes) => { } ) + TenantDomain.associate = (models) => { + TenantDomain.belongsTo(models.Tenant, { + foreignKey: 'tenant_code', + targetKey: 'code', + as: 'tenant', + }) + } + return TenantDomain } diff --git a/src/database/queries/tenantDomain.js b/src/database/queries/tenantDomain.js index 0a832b0f9..4751e365e 100644 --- a/src/database/queries/tenantDomain.js +++ b/src/database/queries/tenantDomain.js @@ -1,5 +1,5 @@ 'use strict' -const { TenantDomain, sequelize } = require('@database/models/index') +const { TenantDomain, Tenant } = require('@database/models/index') const { Op } = require('sequelize') exports.create = async (data) => { @@ -26,6 +26,42 @@ exports.findOne = async (filter, options = {}) => { } } +// Fetch tenant domain with tenant details in a single query +exports.findOneWithTenant = async (filter, options = {}) => { + try { + const result = await TenantDomain.findOne({ + where: filter, + attributes: options.attributes || undefined, + include: [ + { + model: Tenant, + as: 'tenant', + required: false, // LEFT JOIN to allow separate validation + attributes: options.tenantAttributes || [ + 'code', + 'name', + 'status', + 'description', + 'logo', + 'meta', + 'theming', + ], + }, + ], + raw: false, // Need nested object structure + }) + + if (!result) return null + + // Convert to plain object and flatten structure for easier access + const plainResult = result.get({ plain: true }) + return plainResult + } catch (error) { + console.error(error) + return error + } +} + exports.findAll = async (filter = {}, options = {}) => { try { return await TenantDomain.findAll({ diff --git a/src/services/account.js b/src/services/account.js index 88d9b71ec..2942393a7 100644 --- a/src/services/account.js +++ b/src/services/account.js @@ -655,24 +655,22 @@ module.exports = class AccountHelper { responseCode: 'CLIENT_ERROR', }) - // Validate tenant domain - const tenantDomain = await tenantDomainQueries.findOne({ domain }) - if (!tenantDomain) { + // Validate tenant domain and tenant + const domainWithTenant = await tenantDomainQueries.findOneWithTenant({ domain }) + if (!domainWithTenant) { return notFoundResponse('TENANT_DOMAIN_NOT_FOUND_PING_ADMIN') } - // Validate tenant - const tenantDetail = await tenantQueries.findOne({ - code: tenantDomain.tenant_code, - }) - if (!tenantDetail) { + // Validate tenant exists and is active + const tenantDetail = domainWithTenant.tenant + if (!tenantDetail || tenantDetail.status !== common.ACTIVE_STATUS) { return notFoundResponse('TENANT_NOT_FOUND_PING_ADMIN') } // Helper functions to detect identifier type const isEmail = (str) => /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/.test(str) const isPhone = (str) => /^\+?[1-9]\d{1,14}$/.test(str) // Adjust regex as needed - const isUsername = (str) => /^[a-zA-Z0-9_]{3,30}$/.test(str) + // const isUsername = (str) => /^[a-zA-Z0-9_]{3,30}$/.test(str) const identifier = bodyData.identifier?.toLowerCase() if (!identifier) { @@ -1048,7 +1046,7 @@ module.exports = class AccountHelper { responseCode: 'CLIENT_ERROR', }) } - const user = await userQueries.findUserWithOrganization(query, {}, tenantDomain.tenant_code) + const user = await userQueries.findUserWithOrganization(query, {}, tenantDetail.code) if (!user) { return responses.failureResponse({ From cbc1abc4490dd9fe38a6adc12daae37fe54d751e Mon Sep 17 00:00:00 2001 From: priyanka-TL Date: Mon, 8 Dec 2025 23:30:26 +0530 Subject: [PATCH 2/9] refactor: update tenant domain validation and notification logic in AccountHelper --- src/services/account.js | 133 ++++++++++++++++++++-------------------- 1 file changed, 68 insertions(+), 65 deletions(-) diff --git a/src/services/account.js b/src/services/account.js index 2942393a7..342b2474b 100644 --- a/src/services/account.js +++ b/src/services/account.js @@ -66,16 +66,19 @@ module.exports = class AccountHelper { responseCode: 'CLIENT_ERROR', }) - const tenantDomain = await tenantDomainQueries.findOne({ domain }) - if (!tenantDomain) { + // const tenantDomain = await tenantDomainQueries.findOne({ domain }) + // if (!tenantDomain) { + // return notFoundResponse('TENANT_DOMAIN_NOT_FOUND_PING_ADMIN') + // } + + const domainWithTenant = await tenantDomainQueries.findOneWithTenant({ domain }) + if (!domainWithTenant) { return notFoundResponse('TENANT_DOMAIN_NOT_FOUND_PING_ADMIN') } - const tenantDetail = await tenantQueries.findOne({ - code: tenantDomain.tenant_code, - status: common.ACTIVE_STATUS, - }) - if (!tenantDetail) { + // Validate tenant exists and is active + const tenantDetail = domainWithTenant.tenant + if (!tenantDetail || tenantDetail.status !== common.ACTIVE_STATUS) { return notFoundResponse('TENANT_NOT_FOUND_PING_ADMIN') } @@ -173,13 +176,13 @@ module.exports = class AccountHelper { } } - if (!isOtpValid) { - return responses.failureResponse({ - message: 'OTP_INVALID', - statusCode: httpStatusCode.bad_request, - responseCode: 'CLIENT_ERROR', - }) - } + // if (!isOtpValid) { + // return responses.failureResponse({ + // message: 'OTP_INVALID', + // statusCode: httpStatusCode.bad_request, + // responseCode: 'CLIENT_ERROR', + // }) + // } } bodyData.password = utilsHelper.hashPassword(bodyData.password) @@ -209,7 +212,7 @@ module.exports = class AccountHelper { filterCondition.phone_code = bodyData.phone_code } - filterCondition.tenant_code = tenantDomain.tenant_code + filterCondition.tenant_code = domainWithTenant.tenant_code filterCondition.status = common.INVITED_STATUS invitedUserMatch = await userInviteQueries.findOne(filterCondition, { @@ -561,36 +564,36 @@ module.exports = class AccountHelper { const result = { access_token: accessToken, refresh_token: refreshToken, user } - if (plaintextEmailId) { - notificationUtils.sendEmailNotification({ - emailId: plaintextEmailId, - templateCode: process.env.REGISTRATION_EMAIL_TEMPLATE_CODE, - variables: { - name: bodyData.name, - appName: tenantDetail.name, - roles: roleToString || '', - portalURL: tenantDomain.domain, - }, - tenantCode: tenantDetail.code, - organization_code: user.organizations?.[0].code || null, - }) - } - - // Send SMS notification with OTP if phone is provided - if (plaintextPhoneNumber) { - notificationUtils.sendSMSNotification({ - phoneNumber: plaintextPhoneNumber, - templateCode: process.env.REGISTRATION_EMAIL_TEMPLATE_CODE, - variables: { - name: bodyData.name, - appName: tenantDetail.name, - roles: roleToString || '', - portalURL: tenantDomain.domain, - }, - tenantCode: tenantDetail.code, - organization_code: user.organizations?.[0].code || null, - }) - } + // if (plaintextEmailId) { + // notificationUtils.sendEmailNotification({ + // emailId: plaintextEmailId, + // templateCode: process.env.REGISTRATION_EMAIL_TEMPLATE_CODE, + // variables: { + // name: bodyData.name, + // appName: tenantDetail.name, + // roles: roleToString || '', + // portalURL: domainWithTenant.domain, + // }, + // tenantCode: tenantDetail.code, + // organization_code: user.organizations?.[0].code || null, + // }) + // } + + // // Send SMS notification with OTP if phone is provided + // if (plaintextPhoneNumber) { + // notificationUtils.sendSMSNotification({ + // phoneNumber: plaintextPhoneNumber, + // templateCode: process.env.REGISTRATION_EMAIL_TEMPLATE_CODE, + // variables: { + // name: bodyData.name, + // appName: tenantDetail.name, + // roles: roleToString || '', + // portalURL: domainWithTenant.domain, + // }, + // tenantCode: tenantDetail.code, + // organization_code: user.organizations?.[0].code || null, + // }) + // } result.user = await utils.processDbResponse(result.user, prunedEntities) result.user.email = plaintextEmailId result.user.phone = plaintextPhoneNumber @@ -1270,26 +1273,26 @@ module.exports = class AccountHelper { } // Send email notification with OTP if email is provided - if (plaintextEmailId) { - notificationUtils.sendEmailNotification({ - emailId: plaintextEmailId, - templateCode: process.env.REGISTRATION_OTP_EMAIL_TEMPLATE_CODE, - variables: { name: bodyData.name || plaintextEmailId, otp }, - tenantCode: tenantDetail.code, - organization_code: process.env.DEFAULT_ORGANISATION_CODE || null, - }) - } - - // Send SMS notification with OTP if phone is provided - if (plaintextPhoneNumber && bodyData.phone_code) { - notificationUtils.sendSMSNotification({ - phoneNumber: plaintextPhoneNumber, - templateCode: process.env.REGISTRATION_OTP_EMAIL_TEMPLATE_CODE, - variables: { app_name: tenantDetail.name, otp }, - tenantCode: tenantDetail.code, - organization_code: process.env.DEFAULT_ORGANISATION_CODE || null, - }) - } + // if (plaintextEmailId) { + // notificationUtils.sendEmailNotification({ + // emailId: plaintextEmailId, + // templateCode: process.env.REGISTRATION_OTP_EMAIL_TEMPLATE_CODE, + // variables: { name: bodyData.name || plaintextEmailId, otp }, + // tenantCode: tenantDetail.code, + // organization_code: process.env.DEFAULT_ORGANISATION_CODE || null, + // }) + // } + + // // Send SMS notification with OTP if phone is provided + // if (plaintextPhoneNumber && bodyData.phone_code) { + // notificationUtils.sendSMSNotification({ + // phoneNumber: plaintextPhoneNumber, + // templateCode: process.env.REGISTRATION_OTP_EMAIL_TEMPLATE_CODE, + // variables: { app_name: tenantDetail.name, otp }, + // tenantCode: tenantDetail.code, + // organization_code: process.env.DEFAULT_ORGANISATION_CODE || null, + // }) + // } // Log OTP in development environment for testing if (process.env.APPLICATION_ENV === 'development') { From 8f3e4331c9ceb229e4431dd3b32156dadef9aa8c Mon Sep 17 00:00:00 2001 From: priyanka-TL Date: Mon, 8 Dec 2025 23:34:03 +0530 Subject: [PATCH 3/9] refactor: clean up commented-out code in AccountHelper and improve OTP validation logic --- src/services/account.js | 79 +++++++++++++++++++---------------------- 1 file changed, 37 insertions(+), 42 deletions(-) diff --git a/src/services/account.js b/src/services/account.js index 342b2474b..f5cc443aa 100644 --- a/src/services/account.js +++ b/src/services/account.js @@ -66,11 +66,6 @@ module.exports = class AccountHelper { responseCode: 'CLIENT_ERROR', }) - // const tenantDomain = await tenantDomainQueries.findOne({ domain }) - // if (!tenantDomain) { - // return notFoundResponse('TENANT_DOMAIN_NOT_FOUND_PING_ADMIN') - // } - const domainWithTenant = await tenantDomainQueries.findOneWithTenant({ domain }) if (!domainWithTenant) { return notFoundResponse('TENANT_DOMAIN_NOT_FOUND_PING_ADMIN') @@ -176,13 +171,13 @@ module.exports = class AccountHelper { } } - // if (!isOtpValid) { - // return responses.failureResponse({ - // message: 'OTP_INVALID', - // statusCode: httpStatusCode.bad_request, - // responseCode: 'CLIENT_ERROR', - // }) - // } + if (!isOtpValid) { + return responses.failureResponse({ + message: 'OTP_INVALID', + statusCode: httpStatusCode.bad_request, + responseCode: 'CLIENT_ERROR', + }) + } } bodyData.password = utilsHelper.hashPassword(bodyData.password) @@ -564,36 +559,36 @@ module.exports = class AccountHelper { const result = { access_token: accessToken, refresh_token: refreshToken, user } - // if (plaintextEmailId) { - // notificationUtils.sendEmailNotification({ - // emailId: plaintextEmailId, - // templateCode: process.env.REGISTRATION_EMAIL_TEMPLATE_CODE, - // variables: { - // name: bodyData.name, - // appName: tenantDetail.name, - // roles: roleToString || '', - // portalURL: domainWithTenant.domain, - // }, - // tenantCode: tenantDetail.code, - // organization_code: user.organizations?.[0].code || null, - // }) - // } - - // // Send SMS notification with OTP if phone is provided - // if (plaintextPhoneNumber) { - // notificationUtils.sendSMSNotification({ - // phoneNumber: plaintextPhoneNumber, - // templateCode: process.env.REGISTRATION_EMAIL_TEMPLATE_CODE, - // variables: { - // name: bodyData.name, - // appName: tenantDetail.name, - // roles: roleToString || '', - // portalURL: domainWithTenant.domain, - // }, - // tenantCode: tenantDetail.code, - // organization_code: user.organizations?.[0].code || null, - // }) - // } + if (plaintextEmailId) { + notificationUtils.sendEmailNotification({ + emailId: plaintextEmailId, + templateCode: process.env.REGISTRATION_EMAIL_TEMPLATE_CODE, + variables: { + name: bodyData.name, + appName: tenantDetail.name, + roles: roleToString || '', + portalURL: domainWithTenant.domain, + }, + tenantCode: tenantDetail.code, + organization_code: user.organizations?.[0].code || null, + }) + } + + // Send SMS notification with OTP if phone is provided + if (plaintextPhoneNumber) { + notificationUtils.sendSMSNotification({ + phoneNumber: plaintextPhoneNumber, + templateCode: process.env.REGISTRATION_EMAIL_TEMPLATE_CODE, + variables: { + name: bodyData.name, + appName: tenantDetail.name, + roles: roleToString || '', + portalURL: domainWithTenant.domain, + }, + tenantCode: tenantDetail.code, + organization_code: user.organizations?.[0].code || null, + }) + } result.user = await utils.processDbResponse(result.user, prunedEntities) result.user.email = plaintextEmailId result.user.phone = plaintextPhoneNumber From d5b636d4659f1ba00f3f747e48695dcfe293c599 Mon Sep 17 00:00:00 2001 From: priyanka-TL Date: Mon, 8 Dec 2025 23:37:41 +0530 Subject: [PATCH 4/9] refactor: restore OTP notification logic in AccountHelper --- src/database/queries/tenantDomain.js | 1 - src/services/account.js | 40 ++++++++++++++-------------- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/src/database/queries/tenantDomain.js b/src/database/queries/tenantDomain.js index 4751e365e..111e02b0f 100644 --- a/src/database/queries/tenantDomain.js +++ b/src/database/queries/tenantDomain.js @@ -1,6 +1,5 @@ 'use strict' const { TenantDomain, Tenant } = require('@database/models/index') -const { Op } = require('sequelize') exports.create = async (data) => { try { diff --git a/src/services/account.js b/src/services/account.js index f5cc443aa..95fb46fff 100644 --- a/src/services/account.js +++ b/src/services/account.js @@ -1268,26 +1268,26 @@ module.exports = class AccountHelper { } // Send email notification with OTP if email is provided - // if (plaintextEmailId) { - // notificationUtils.sendEmailNotification({ - // emailId: plaintextEmailId, - // templateCode: process.env.REGISTRATION_OTP_EMAIL_TEMPLATE_CODE, - // variables: { name: bodyData.name || plaintextEmailId, otp }, - // tenantCode: tenantDetail.code, - // organization_code: process.env.DEFAULT_ORGANISATION_CODE || null, - // }) - // } - - // // Send SMS notification with OTP if phone is provided - // if (plaintextPhoneNumber && bodyData.phone_code) { - // notificationUtils.sendSMSNotification({ - // phoneNumber: plaintextPhoneNumber, - // templateCode: process.env.REGISTRATION_OTP_EMAIL_TEMPLATE_CODE, - // variables: { app_name: tenantDetail.name, otp }, - // tenantCode: tenantDetail.code, - // organization_code: process.env.DEFAULT_ORGANISATION_CODE || null, - // }) - // } + if (plaintextEmailId) { + notificationUtils.sendEmailNotification({ + emailId: plaintextEmailId, + templateCode: process.env.REGISTRATION_OTP_EMAIL_TEMPLATE_CODE, + variables: { name: bodyData.name || plaintextEmailId, otp }, + tenantCode: tenantDetail.code, + organization_code: process.env.DEFAULT_ORGANISATION_CODE || null, + }) + } + + // Send SMS notification with OTP if phone is provided + if (plaintextPhoneNumber && bodyData.phone_code) { + notificationUtils.sendSMSNotification({ + phoneNumber: plaintextPhoneNumber, + templateCode: process.env.REGISTRATION_OTP_EMAIL_TEMPLATE_CODE, + variables: { app_name: tenantDetail.name, otp }, + tenantCode: tenantDetail.code, + organization_code: process.env.DEFAULT_ORGANISATION_CODE || null, + }) + } // Log OTP in development environment for testing if (process.env.APPLICATION_ENV === 'development') { From e14e83068be4758e081f9f360608008032cf6682 Mon Sep 17 00:00:00 2001 From: priyanka-TL Date: Thu, 11 Dec 2025 17:12:41 +0530 Subject: [PATCH 5/9] refactor: update tenant domain validation logic in AccountHelper and modify foreign key constraints in migrations --- ...47-add-feature-role-mapping-constraints.js | 115 ++++++++--------- ...ify-feature-role-mapping-fk-constraints.js | 118 ++++++++++++++++++ src/services/account.js | 35 +++--- 3 files changed, 187 insertions(+), 81 deletions(-) create mode 100644 src/database/migrations/20251211000000-modify-feature-role-mapping-fk-constraints.js diff --git a/src/database/migrations/20251003155747-add-feature-role-mapping-constraints.js b/src/database/migrations/20251003155747-add-feature-role-mapping-constraints.js index 3157f3fb7..590e9d68e 100644 --- a/src/database/migrations/20251003155747-add-feature-role-mapping-constraints.js +++ b/src/database/migrations/20251003155747-add-feature-role-mapping-constraints.js @@ -3,77 +3,62 @@ /** @type {import('sequelize-cli').Migration} */ module.exports = { async up(queryInterface, Sequelize) { - // Add foreign key constraint for feature_code - await queryInterface.addConstraint('feature_role_mapping', { - fields: ['feature_code'], - type: 'foreign key', - name: 'fk_feature_role_mapping_feature_code', - references: { - table: 'features', - field: 'code', - }, - onUpdate: 'CASCADE', - onDelete: 'CASCADE', - }) + try { + await queryInterface.addConstraint('feature_role_mapping', { + fields: ['feature_code'], + type: 'foreign key', + name: 'fk_feature_role_mapping_feature_code', + references: { table: 'features', field: 'code' }, + onUpdate: 'NO ACTION', + onDelete: 'NO ACTION', + }) + // Composite FK: (organization_code, tenant_code) → organizations(code, tenant_code) + await queryInterface.sequelize.query(` + ALTER TABLE feature_role_mapping + ADD CONSTRAINT fk_feature_role_mapping_organization_code + FOREIGN KEY (organization_code, tenant_code) + REFERENCES organizations (code, tenant_code) + ON UPDATE CASCADE + ON DELETE CASCADE; + `) - // Add foreign key constraint for tenant_code - await queryInterface.addConstraint('feature_role_mapping', { - fields: ['tenant_code'], - type: 'foreign key', - name: 'fk_feature_role_mapping_tenant_code', - references: { - table: 'tenants', - field: 'code', - }, - onUpdate: 'CASCADE', - onDelete: 'CASCADE', - }) + // Unique index ignoring soft-deleted rows + await queryInterface.sequelize.query(` + CREATE UNIQUE INDEX feature_role_org_tenant_unique + ON feature_role_mapping (feature_code, role_title, organization_code, tenant_code) + WHERE deleted_at IS NULL; + `) - // Add composite foreign key for organization_code (organization_code, tenant_code) -> organizations (code, tenant_code) - await queryInterface.sequelize.query(` - ALTER TABLE feature_role_mapping - ADD CONSTRAINT fk_feature_role_mapping_organization_code - FOREIGN KEY (organization_code, tenant_code) - REFERENCES organizations (code, tenant_code) - ON UPDATE CASCADE - ON DELETE CASCADE; - `) - - // Add composite foreign key for role_title (tenant_code, role_title) -> user_roles (tenant_code, title) - //commenting this as of now because of issue in user_roles table which is using the organization_id as foreign key - // await queryInterface.sequelize.query(` - // ALTER TABLE feature_role_mapping - // ADD CONSTRAINT fk_feature_role_mapping_role_title - // FOREIGN KEY (tenant_code, role_title) - // REFERENCES user_roles (tenant_code, title) - // ON UPDATE CASCADE - // ON DELETE NO ACTION; - // `) - - // Unique constraint for feature_code, role_title, organization_code, tenant_code - await queryInterface.sequelize.query(` - CREATE UNIQUE INDEX feature_role_org_tenant_unique - ON feature_role_mapping (feature_code, role_title, organization_code, tenant_code) - WHERE deleted_at IS NULL; - `) - - await queryInterface.sequelize.query(` - ALTER TABLE feature_role_mapping - ADD CONSTRAINT fk_org_feature_role_mapping_organization_code - FOREIGN KEY (feature_code, tenant_code, organization_code) - REFERENCES organization_features (feature_code, tenant_code, organization_code) - ON UPDATE CASCADE - ON DELETE CASCADE; - `) + // Composite FK: (feature_code, tenant_code, organization_code) → organization_features(...) + await queryInterface.sequelize.query(` + ALTER TABLE feature_role_mapping + ADD CONSTRAINT fk_org_feature_role_mapping_organization_code + FOREIGN KEY (feature_code, tenant_code, organization_code) + REFERENCES organization_features (feature_code, tenant_code, organization_code) + ON UPDATE CASCADE + ON DELETE CASCADE; + `) + } catch (error) { + console.error('Migration failed:', error) + throw error // important so sequelize knows migration failed + } }, async down(queryInterface, Sequelize) { - // Drop foreign key constraints - await queryInterface.removeConstraint('feature_role_mapping', 'fk_feature_role_mapping_tenant_code') - await queryInterface.removeConstraint('feature_role_mapping', 'fk_feature_role_mapping_organization_code') + // Remove constraints safely + await queryInterface + .removeConstraint('feature_role_mapping', 'fk_feature_role_mapping_tenant_code') + .catch(() => {}) + await queryInterface + .removeConstraint('feature_role_mapping', 'fk_feature_role_mapping_organization_code') + .catch(() => {}) // await queryInterface.removeConstraint('feature_role_mapping', 'fk_feature_role_mapping_role_title') - await queryInterface.removeConstraint('feature_role_mapping', 'fk_feature_role_mapping_feature_code') - await queryInterface.removeConstraint('feature_role_mapping', 'fk_org_feature_role_mapping_organization_code') + await queryInterface + .removeConstraint('feature_role_mapping', 'fk_feature_role_mapping_feature_code') + .catch(() => {}) + await queryInterface + .removeConstraint('feature_role_mapping', 'fk_org_feature_role_mapping_organization_code') + .catch(() => {}) await queryInterface.sequelize.query('DROP INDEX IF EXISTS feature_role_org_tenant_unique;') }, diff --git a/src/database/migrations/20251211000000-modify-feature-role-mapping-fk-constraints.js b/src/database/migrations/20251211000000-modify-feature-role-mapping-fk-constraints.js new file mode 100644 index 000000000..9ae6b38af --- /dev/null +++ b/src/database/migrations/20251211000000-modify-feature-role-mapping-fk-constraints.js @@ -0,0 +1,118 @@ +'use strict' + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface) { + try { + console.log('Removing existing foreign key constraints...') + + // Remove the existing FK constraint: feature_code → features(code) + await queryInterface.sequelize + .query( + ` + ALTER TABLE feature_role_mapping + DROP CONSTRAINT IF EXISTS fk_feature_role_mapping_feature_code; + ` + ) + .catch(() => { + console.warn( + 'Warning: fk_feature_role_mapping_feature_code constraint not found or already removed' + ) + }) + + // Remove the existing composite FK: (organization_code, tenant_code) → organizations + await queryInterface.sequelize + .query( + ` + ALTER TABLE feature_role_mapping + DROP CONSTRAINT IF EXISTS fk_feature_role_mapping_organization_code; + ` + ) + .catch(() => { + console.warn( + 'Warning: fk_feature_role_mapping_organization_code constraint not found or already removed' + ) + }) + + // Remove the existing composite FK: (feature_code, tenant_code, organization_code) → organization_features + await queryInterface.sequelize + .query( + ` + ALTER TABLE feature_role_mapping + DROP CONSTRAINT IF EXISTS fk_org_feature_role_mapping_organization_code; + ` + ) + .catch(() => { + console.warn( + 'Warning: fk_org_feature_role_mapping_organization_code constraint not found or already removed' + ) + }) + + console.log('Creating new foreign key constraints with NO ACTION...') + + // Recreate composite FK: (organization_code, tenant_code) → organizations with NO ACTION + await queryInterface.sequelize.query(` + ALTER TABLE feature_role_mapping + ADD CONSTRAINT fk_feature_role_mapping_organization_code + FOREIGN KEY (organization_code, tenant_code) + REFERENCES organizations (code, tenant_code) + ON UPDATE NO ACTION + ON DELETE NO ACTION; + `) + + // Recreate composite FK: (feature_code, tenant_code, organization_code) → organization_features with NO ACTION + await queryInterface.sequelize.query(` + ALTER TABLE feature_role_mapping + ADD CONSTRAINT fk_org_feature_role_mapping_organization_code + FOREIGN KEY (feature_code, tenant_code, organization_code) + REFERENCES organization_features (feature_code, tenant_code, organization_code) + ON UPDATE NO ACTION + ON DELETE NO ACTION; + `) + + console.log('Foreign key constraints successfully modified to NO ACTION') + } catch (error) { + console.error('Migration failed:', error) + throw error + } + }, + + async down(queryInterface) { + try { + console.log('Reverting to CASCADE constraints...') + + // Remove NO ACTION constraints + await queryInterface.sequelize + .query( + ` + ALTER TABLE feature_role_mapping + DROP CONSTRAINT IF EXISTS fk_feature_role_mapping_feature_code; + ` + ) + .catch(() => {}) + + await queryInterface.sequelize + .query( + ` + ALTER TABLE feature_role_mapping + DROP CONSTRAINT IF EXISTS fk_feature_role_mapping_organization_code; + ` + ) + .catch(() => {}) + + await queryInterface.sequelize + .query( + ` + ALTER TABLE feature_role_mapping + DROP CONSTRAINT IF EXISTS fk_org_feature_role_mapping_organization_code; + ` + ) + .catch(() => {}) + + console.log('Successfully reverted to CASCADE constraints') + } catch (error) { + console.error('Rollback failed:', error) + throw error + } + }, +} diff --git a/src/services/account.js b/src/services/account.js index 95fb46fff..705cac62c 100644 --- a/src/services/account.js +++ b/src/services/account.js @@ -53,7 +53,6 @@ module.exports = class AccountHelper { * @param {Object} deviceInfo - Device information * @returns {JSON} - returns account creation details. */ - static async create(bodyData, deviceInfo, domain) { const projection = ['password'] let isInvitedUserId = false @@ -66,14 +65,16 @@ module.exports = class AccountHelper { responseCode: 'CLIENT_ERROR', }) - const domainWithTenant = await tenantDomainQueries.findOneWithTenant({ domain }) - if (!domainWithTenant) { + const tenantDomain = await tenantDomainQueries.findOne({ domain }) + if (!tenantDomain) { return notFoundResponse('TENANT_DOMAIN_NOT_FOUND_PING_ADMIN') } - // Validate tenant exists and is active - const tenantDetail = domainWithTenant.tenant - if (!tenantDetail || tenantDetail.status !== common.ACTIVE_STATUS) { + const tenantDetail = await tenantQueries.findOne({ + code: tenantDomain.tenant_code, + status: common.ACTIVE_STATUS, + }) + if (!tenantDetail) { return notFoundResponse('TENANT_NOT_FOUND_PING_ADMIN') } @@ -207,7 +208,7 @@ module.exports = class AccountHelper { filterCondition.phone_code = bodyData.phone_code } - filterCondition.tenant_code = domainWithTenant.tenant_code + filterCondition.tenant_code = tenantDomain.tenant_code filterCondition.status = common.INVITED_STATUS invitedUserMatch = await userInviteQueries.findOne(filterCondition, { @@ -567,7 +568,7 @@ module.exports = class AccountHelper { name: bodyData.name, appName: tenantDetail.name, roles: roleToString || '', - portalURL: domainWithTenant.domain, + portalURL: tenantDomain.domain, }, tenantCode: tenantDetail.code, organization_code: user.organizations?.[0].code || null, @@ -583,7 +584,7 @@ module.exports = class AccountHelper { name: bodyData.name, appName: tenantDetail.name, roles: roleToString || '', - portalURL: domainWithTenant.domain, + portalURL: tenantDomain.domain, }, tenantCode: tenantDetail.code, organization_code: user.organizations?.[0].code || null, @@ -653,22 +654,24 @@ module.exports = class AccountHelper { responseCode: 'CLIENT_ERROR', }) - // Validate tenant domain and tenant - const domainWithTenant = await tenantDomainQueries.findOneWithTenant({ domain }) - if (!domainWithTenant) { + // Validate tenant domain + const tenantDomain = await tenantDomainQueries.findOne({ domain }) + if (!tenantDomain) { return notFoundResponse('TENANT_DOMAIN_NOT_FOUND_PING_ADMIN') } - // Validate tenant exists and is active - const tenantDetail = domainWithTenant.tenant - if (!tenantDetail || tenantDetail.status !== common.ACTIVE_STATUS) { + // Validate tenant + const tenantDetail = await tenantQueries.findOne({ + code: tenantDomain.tenant_code, + }) + if (!tenantDetail) { return notFoundResponse('TENANT_NOT_FOUND_PING_ADMIN') } // Helper functions to detect identifier type const isEmail = (str) => /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/.test(str) const isPhone = (str) => /^\+?[1-9]\d{1,14}$/.test(str) // Adjust regex as needed - // const isUsername = (str) => /^[a-zA-Z0-9_]{3,30}$/.test(str) + const isUsername = (str) => /^[a-zA-Z0-9_]{3,30}$/.test(str) const identifier = bodyData.identifier?.toLowerCase() if (!identifier) { From 4fea988f9fada066c48f2e60466bc9a725996106 Mon Sep 17 00:00:00 2001 From: priyanka-TL Date: Thu, 11 Dec 2025 17:31:02 +0530 Subject: [PATCH 6/9] refactor: remove redundant association for FeatureRoleMapping in Feature model --- src/database/models/Feature.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/database/models/Feature.js b/src/database/models/Feature.js index a5d20a871..e098c428c 100644 --- a/src/database/models/Feature.js +++ b/src/database/models/Feature.js @@ -62,11 +62,6 @@ module.exports = (sequelize, DataTypes) => { foreignKey: 'feature_code', as: 'organization_features', }) - Feature.hasMany(models.FeatureRoleMapping, { - foreignKey: 'feature_code', - sourceKey: 'code', - as: 'featureRoleMappings', - }) } return Feature From 6db7ffd093e10a545949ec11f1e4572a1dbdee2d Mon Sep 17 00:00:00 2001 From: priyanka-TL Date: Thu, 11 Dec 2025 17:35:58 +0530 Subject: [PATCH 7/9] refactor: remove unused tenant association in TenantDomain model and simplify user query in AccountHelper --- src/database/models/TenantDomain.js | 8 ------ src/database/queries/tenantDomain.js | 39 ++-------------------------- src/services/account.js | 2 +- 3 files changed, 3 insertions(+), 46 deletions(-) diff --git a/src/database/models/TenantDomain.js b/src/database/models/TenantDomain.js index c675b3191..b1e5ed6ee 100644 --- a/src/database/models/TenantDomain.js +++ b/src/database/models/TenantDomain.js @@ -45,13 +45,5 @@ module.exports = (sequelize, DataTypes) => { } ) - TenantDomain.associate = (models) => { - TenantDomain.belongsTo(models.Tenant, { - foreignKey: 'tenant_code', - targetKey: 'code', - as: 'tenant', - }) - } - return TenantDomain } diff --git a/src/database/queries/tenantDomain.js b/src/database/queries/tenantDomain.js index 111e02b0f..0a832b0f9 100644 --- a/src/database/queries/tenantDomain.js +++ b/src/database/queries/tenantDomain.js @@ -1,5 +1,6 @@ 'use strict' -const { TenantDomain, Tenant } = require('@database/models/index') +const { TenantDomain, sequelize } = require('@database/models/index') +const { Op } = require('sequelize') exports.create = async (data) => { try { @@ -25,42 +26,6 @@ exports.findOne = async (filter, options = {}) => { } } -// Fetch tenant domain with tenant details in a single query -exports.findOneWithTenant = async (filter, options = {}) => { - try { - const result = await TenantDomain.findOne({ - where: filter, - attributes: options.attributes || undefined, - include: [ - { - model: Tenant, - as: 'tenant', - required: false, // LEFT JOIN to allow separate validation - attributes: options.tenantAttributes || [ - 'code', - 'name', - 'status', - 'description', - 'logo', - 'meta', - 'theming', - ], - }, - ], - raw: false, // Need nested object structure - }) - - if (!result) return null - - // Convert to plain object and flatten structure for easier access - const plainResult = result.get({ plain: true }) - return plainResult - } catch (error) { - console.error(error) - return error - } -} - exports.findAll = async (filter = {}, options = {}) => { try { return await TenantDomain.findAll({ diff --git a/src/services/account.js b/src/services/account.js index 705cac62c..5bb25a69e 100644 --- a/src/services/account.js +++ b/src/services/account.js @@ -1047,7 +1047,7 @@ module.exports = class AccountHelper { responseCode: 'CLIENT_ERROR', }) } - const user = await userQueries.findUserWithOrganization(query, {}, tenantDetail.code) + const user = await userQueries.findUserWithOrganization(query, {}, true) if (!user) { return responses.failureResponse({ From b4f8d8d6d2525282751e8f780f5fc773231667e5 Mon Sep 17 00:00:00 2001 From: priyanka-TL Date: Thu, 11 Dec 2025 17:39:54 +0530 Subject: [PATCH 8/9] refactor: update down migration logic to clarify constraint removal process --- .../20251003155747-add-feature-role-mapping-constraints.js | 3 --- ...251211000000-modify-feature-role-mapping-fk-constraints.js | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/database/migrations/20251003155747-add-feature-role-mapping-constraints.js b/src/database/migrations/20251003155747-add-feature-role-mapping-constraints.js index 590e9d68e..2d82cd931 100644 --- a/src/database/migrations/20251003155747-add-feature-role-mapping-constraints.js +++ b/src/database/migrations/20251003155747-add-feature-role-mapping-constraints.js @@ -46,9 +46,6 @@ module.exports = { async down(queryInterface, Sequelize) { // Remove constraints safely - await queryInterface - .removeConstraint('feature_role_mapping', 'fk_feature_role_mapping_tenant_code') - .catch(() => {}) await queryInterface .removeConstraint('feature_role_mapping', 'fk_feature_role_mapping_organization_code') .catch(() => {}) diff --git a/src/database/migrations/20251211000000-modify-feature-role-mapping-fk-constraints.js b/src/database/migrations/20251211000000-modify-feature-role-mapping-fk-constraints.js index 9ae6b38af..96c77ff31 100644 --- a/src/database/migrations/20251211000000-modify-feature-role-mapping-fk-constraints.js +++ b/src/database/migrations/20251211000000-modify-feature-role-mapping-fk-constraints.js @@ -79,7 +79,7 @@ module.exports = { async down(queryInterface) { try { - console.log('Reverting to CASCADE constraints...') + console.log('Removing NO ACTION constraints (manual recreation of CASCADE constraints may be needed)...') // Remove NO ACTION constraints await queryInterface.sequelize @@ -109,7 +109,7 @@ module.exports = { ) .catch(() => {}) - console.log('Successfully reverted to CASCADE constraints') + console.log('Constraints removed. Run previous migration to restore CASCADE constraints.') } catch (error) { console.error('Rollback failed:', error) throw error From 161d2a2e08974f3231ec259ed1e72a8256ed6cb4 Mon Sep 17 00:00:00 2001 From: priyanka-TL Date: Thu, 11 Dec 2025 17:41:13 +0530 Subject: [PATCH 9/9] refactor: clarify reason for removing foreign key constraint in migration --- .../20251211000000-modify-feature-role-mapping-fk-constraints.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/database/migrations/20251211000000-modify-feature-role-mapping-fk-constraints.js b/src/database/migrations/20251211000000-modify-feature-role-mapping-fk-constraints.js index 96c77ff31..95c3d5ba2 100644 --- a/src/database/migrations/20251211000000-modify-feature-role-mapping-fk-constraints.js +++ b/src/database/migrations/20251211000000-modify-feature-role-mapping-fk-constraints.js @@ -7,6 +7,7 @@ module.exports = { console.log('Removing existing foreign key constraints...') // Remove the existing FK constraint: feature_code → features(code) + // This constraint is incompatible with Citus distributed database and is being permanently removed await queryInterface.sequelize .query( `