From f498c5db13e5db3ae14a504c7e7a40f0d5a7866e Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Wed, 16 Jul 2025 20:03:23 +0530 Subject: [PATCH 01/55] added: removeDefaultOrgEntityTypes to readAllSystemEntityTypes --- src/services/entityType.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/services/entityType.js b/src/services/entityType.js index 05c6b58f9..2f4f604a0 100644 --- a/src/services/entityType.js +++ b/src/services/entityType.js @@ -98,11 +98,12 @@ module.exports = class EntityHelper { ) const defaultOrgId = defaultOrg.id - const attributes = ['value', 'label', 'id'] + const attributes = ['value', 'label', 'id', 'organization_id'] const entities = await entityTypeQueries.findAllEntityTypes([orgId, defaultOrgId], attributes) + const prunedEntities = removeDefaultOrgEntityTypes(entities, orgId) - if (!entities.length) { + if (!prunedEntities.length) { return responses.failureResponse({ message: 'ENTITY_TYPE_NOT_FOUND', statusCode: httpStatusCode.bad_request, @@ -112,7 +113,7 @@ module.exports = class EntityHelper { return responses.successResponse({ statusCode: httpStatusCode.ok, message: 'ENTITY_TYPE_FETCHED_SUCCESSFULLY', - result: entities, + result: prunedEntities, }) } catch (error) { throw error From 510cde2ea7e7cbf3d7aafac8b0539ae92a7bdf2b Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Wed, 16 Jul 2025 20:06:21 +0530 Subject: [PATCH 02/55] added: foreign keys and join queries to fetch entity types along with their entities --- src/database/models/entities.js | 7 +++++-- src/database/models/entityType.js | 4 ++-- src/database/queries/entityType.js | 27 +++++++++++++-------------- src/services/entityType.js | 16 +++++++++++++--- 4 files changed, 33 insertions(+), 21 deletions(-) diff --git a/src/database/models/entities.js b/src/database/models/entities.js index 3f31a7565..c0991dfef 100644 --- a/src/database/models/entities.js +++ b/src/database/models/entities.js @@ -23,14 +23,17 @@ module.exports = (sequelize, DataTypes) => { }, { sequelize, modelName: 'Entity', tableName: 'entities', freezeTableName: true, paranoid: true } ) - /* Entity.associate = (models) => { + + Entity.associate = (models) => { Entity.belongsTo(models.EntityType, { foreignKey: 'entity_type_id', as: 'entity_type', + targetKey: 'id', scope: { deleted_at: null, // Only associate with active EntityType records }, }) - } */ + } + return Entity } diff --git a/src/database/models/entityType.js b/src/database/models/entityType.js index 0d5e37337..bc527ea93 100644 --- a/src/database/models/entityType.js +++ b/src/database/models/entityType.js @@ -36,7 +36,7 @@ module.exports = (sequelize, DataTypes) => { }, { sequelize, modelName: 'EntityType', tableName: 'entity_types', freezeTableName: true, paranoid: true } ) - /* EntityType.associate = (models) => { + EntityType.associate = (models) => { EntityType.hasMany(models.Entity, { foreignKey: 'entity_type_id', as: 'entities', @@ -44,7 +44,7 @@ module.exports = (sequelize, DataTypes) => { deleted_at: null, // Only associate with active EntityType records }, }) - } */ + } EntityType.addHook('beforeDestroy', async (instance, options) => { try { diff --git a/src/database/queries/entityType.js b/src/database/queries/entityType.js index 49163b199..5c792d1da 100644 --- a/src/database/queries/entityType.js +++ b/src/database/queries/entityType.js @@ -43,29 +43,28 @@ module.exports = class UserEntityData { try { const entityTypes = await EntityType.findAll({ where: filter, - raw: true, - }) - - const entityTypeIds = entityTypes.map((entityType) => entityType.id) - - const entities = await Entity.findAll({ - where: { entity_type_id: entityTypeIds, status: 'ACTIVE' }, - raw: true, - //attributes: { exclude: ['entity_type_id'] }, + include: [ + { + model: Entity, + as: 'entities', + where: { status: filter.status, tenant_code: filter.tenant_code }, // Ensure tenant isolation and citus compatibility + required: false, // LEFT JOIN to include entity types with no entities + }, + ], }) const result = entityTypes.map((entityType) => { - const matchingEntities = entities.filter((entity) => entity.entity_type_id === entityType.id) + const plainEntityType = entityType.get({ plain: true }) return { - ...entityType, - entities: matchingEntities, + ...plainEntityType, + entities: plainEntityType.entities || [], // alias is 'entities' } }) return result } catch (error) { - console.error('Error fetching data:', error) - throw error + console.error('Error fetching entity types and entities:', error) + throw new Error(`Failed to fetch data: ${error.message}`) } } diff --git a/src/services/entityType.js b/src/services/entityType.js index 2f4f604a0..566ed0a26 100644 --- a/src/services/entityType.js +++ b/src/services/entityType.js @@ -122,28 +122,38 @@ module.exports = class EntityHelper { static async readUserEntityTypes(body, userId, orgId, tenantCode) { try { - let defaultOrg = await organizationQueries.findOne( + // Fetch default organization ID (consider caching this) + const defaultOrg = await organizationQueries.findOne( { code: process.env.DEFAULT_ORGANISATION_CODE, tenant_code: tenantCode }, { attributes: ['id'] } ) - let defaultOrgId = defaultOrg.id + if (!defaultOrg) { + throw new Error('Default organization not found') + } + const defaultOrgId = defaultOrg.id + + // Include tenant_code in filter for consistency with schema const filter = { value: body.value, status: 'ACTIVE', + tenant_code: tenantCode, // Ensure tenant isolation organization_id: { [Op.in]: [orgId, defaultOrgId], }, } + const entities = await entityTypeQueries.findUserEntityTypesAndEntities(filter) + // Deduplicate entity types by value, prioritizing orgId const prunedEntities = removeDefaultOrgEntityTypes(entities, orgId) + return responses.successResponse({ statusCode: httpStatusCode.ok, message: 'ENTITY_TYPE_FETCHED_SUCCESSFULLY', result: { entity_types: prunedEntities }, }) } catch (error) { - console.log(error) + console.error('Error in readUserEntityTypes:', error) throw error } } From 17d7c7063500a9106f1cf94adc3de1b667c33d12 Mon Sep 17 00:00:00 2001 From: adithya_dinesh Date: Mon, 28 Jul 2025 13:53:11 +0530 Subject: [PATCH 03/55] fix organization code --- src/scripts/correct-org-code/fix-org-code.js | 220 +++++++++++++++++++ src/validators/v1/organization.js | 7 +- 2 files changed, 226 insertions(+), 1 deletion(-) create mode 100644 src/scripts/correct-org-code/fix-org-code.js diff --git a/src/scripts/correct-org-code/fix-org-code.js b/src/scripts/correct-org-code/fix-org-code.js new file mode 100644 index 000000000..7b4a8a646 --- /dev/null +++ b/src/scripts/correct-org-code/fix-org-code.js @@ -0,0 +1,220 @@ +const { Sequelize } = require('sequelize') +require('dotenv').config({ path: '../../.env' }) + +// Environment setup +const nodeEnv = process.env.NODE_ENV || 'development' + +let databaseUrl + +switch (nodeEnv) { + case 'production': + databaseUrl = process?.env?.PROD_DATABASE_URL || process.env.DEV_DATABASE_URL + break + case 'test': + databaseUrl = process?.env?.TEST_DATABASE_URL || process.env.DEV_DATABASE_URL + break + default: + databaseUrl = process.env.DEV_DATABASE_URL +} + +console.info('Database selected: ', databaseUrl.split('/').at(-1)) + +// Initialize Sequelize +const sequelize = new Sequelize(databaseUrl, { + dialect: 'postgres', + logging: process.env.NODE_ENV === 'development' ? console.log : false, +}) + +;(async () => { + let transaction + try { + // Start a transaction + transaction = await sequelize.transaction() + + const DISABLE_FK_QUERY = 'SET CONSTRAINTS ALL DEFERRED' // temporarily remove constraint checks till the transaction is completed + + const ORG_UPDATE_QUERY = `UPDATE organizations SET code = REGEXP_REPLACE(code, '\s+', '', 'g') WHERE code ~ '\s+';` + const ORG_FETCH_QUERY = `SELECT id, name, code FROM organizations WHERE code ~ '\s+';` + + // Test database connection + await sequelize.authenticate() + console.log('Database connection established successfully.') + await sequelize.query(DISABLE_FK_QUERY, { type: Sequelize.QueryTypes.RAW, raw: true, transaction }) + // Execute the query with replacements + const updateOrgs = await sequelize.query(ORG_UPDATE_QUERY, { + type: Sequelize.QueryTypes.UPDATE, + raw: true, + transaction, + }) + // Execute the query with replacements + const fetchOrgs = await sequelize.query(ORG_FETCH_QUERY, { + type: Sequelize.QueryTypes.UPDATE, + raw: true, + transaction, + }) + + console.log(fetchOrgs) + if (transaction) await transaction.rollback() + /* + const orgsToRemove = fetchOrgs + .filter((orgs) => orgs.code !== process.env.DEFAULT_ORGANISATION_CODE) + .map((orgs) => orgs.id) + const orgsCodesToRemove = fetchOrgs + .map((orgs) => orgs.code) + .filter((code) => code !== process.env.DEFAULT_ORGANISATION_CODE) + console.info('Organizations to remove: ', fetchOrgs) + + // Fetch system admin and org admin roles + const allowedRoles = ['admin', 'org_admin'] + const ROLES_FETCH_QUERY = + 'SELECT id FROM user_roles WHERE tenant_code IN (:allowedTenants) AND title IN (:allowedRoles)' + const fetchRoles = await sequelize.query(ROLES_FETCH_QUERY, { + replacements: { allowedTenants: allowed_tenants, allowedRoles }, + type: Sequelize.QueryTypes.SELECT, + raw: true, + transaction + }) + + const systemRoles = fetchRoles.map((role) => role.id) + console.log('System Role ids: ', systemRoles) + + const FETCH_USERID_QUERY = 'SELECT user_id FROM user_organization_roles WHERE role_id IN (:systemRoles)' + const fetchUserIds = await sequelize.query(FETCH_USERID_QUERY, { + replacements: { orgsCodesToRemove, systemRoles, allowedTenants: allowed_tenants }, + type: Sequelize.QueryTypes.SELECT, + raw: true, + transaction + }) + + const adminRoles = fetchUserIds.map((user) => user.user_id) + + const REFETCH_USERID_QUERY = + 'SELECT user_id FROM user_organization_roles WHERE user_id NOT IN (:adminRoles) OR organization_code IN (:orgsCodesToRemove)' + const reFetchUserIds = await sequelize.query(REFETCH_USERID_QUERY, { + replacements: { adminRoles, orgsCodesToRemove }, + type: Sequelize.QueryTypes.SELECT, + raw: true, + transaction + }) + + const userIdsToDelete = reFetchUserIds.map((user) => user.user_id) + console.log('User ids to delete: ', userIdsToDelete) + + // Delete user org roles + const DELETE_USER_ORG_ROLES_QUERY = 'DELETE FROM user_organization_roles WHERE user_id IN (:userIdsToDelete)' + await sequelize.query(DISABLE_FK_QUERY, { type: Sequelize.QueryTypes.RAW, raw: true, transaction }) + const deleteOrgUserRoles = await sequelize.query(DELETE_USER_ORG_ROLES_QUERY, { + replacements: { userIdsToDelete }, + type: Sequelize.QueryTypes.DELETE, + raw: true, + transaction + }) + + // Delete user organizations + const DELETE_USER_ORGS_QUERY = 'DELETE FROM user_organizations WHERE user_id IN (:userIdsToDelete)' + await sequelize.query(DISABLE_FK_QUERY, { type: Sequelize.QueryTypes.RAW, raw: true, transaction }) + const deleteOrgUsers = await sequelize.query(DELETE_USER_ORGS_QUERY, { + replacements: { userIdsToDelete }, + type: Sequelize.QueryTypes.DELETE, + raw: true, + transaction + }) + + // Delete users + const DELETE_USERS_QUERY = 'DELETE FROM users WHERE id IN (:userIdsToDelete)' + await sequelize.query(DISABLE_FK_QUERY, { type: Sequelize.QueryTypes.RAW, raw: true, transaction }) + const deleteUsers = await sequelize.query(DELETE_USERS_QUERY, { + replacements: { userIdsToDelete }, + type: Sequelize.QueryTypes.DELETE, + raw: true, + transaction + }) + + // Truncate uploads + const TRUNCATE_UPLOADS_QUERY = 'TRUNCATE TABLE file_uploads RESTART IDENTITY' + const truncateUploads = await sequelize.query(TRUNCATE_UPLOADS_QUERY, { + type: Sequelize.QueryTypes.RAW, + raw: true, + transaction + }) + + // Truncate invites + const TRUNCATE_INVITES_QUERY = 'TRUNCATE TABLE organization_user_invites RESTART IDENTITY' + const truncateInvites = await sequelize.query(TRUNCATE_INVITES_QUERY, { + type: Sequelize.QueryTypes.RAW, + raw: true, + transaction + }) + + // Delete organization features + const DELETE_ORG_FEATURES_QUERY = + 'DELETE FROM organization_features WHERE organization_code IN (:orgsCodesToRemove)' + await sequelize.query(DISABLE_FK_QUERY, { type: Sequelize.QueryTypes.RAW, raw: true, transaction }) + const deleteOrganizationFeatures = await sequelize.query(DELETE_ORG_FEATURES_QUERY, { + replacements: { orgsCodesToRemove }, + type: Sequelize.QueryTypes.DELETE, + raw: true, + transaction + }) + + // Delete organization domains + const DELETE_ORG_DOMAINS_QUERY = + 'DELETE FROM organization_email_domains WHERE organization_id IN (:orgsToRemove)' + await sequelize.query(DISABLE_FK_QUERY, { type: Sequelize.QueryTypes.RAW, raw: true, transaction }) + const deleteOrganizationDomains = await sequelize.query(DELETE_ORG_DOMAINS_QUERY, { + replacements: { orgsToRemove }, + type: Sequelize.QueryTypes.DELETE, + raw: true, + transaction + }) + + // Delete organization role requests + const DELETE_ORG_ROLE_REQUEST_QUERY = + 'DELETE FROM organization_role_requests WHERE organization_id IN (:orgsToRemove)' + const deleteOrganizatioRoleRequest = await sequelize.query(DELETE_ORG_ROLE_REQUEST_QUERY, { + replacements: { orgsToRemove }, + type: Sequelize.QueryTypes.DELETE, + raw: true, + transaction + }) + + // Delete organizations + const DELETE_ORGS_QUERY = 'DELETE FROM organizations WHERE id IN (:orgsToRemove)' + await sequelize.query(DISABLE_FK_QUERY, { type: Sequelize.QueryTypes.RAW, raw: true, transaction }) + const deleteOrganizations = await sequelize.query(DELETE_ORGS_QUERY, { + replacements: { orgsToRemove }, + type: Sequelize.QueryTypes.DELETE, + raw: true, + transaction + }) + + // Delete tenant domains + const DELETE_TENANT_DOMAINS_QUERY = 'DELETE FROM tenant_domains WHERE tenant_code NOT IN (:allowed_tenants)' + const deleteTenantDomains = await sequelize.query(DELETE_TENANT_DOMAINS_QUERY, { + replacements: { allowed_tenants }, + type: Sequelize.QueryTypes.DELETE, + raw: true, + transaction + }) + + // Delete tenants + const DELETE_TENANTS_QUERY = 'DELETE FROM tenants WHERE code NOT IN (:allowed_tenants)' + await sequelize.query(DISABLE_FK_QUERY, { type: Sequelize.QueryTypes.RAW, raw: true, transaction }) + const deleteTenants = await sequelize.query(DELETE_TENANTS_QUERY, { + replacements: { allowed_tenants }, + type: Sequelize.QueryTypes.DELETE, + raw: true, + transaction + }) + + // Commit the transaction + await transaction.commit(); */ + console.log('Transaction committed successfully.') + } catch (error) { + // Rollback transaction on error + if (transaction) await transaction.rollback() + console.error(`Error during transaction: ${error}`) + } finally { + sequelize.close() + } +})() diff --git a/src/validators/v1/organization.js b/src/validators/v1/organization.js index fe4beece9..fd80a1708 100644 --- a/src/validators/v1/organization.js +++ b/src/validators/v1/organization.js @@ -12,7 +12,12 @@ const common = require('@constants/common') module.exports = { create: (req) => { req.body = filterRequestBody(req.body, organization.create) - req.checkBody('code').trim().notEmpty().withMessage('code field is empty') + req.checkBody('code') + .trim() + .notEmpty() + .withMessage('code field is empty') + .matches(/^[a-z0-9]+$/) + .withMessage('code is invalid. Only lowercase alphanumeric characters allowed') req.checkBody('tenant_code').trim().notEmpty().withMessage('tenant_code field is empty') req.checkBody('registration_codes') .optional({ checkFalsy: true }) From 42126ecf6ca22007672780ba36fafe4dbdbf974c Mon Sep 17 00:00:00 2001 From: adithya_dinesh Date: Mon, 28 Jul 2025 18:51:19 +0530 Subject: [PATCH 04/55] final --- src/scripts/correct-org-code/fix-org-code.js | 272 ++++++++----------- 1 file changed, 114 insertions(+), 158 deletions(-) diff --git a/src/scripts/correct-org-code/fix-org-code.js b/src/scripts/correct-org-code/fix-org-code.js index 7b4a8a646..497304626 100644 --- a/src/scripts/correct-org-code/fix-org-code.js +++ b/src/scripts/correct-org-code/fix-org-code.js @@ -4,6 +4,8 @@ require('dotenv').config({ path: '../../.env' }) // Environment setup const nodeEnv = process.env.NODE_ENV || 'development' +let fk_retainer = [] +let table, fk_name, fkey, refTable, refKey let databaseUrl switch (nodeEnv) { @@ -33,182 +35,136 @@ const sequelize = new Sequelize(databaseUrl, { const DISABLE_FK_QUERY = 'SET CONSTRAINTS ALL DEFERRED' // temporarily remove constraint checks till the transaction is completed - const ORG_UPDATE_QUERY = `UPDATE organizations SET code = REGEXP_REPLACE(code, '\s+', '', 'g') WHERE code ~ '\s+';` const ORG_FETCH_QUERY = `SELECT id, name, code FROM organizations WHERE code ~ '\s+';` + const disableFK = (table, fk_name) => `ALTER TABLE ${table} DROP CONSTRAINT IF EXISTS ${fk_name};` + const enableFK = (table, fk_name, fkey, refTable, refKey) => + `ALTER TABLE ${table} ADD CONSTRAINT ${fk_name} FOREIGN KEY ${fkey} REFERENCES ${refTable} ${refKey} ON UPDATE NO ACTION ON DELETE CASCADE;` + const updateQuery = (table, key) => + `UPDATE ${table} SET ${key} = REGEXP_REPLACE(${key}, '\\s+', '', 'g') WHERE ${key} ~ '\\s+';` + // Execute the query with replacements + const fetchOrg = await sequelize.query(ORG_FETCH_QUERY, { + type: Sequelize.QueryTypes.SELECT, + raw: true, + transaction, + }) + + console.log(fetchOrg) // Test database connection await sequelize.authenticate() console.log('Database connection established successfully.') + + table = 'organization_registration_codes' + fk_name = 'fk_organization_code_tenant_code_in_org_reg_code' + fkey = '(organization_code, tenant_code)' + refTable = 'organizations' + refKey = '(code, tenant_code)' + fk_retainer.push(enableFK(table, fk_name, fkey, refTable, refKey)) + await sequelize.query(disableFK(table, fk_name), { type: Sequelize.QueryTypes.RAW, raw: true, transaction }) + await sequelize.query(DISABLE_FK_QUERY, { type: Sequelize.QueryTypes.RAW, raw: true, transaction }) + + table = 'user_organizations' + fk_name = 'fk_user_organizations_organizations' + fkey = '(organization_code, tenant_code)' + refTable = 'organizations' + refKey = '(code, tenant_code)' + await sequelize.query(disableFK(table, fk_name), { type: Sequelize.QueryTypes.RAW, raw: true, transaction }) + fk_retainer.push(enableFK(table, fk_name, fkey, refTable, refKey)) + + table = 'organization_user_invites' + fk_name = 'fk_org_user_invites_org_code' + fkey = '(organization_code, tenant_code)' + refTable = 'organizations' + refKey = '(code, tenant_code)' + await sequelize.query(disableFK(table, fk_name), { type: Sequelize.QueryTypes.RAW, raw: true, transaction }) + await sequelize.query(DISABLE_FK_QUERY, { type: Sequelize.QueryTypes.RAW, raw: true, transaction }) + fk_retainer.push(enableFK(table, fk_name, fkey, refTable, refKey)) + + table = 'organization_user_invites' + fk_name = 'fk_org_user_invites_organization_id' + fkey = '(organization_code, tenant_code)' + refTable = 'organizations' + refKey = '(code, tenant_code)' + await sequelize.query(disableFK(table, fk_name), { type: Sequelize.QueryTypes.RAW, raw: true, transaction }) + await sequelize.query(DISABLE_FK_QUERY, { type: Sequelize.QueryTypes.RAW, raw: true, transaction }) + fk_retainer.push(enableFK(table, fk_name, fkey, refTable, refKey)) + + table = 'user_organization_roles' + fk_name = 'fk_user_org_roles_user_organizations' + fkey = '(user_id, organization_code, tenant_code)' + refTable = 'user_organizations' + refKey = '(user_id, organization_code, tenant_code)' + await sequelize.query(disableFK(table, fk_name), { type: Sequelize.QueryTypes.RAW, raw: true, transaction }) await sequelize.query(DISABLE_FK_QUERY, { type: Sequelize.QueryTypes.RAW, raw: true, transaction }) + fk_retainer.push(enableFK(table, fk_name, fkey, refTable, refKey)) + + let updateTable = 'organizations' + let key = 'code' // Execute the query with replacements - const updateOrgs = await sequelize.query(ORG_UPDATE_QUERY, { + const updateOrgs = await sequelize.query(updateQuery(updateTable, key), { type: Sequelize.QueryTypes.UPDATE, raw: true, transaction, }) + console.log('------------>>', updateOrgs) + updateTable = 'organization_registration_codes' + key = 'organization_code' // Execute the query with replacements - const fetchOrgs = await sequelize.query(ORG_FETCH_QUERY, { + await sequelize.query(updateQuery(updateTable, key), { + type: Sequelize.QueryTypes.UPDATE, + raw: true, + transaction, + }) + + updateTable = 'organization_user_invites' + key = 'organization_code' + // Execute the query with replacements + await sequelize.query(updateQuery(updateTable, key), { type: Sequelize.QueryTypes.UPDATE, raw: true, transaction, }) + updateTable = 'user_organizations' + key = 'organization_code' + // Execute the query with replacements + await sequelize.query(updateQuery(updateTable, key), { + type: Sequelize.QueryTypes.UPDATE, + raw: true, + transaction, + }) + updateTable = 'user_organization_roles' + key = 'organization_code' + // Execute the query with replacements + await sequelize.query(updateQuery(updateTable, key), { + type: Sequelize.QueryTypes.UPDATE, + raw: true, + transaction, + }) + + // Execute the query with replacements + const fetchOrgs = await sequelize.query(ORG_FETCH_QUERY, { + type: Sequelize.QueryTypes.SELECT, + raw: true, + transaction, + }) + console.log(fetchOrgs) - if (transaction) await transaction.rollback() - /* - const orgsToRemove = fetchOrgs - .filter((orgs) => orgs.code !== process.env.DEFAULT_ORGANISATION_CODE) - .map((orgs) => orgs.id) - const orgsCodesToRemove = fetchOrgs - .map((orgs) => orgs.code) - .filter((code) => code !== process.env.DEFAULT_ORGANISATION_CODE) - console.info('Organizations to remove: ', fetchOrgs) - - // Fetch system admin and org admin roles - const allowedRoles = ['admin', 'org_admin'] - const ROLES_FETCH_QUERY = - 'SELECT id FROM user_roles WHERE tenant_code IN (:allowedTenants) AND title IN (:allowedRoles)' - const fetchRoles = await sequelize.query(ROLES_FETCH_QUERY, { - replacements: { allowedTenants: allowed_tenants, allowedRoles }, - type: Sequelize.QueryTypes.SELECT, - raw: true, - transaction - }) - - const systemRoles = fetchRoles.map((role) => role.id) - console.log('System Role ids: ', systemRoles) - - const FETCH_USERID_QUERY = 'SELECT user_id FROM user_organization_roles WHERE role_id IN (:systemRoles)' - const fetchUserIds = await sequelize.query(FETCH_USERID_QUERY, { - replacements: { orgsCodesToRemove, systemRoles, allowedTenants: allowed_tenants }, - type: Sequelize.QueryTypes.SELECT, - raw: true, - transaction - }) - - const adminRoles = fetchUserIds.map((user) => user.user_id) - - const REFETCH_USERID_QUERY = - 'SELECT user_id FROM user_organization_roles WHERE user_id NOT IN (:adminRoles) OR organization_code IN (:orgsCodesToRemove)' - const reFetchUserIds = await sequelize.query(REFETCH_USERID_QUERY, { - replacements: { adminRoles, orgsCodesToRemove }, - type: Sequelize.QueryTypes.SELECT, - raw: true, - transaction - }) - - const userIdsToDelete = reFetchUserIds.map((user) => user.user_id) - console.log('User ids to delete: ', userIdsToDelete) - - // Delete user org roles - const DELETE_USER_ORG_ROLES_QUERY = 'DELETE FROM user_organization_roles WHERE user_id IN (:userIdsToDelete)' - await sequelize.query(DISABLE_FK_QUERY, { type: Sequelize.QueryTypes.RAW, raw: true, transaction }) - const deleteOrgUserRoles = await sequelize.query(DELETE_USER_ORG_ROLES_QUERY, { - replacements: { userIdsToDelete }, - type: Sequelize.QueryTypes.DELETE, - raw: true, - transaction - }) - - // Delete user organizations - const DELETE_USER_ORGS_QUERY = 'DELETE FROM user_organizations WHERE user_id IN (:userIdsToDelete)' - await sequelize.query(DISABLE_FK_QUERY, { type: Sequelize.QueryTypes.RAW, raw: true, transaction }) - const deleteOrgUsers = await sequelize.query(DELETE_USER_ORGS_QUERY, { - replacements: { userIdsToDelete }, - type: Sequelize.QueryTypes.DELETE, - raw: true, - transaction - }) - - // Delete users - const DELETE_USERS_QUERY = 'DELETE FROM users WHERE id IN (:userIdsToDelete)' - await sequelize.query(DISABLE_FK_QUERY, { type: Sequelize.QueryTypes.RAW, raw: true, transaction }) - const deleteUsers = await sequelize.query(DELETE_USERS_QUERY, { - replacements: { userIdsToDelete }, - type: Sequelize.QueryTypes.DELETE, - raw: true, - transaction - }) - - // Truncate uploads - const TRUNCATE_UPLOADS_QUERY = 'TRUNCATE TABLE file_uploads RESTART IDENTITY' - const truncateUploads = await sequelize.query(TRUNCATE_UPLOADS_QUERY, { - type: Sequelize.QueryTypes.RAW, - raw: true, - transaction - }) - - // Truncate invites - const TRUNCATE_INVITES_QUERY = 'TRUNCATE TABLE organization_user_invites RESTART IDENTITY' - const truncateInvites = await sequelize.query(TRUNCATE_INVITES_QUERY, { - type: Sequelize.QueryTypes.RAW, - raw: true, - transaction - }) - - // Delete organization features - const DELETE_ORG_FEATURES_QUERY = - 'DELETE FROM organization_features WHERE organization_code IN (:orgsCodesToRemove)' - await sequelize.query(DISABLE_FK_QUERY, { type: Sequelize.QueryTypes.RAW, raw: true, transaction }) - const deleteOrganizationFeatures = await sequelize.query(DELETE_ORG_FEATURES_QUERY, { - replacements: { orgsCodesToRemove }, - type: Sequelize.QueryTypes.DELETE, - raw: true, - transaction - }) - - // Delete organization domains - const DELETE_ORG_DOMAINS_QUERY = - 'DELETE FROM organization_email_domains WHERE organization_id IN (:orgsToRemove)' - await sequelize.query(DISABLE_FK_QUERY, { type: Sequelize.QueryTypes.RAW, raw: true, transaction }) - const deleteOrganizationDomains = await sequelize.query(DELETE_ORG_DOMAINS_QUERY, { - replacements: { orgsToRemove }, - type: Sequelize.QueryTypes.DELETE, - raw: true, - transaction - }) - - // Delete organization role requests - const DELETE_ORG_ROLE_REQUEST_QUERY = - 'DELETE FROM organization_role_requests WHERE organization_id IN (:orgsToRemove)' - const deleteOrganizatioRoleRequest = await sequelize.query(DELETE_ORG_ROLE_REQUEST_QUERY, { - replacements: { orgsToRemove }, - type: Sequelize.QueryTypes.DELETE, - raw: true, - transaction - }) - - // Delete organizations - const DELETE_ORGS_QUERY = 'DELETE FROM organizations WHERE id IN (:orgsToRemove)' - await sequelize.query(DISABLE_FK_QUERY, { type: Sequelize.QueryTypes.RAW, raw: true, transaction }) - const deleteOrganizations = await sequelize.query(DELETE_ORGS_QUERY, { - replacements: { orgsToRemove }, - type: Sequelize.QueryTypes.DELETE, - raw: true, - transaction - }) - - // Delete tenant domains - const DELETE_TENANT_DOMAINS_QUERY = 'DELETE FROM tenant_domains WHERE tenant_code NOT IN (:allowed_tenants)' - const deleteTenantDomains = await sequelize.query(DELETE_TENANT_DOMAINS_QUERY, { - replacements: { allowed_tenants }, - type: Sequelize.QueryTypes.DELETE, - raw: true, - transaction - }) - - // Delete tenants - const DELETE_TENANTS_QUERY = 'DELETE FROM tenants WHERE code NOT IN (:allowed_tenants)' - await sequelize.query(DISABLE_FK_QUERY, { type: Sequelize.QueryTypes.RAW, raw: true, transaction }) - const deleteTenants = await sequelize.query(DELETE_TENANTS_QUERY, { - replacements: { allowed_tenants }, - type: Sequelize.QueryTypes.DELETE, - raw: true, - transaction - }) - - // Commit the transaction - await transaction.commit(); */ + let fk_retainerPromise = [] + for (let i = 0; i < fk_retainer.length; i++) { + fk_retainerPromise.push( + sequelize.query(fk_retainer[i], { + type: Sequelize.QueryTypes.UPDATE, + raw: true, + transaction, + }) + ) + } + + await Promise.all(fk_retainerPromise) + + if (transaction) await transaction.commit() + console.log('Transaction committed successfully.') } catch (error) { // Rollback transaction on error From d7d1fd592118b20e370b30fbf4a143f6e008781f Mon Sep 17 00:00:00 2001 From: adithya_dinesh Date: Tue, 29 Jul 2025 12:34:02 +0530 Subject: [PATCH 05/55] Converted file to migration --- .../migrations/20250729064710-org-code-fix.js | 168 +++++++++++++++++ src/scripts/correct-org-code/fix-org-code.js | 176 ------------------ 2 files changed, 168 insertions(+), 176 deletions(-) create mode 100644 src/database/migrations/20250729064710-org-code-fix.js delete mode 100644 src/scripts/correct-org-code/fix-org-code.js diff --git a/src/database/migrations/20250729064710-org-code-fix.js b/src/database/migrations/20250729064710-org-code-fix.js new file mode 100644 index 000000000..6b6217f28 --- /dev/null +++ b/src/database/migrations/20250729064710-org-code-fix.js @@ -0,0 +1,168 @@ +const { Sequelize } = require('sequelize') + +module.exports = { + async up(queryInterface, Sequelize) { + let transaction + let fk_retainer = [] + let table, fk_name, fkey, refTable, refKey + + try { + // Start a transaction + transaction = await queryInterface.sequelize.transaction() + + const ORG_FETCH_QUERY = `SELECT id, name, code FROM organizations WHERE code ~ '\s+';` + const disableFK = (table, fk_name) => `ALTER TABLE ${table} DROP CONSTRAINT IF EXISTS ${fk_name};` + const enableFK = (table, fk_name, fkey, refTable, refKey) => + `ALTER TABLE ${table} ADD CONSTRAINT ${fk_name} FOREIGN KEY ${fkey} REFERENCES ${refTable} ${refKey} ON UPDATE NO ACTION ON DELETE CASCADE;` + const updateQuery = (table, key) => + `UPDATE ${table} SET ${key} = REGEXP_REPLACE(${key}, '\\s+', '', 'g') WHERE ${key} ~ '\\s+';` + + // Execute the query to fetch organizations with whitespace + const fetchOrg = await queryInterface.sequelize.query(ORG_FETCH_QUERY, { + type: Sequelize.QueryTypes.SELECT, + raw: true, + transaction, + }) + console.log(fetchOrg) + + // Disable foreign key constraints and store enable queries + table = 'organization_registration_codes' + fk_name = 'fk_organization_code_tenant_code_in_org_reg_code' + fkey = '(organization_code, tenant_code)' + refTable = 'organizations' + refKey = '(code, tenant_code)' + fk_retainer.push(enableFK(table, fk_name, fkey, refTable, refKey)) + await queryInterface.sequelize.query(disableFK(table, fk_name), { + type: Sequelize.QueryTypes.RAW, + raw: true, + transaction, + }) + + table = 'user_organizations' + fk_name = 'fk_user_organizations_organizations' + fkey = '(organization_code, tenant_code)' + refTable = 'organizations' + refKey = '(code, tenant_code)' + await queryInterface.sequelize.query(disableFK(table, fk_name), { + type: Sequelize.QueryTypes.RAW, + raw: true, + transaction, + }) + fk_retainer.push(enableFK(table, fk_name, fkey, refTable, refKey)) + + table = 'organization_user_invites' + fk_name = 'fk_org_user_invites_org_code' + fkey = '(organization_code, tenant_code)' + refTable = 'organizations' + refKey = '(code, tenant_code)' + await queryInterface.sequelize.query(disableFK(table, fk_name), { + type: Sequelize.QueryTypes.RAW, + raw: true, + transaction, + }) + fk_retainer.push(enableFK(table, fk_name, fkey, refTable, refKey)) + + table = 'organization_user_invites' + fk_name = 'fk_org_user_invites_organization_id' + fkey = '(organization_code, tenant_code)' + refTable = 'organizations' + refKey = '(code, tenant_code)' + await queryInterface.sequelize.query(disableFK(table, fk_name), { + type: Sequelize.QueryTypes.RAW, + raw: true, + transaction, + }) + fk_retainer.push(enableFK(table, fk_name, fkey, refTable, refKey)) + + table = 'user_organization_roles' + fk_name = 'fk_user_org_roles_user_organizations' + fkey = '(user_id, organization_code, tenant_code)' + refTable = 'user_organizations' + refKey = '(user_id, organization_code, tenant_code)' + await queryInterface.sequelize.query(disableFK(table, fk_name), { + type: Sequelize.QueryTypes.RAW, + raw: true, + transaction, + }) + fk_retainer.push(enableFK(table, fk_name, fkey, refTable, refKey)) + + // Update tables to remove whitespace + let updateTable = 'organizations' + let key = 'code' + const updateOrgs = await queryInterface.sequelize.query(updateQuery(updateTable, key), { + type: Sequelize.QueryTypes.UPDATE, + raw: true, + transaction, + }) + + updateTable = 'organization_registration_codes' + key = 'organization_code' + await queryInterface.sequelize.query(updateQuery(updateTable, key), { + type: Sequelize.QueryTypes.UPDATE, + raw: true, + transaction, + }) + + updateTable = 'organization_user_invites' + key = 'organization_code' + await queryInterface.sequelize.query(updateQuery(updateTable, key), { + type: Sequelize.QueryTypes.UPDATE, + raw: true, + transaction, + }) + + updateTable = 'user_organizations' + key = 'organization_code' + await queryInterface.sequelize.query(updateQuery(updateTable, key), { + type: Sequelize.QueryTypes.UPDATE, + raw: true, + transaction, + }) + + updateTable = 'user_organization_roles' + key = 'organization_code' + await queryInterface.sequelize.query(updateQuery(updateTable, key), { + type: Sequelize.QueryTypes.UPDATE, + raw: true, + transaction, + }) + + // Verify the update + const fetchOrgs = await queryInterface.sequelize.query(ORG_FETCH_QUERY, { + type: Sequelize.QueryTypes.SELECT, + raw: true, + transaction, + }) + console.log(fetchOrgs) + + // Re-enable foreign key constraints + let fk_retainerPromise = [] + for (let i = 0; i < fk_retainer.length; i++) { + fk_retainerPromise.push( + queryInterface.sequelize.query(fk_retainer[i], { + type: Sequelize.QueryTypes.RAW, + raw: true, + transaction, + }) + ) + } + + await Promise.all(fk_retainerPromise) + + // Commit the transaction + await transaction.commit() + console.log('Transaction committed successfully.') + } catch (error) { + // Rollback transaction on error + if (transaction) await transaction.rollback() + console.error(`Error during transaction: ${error}`) + throw error + } + }, + + async down(queryInterface, Sequelize) { + console.warn( + 'Down migration not implemented: Cannot reliably restore original whitespace in organization codes.' + ) + }, +} diff --git a/src/scripts/correct-org-code/fix-org-code.js b/src/scripts/correct-org-code/fix-org-code.js deleted file mode 100644 index 497304626..000000000 --- a/src/scripts/correct-org-code/fix-org-code.js +++ /dev/null @@ -1,176 +0,0 @@ -const { Sequelize } = require('sequelize') -require('dotenv').config({ path: '../../.env' }) - -// Environment setup -const nodeEnv = process.env.NODE_ENV || 'development' - -let fk_retainer = [] -let table, fk_name, fkey, refTable, refKey -let databaseUrl - -switch (nodeEnv) { - case 'production': - databaseUrl = process?.env?.PROD_DATABASE_URL || process.env.DEV_DATABASE_URL - break - case 'test': - databaseUrl = process?.env?.TEST_DATABASE_URL || process.env.DEV_DATABASE_URL - break - default: - databaseUrl = process.env.DEV_DATABASE_URL -} - -console.info('Database selected: ', databaseUrl.split('/').at(-1)) - -// Initialize Sequelize -const sequelize = new Sequelize(databaseUrl, { - dialect: 'postgres', - logging: process.env.NODE_ENV === 'development' ? console.log : false, -}) - -;(async () => { - let transaction - try { - // Start a transaction - transaction = await sequelize.transaction() - - const DISABLE_FK_QUERY = 'SET CONSTRAINTS ALL DEFERRED' // temporarily remove constraint checks till the transaction is completed - - const ORG_FETCH_QUERY = `SELECT id, name, code FROM organizations WHERE code ~ '\s+';` - const disableFK = (table, fk_name) => `ALTER TABLE ${table} DROP CONSTRAINT IF EXISTS ${fk_name};` - const enableFK = (table, fk_name, fkey, refTable, refKey) => - `ALTER TABLE ${table} ADD CONSTRAINT ${fk_name} FOREIGN KEY ${fkey} REFERENCES ${refTable} ${refKey} ON UPDATE NO ACTION ON DELETE CASCADE;` - const updateQuery = (table, key) => - `UPDATE ${table} SET ${key} = REGEXP_REPLACE(${key}, '\\s+', '', 'g') WHERE ${key} ~ '\\s+';` - - // Execute the query with replacements - const fetchOrg = await sequelize.query(ORG_FETCH_QUERY, { - type: Sequelize.QueryTypes.SELECT, - raw: true, - transaction, - }) - - console.log(fetchOrg) - // Test database connection - await sequelize.authenticate() - console.log('Database connection established successfully.') - - table = 'organization_registration_codes' - fk_name = 'fk_organization_code_tenant_code_in_org_reg_code' - fkey = '(organization_code, tenant_code)' - refTable = 'organizations' - refKey = '(code, tenant_code)' - fk_retainer.push(enableFK(table, fk_name, fkey, refTable, refKey)) - await sequelize.query(disableFK(table, fk_name), { type: Sequelize.QueryTypes.RAW, raw: true, transaction }) - await sequelize.query(DISABLE_FK_QUERY, { type: Sequelize.QueryTypes.RAW, raw: true, transaction }) - - table = 'user_organizations' - fk_name = 'fk_user_organizations_organizations' - fkey = '(organization_code, tenant_code)' - refTable = 'organizations' - refKey = '(code, tenant_code)' - await sequelize.query(disableFK(table, fk_name), { type: Sequelize.QueryTypes.RAW, raw: true, transaction }) - fk_retainer.push(enableFK(table, fk_name, fkey, refTable, refKey)) - - table = 'organization_user_invites' - fk_name = 'fk_org_user_invites_org_code' - fkey = '(organization_code, tenant_code)' - refTable = 'organizations' - refKey = '(code, tenant_code)' - await sequelize.query(disableFK(table, fk_name), { type: Sequelize.QueryTypes.RAW, raw: true, transaction }) - await sequelize.query(DISABLE_FK_QUERY, { type: Sequelize.QueryTypes.RAW, raw: true, transaction }) - fk_retainer.push(enableFK(table, fk_name, fkey, refTable, refKey)) - - table = 'organization_user_invites' - fk_name = 'fk_org_user_invites_organization_id' - fkey = '(organization_code, tenant_code)' - refTable = 'organizations' - refKey = '(code, tenant_code)' - await sequelize.query(disableFK(table, fk_name), { type: Sequelize.QueryTypes.RAW, raw: true, transaction }) - await sequelize.query(DISABLE_FK_QUERY, { type: Sequelize.QueryTypes.RAW, raw: true, transaction }) - fk_retainer.push(enableFK(table, fk_name, fkey, refTable, refKey)) - - table = 'user_organization_roles' - fk_name = 'fk_user_org_roles_user_organizations' - fkey = '(user_id, organization_code, tenant_code)' - refTable = 'user_organizations' - refKey = '(user_id, organization_code, tenant_code)' - await sequelize.query(disableFK(table, fk_name), { type: Sequelize.QueryTypes.RAW, raw: true, transaction }) - await sequelize.query(DISABLE_FK_QUERY, { type: Sequelize.QueryTypes.RAW, raw: true, transaction }) - fk_retainer.push(enableFK(table, fk_name, fkey, refTable, refKey)) - - let updateTable = 'organizations' - let key = 'code' - // Execute the query with replacements - const updateOrgs = await sequelize.query(updateQuery(updateTable, key), { - type: Sequelize.QueryTypes.UPDATE, - raw: true, - transaction, - }) - console.log('------------>>', updateOrgs) - updateTable = 'organization_registration_codes' - key = 'organization_code' - // Execute the query with replacements - await sequelize.query(updateQuery(updateTable, key), { - type: Sequelize.QueryTypes.UPDATE, - raw: true, - transaction, - }) - - updateTable = 'organization_user_invites' - key = 'organization_code' - // Execute the query with replacements - await sequelize.query(updateQuery(updateTable, key), { - type: Sequelize.QueryTypes.UPDATE, - raw: true, - transaction, - }) - - updateTable = 'user_organizations' - key = 'organization_code' - // Execute the query with replacements - await sequelize.query(updateQuery(updateTable, key), { - type: Sequelize.QueryTypes.UPDATE, - raw: true, - transaction, - }) - updateTable = 'user_organization_roles' - key = 'organization_code' - // Execute the query with replacements - await sequelize.query(updateQuery(updateTable, key), { - type: Sequelize.QueryTypes.UPDATE, - raw: true, - transaction, - }) - - // Execute the query with replacements - const fetchOrgs = await sequelize.query(ORG_FETCH_QUERY, { - type: Sequelize.QueryTypes.SELECT, - raw: true, - transaction, - }) - - console.log(fetchOrgs) - let fk_retainerPromise = [] - for (let i = 0; i < fk_retainer.length; i++) { - fk_retainerPromise.push( - sequelize.query(fk_retainer[i], { - type: Sequelize.QueryTypes.UPDATE, - raw: true, - transaction, - }) - ) - } - - await Promise.all(fk_retainerPromise) - - if (transaction) await transaction.commit() - - console.log('Transaction committed successfully.') - } catch (error) { - // Rollback transaction on error - if (transaction) await transaction.rollback() - console.error(`Error during transaction: ${error}`) - } finally { - sequelize.close() - } -})() From 2b3a44d43584871377ace754d04ebf28cd6dc32d Mon Sep 17 00:00:00 2001 From: adithya_dinesh Date: Tue, 29 Jul 2025 12:42:04 +0530 Subject: [PATCH 06/55] minor fixes --- .../migrations/20250729064710-org-code-fix.js | 257 +++++++++--------- 1 file changed, 129 insertions(+), 128 deletions(-) diff --git a/src/database/migrations/20250729064710-org-code-fix.js b/src/database/migrations/20250729064710-org-code-fix.js index 6b6217f28..72a5a3908 100644 --- a/src/database/migrations/20250729064710-org-code-fix.js +++ b/src/database/migrations/20250729064710-org-code-fix.js @@ -10,7 +10,7 @@ module.exports = { // Start a transaction transaction = await queryInterface.sequelize.transaction() - const ORG_FETCH_QUERY = `SELECT id, name, code FROM organizations WHERE code ~ '\s+';` + const ORG_FETCH_QUERY = `SELECT id, name, code FROM organizations WHERE code ~ '\\s+';` const disableFK = (table, fk_name) => `ALTER TABLE ${table} DROP CONSTRAINT IF EXISTS ${fk_name};` const enableFK = (table, fk_name, fkey, refTable, refKey) => `ALTER TABLE ${table} ADD CONSTRAINT ${fk_name} FOREIGN KEY ${fkey} REFERENCES ${refTable} ${refKey} ON UPDATE NO ACTION ON DELETE CASCADE;` @@ -23,135 +23,136 @@ module.exports = { raw: true, transaction, }) - console.log(fetchOrg) - - // Disable foreign key constraints and store enable queries - table = 'organization_registration_codes' - fk_name = 'fk_organization_code_tenant_code_in_org_reg_code' - fkey = '(organization_code, tenant_code)' - refTable = 'organizations' - refKey = '(code, tenant_code)' - fk_retainer.push(enableFK(table, fk_name, fkey, refTable, refKey)) - await queryInterface.sequelize.query(disableFK(table, fk_name), { - type: Sequelize.QueryTypes.RAW, - raw: true, - transaction, - }) - - table = 'user_organizations' - fk_name = 'fk_user_organizations_organizations' - fkey = '(organization_code, tenant_code)' - refTable = 'organizations' - refKey = '(code, tenant_code)' - await queryInterface.sequelize.query(disableFK(table, fk_name), { - type: Sequelize.QueryTypes.RAW, - raw: true, - transaction, - }) - fk_retainer.push(enableFK(table, fk_name, fkey, refTable, refKey)) - - table = 'organization_user_invites' - fk_name = 'fk_org_user_invites_org_code' - fkey = '(organization_code, tenant_code)' - refTable = 'organizations' - refKey = '(code, tenant_code)' - await queryInterface.sequelize.query(disableFK(table, fk_name), { - type: Sequelize.QueryTypes.RAW, - raw: true, - transaction, - }) - fk_retainer.push(enableFK(table, fk_name, fkey, refTable, refKey)) - - table = 'organization_user_invites' - fk_name = 'fk_org_user_invites_organization_id' - fkey = '(organization_code, tenant_code)' - refTable = 'organizations' - refKey = '(code, tenant_code)' - await queryInterface.sequelize.query(disableFK(table, fk_name), { - type: Sequelize.QueryTypes.RAW, - raw: true, - transaction, - }) - fk_retainer.push(enableFK(table, fk_name, fkey, refTable, refKey)) - - table = 'user_organization_roles' - fk_name = 'fk_user_org_roles_user_organizations' - fkey = '(user_id, organization_code, tenant_code)' - refTable = 'user_organizations' - refKey = '(user_id, organization_code, tenant_code)' - await queryInterface.sequelize.query(disableFK(table, fk_name), { - type: Sequelize.QueryTypes.RAW, - raw: true, - transaction, - }) - fk_retainer.push(enableFK(table, fk_name, fkey, refTable, refKey)) - - // Update tables to remove whitespace - let updateTable = 'organizations' - let key = 'code' - const updateOrgs = await queryInterface.sequelize.query(updateQuery(updateTable, key), { - type: Sequelize.QueryTypes.UPDATE, - raw: true, - transaction, - }) - - updateTable = 'organization_registration_codes' - key = 'organization_code' - await queryInterface.sequelize.query(updateQuery(updateTable, key), { - type: Sequelize.QueryTypes.UPDATE, - raw: true, - transaction, - }) - - updateTable = 'organization_user_invites' - key = 'organization_code' - await queryInterface.sequelize.query(updateQuery(updateTable, key), { - type: Sequelize.QueryTypes.UPDATE, - raw: true, - transaction, - }) - - updateTable = 'user_organizations' - key = 'organization_code' - await queryInterface.sequelize.query(updateQuery(updateTable, key), { - type: Sequelize.QueryTypes.UPDATE, - raw: true, - transaction, - }) - updateTable = 'user_organization_roles' - key = 'organization_code' - await queryInterface.sequelize.query(updateQuery(updateTable, key), { - type: Sequelize.QueryTypes.UPDATE, - raw: true, - transaction, - }) - - // Verify the update - const fetchOrgs = await queryInterface.sequelize.query(ORG_FETCH_QUERY, { - type: Sequelize.QueryTypes.SELECT, - raw: true, - transaction, - }) - console.log(fetchOrgs) - - // Re-enable foreign key constraints - let fk_retainerPromise = [] - for (let i = 0; i < fk_retainer.length; i++) { - fk_retainerPromise.push( - queryInterface.sequelize.query(fk_retainer[i], { - type: Sequelize.QueryTypes.RAW, - raw: true, - transaction, - }) - ) + if (fetchOrg.length > 0) { + // Disable foreign key constraints and store enable queries + table = 'organization_registration_codes' + fk_name = 'fk_organization_code_tenant_code_in_org_reg_code' + fkey = '(organization_code, tenant_code)' + refTable = 'organizations' + refKey = '(code, tenant_code)' + fk_retainer.push(enableFK(table, fk_name, fkey, refTable, refKey)) + await queryInterface.sequelize.query(disableFK(table, fk_name), { + type: Sequelize.QueryTypes.RAW, + raw: true, + transaction, + }) + + table = 'user_organizations' + fk_name = 'fk_user_organizations_organizations' + fkey = '(organization_code, tenant_code)' + refTable = 'organizations' + refKey = '(code, tenant_code)' + await queryInterface.sequelize.query(disableFK(table, fk_name), { + type: Sequelize.QueryTypes.RAW, + raw: true, + transaction, + }) + fk_retainer.push(enableFK(table, fk_name, fkey, refTable, refKey)) + + table = 'organization_user_invites' + fk_name = 'fk_org_user_invites_org_code' + fkey = '(organization_code, tenant_code)' + refTable = 'organizations' + refKey = '(code, tenant_code)' + await queryInterface.sequelize.query(disableFK(table, fk_name), { + type: Sequelize.QueryTypes.RAW, + raw: true, + transaction, + }) + fk_retainer.push(enableFK(table, fk_name, fkey, refTable, refKey)) + + table = 'organization_user_invites' + fk_name = 'fk_org_user_invites_organization_id' + fkey = '(organization_code, tenant_code)' + refTable = 'organizations' + refKey = '(code, tenant_code)' + await queryInterface.sequelize.query(disableFK(table, fk_name), { + type: Sequelize.QueryTypes.RAW, + raw: true, + transaction, + }) + fk_retainer.push(enableFK(table, fk_name, fkey, refTable, refKey)) + + table = 'user_organization_roles' + fk_name = 'fk_user_org_roles_user_organizations' + fkey = '(user_id, organization_code, tenant_code)' + refTable = 'user_organizations' + refKey = '(user_id, organization_code, tenant_code)' + await queryInterface.sequelize.query(disableFK(table, fk_name), { + type: Sequelize.QueryTypes.RAW, + raw: true, + transaction, + }) + fk_retainer.push(enableFK(table, fk_name, fkey, refTable, refKey)) + + // Update tables to remove whitespace + let updateTable = 'organizations' + let key = 'code' + const updateOrgs = await queryInterface.sequelize.query(updateQuery(updateTable, key), { + type: Sequelize.QueryTypes.UPDATE, + raw: true, + transaction, + }) + + updateTable = 'organization_registration_codes' + key = 'organization_code' + await queryInterface.sequelize.query(updateQuery(updateTable, key), { + type: Sequelize.QueryTypes.UPDATE, + raw: true, + transaction, + }) + + updateTable = 'organization_user_invites' + key = 'organization_code' + await queryInterface.sequelize.query(updateQuery(updateTable, key), { + type: Sequelize.QueryTypes.UPDATE, + raw: true, + transaction, + }) + + updateTable = 'user_organizations' + key = 'organization_code' + await queryInterface.sequelize.query(updateQuery(updateTable, key), { + type: Sequelize.QueryTypes.UPDATE, + raw: true, + transaction, + }) + + updateTable = 'user_organization_roles' + key = 'organization_code' + await queryInterface.sequelize.query(updateQuery(updateTable, key), { + type: Sequelize.QueryTypes.UPDATE, + raw: true, + transaction, + }) + + // Verify the update + const fetchOrgs = await queryInterface.sequelize.query(ORG_FETCH_QUERY, { + type: Sequelize.QueryTypes.SELECT, + raw: true, + transaction, + }) + console.log(fetchOrgs) + + // Re-enable foreign key constraints + let fk_retainerPromise = [] + for (let i = 0; i < fk_retainer.length; i++) { + fk_retainerPromise.push( + queryInterface.sequelize.query(fk_retainer[i], { + type: Sequelize.QueryTypes.RAW, + raw: true, + transaction, + }) + ) + } + + await Promise.all(fk_retainerPromise) + + // Commit the transaction + await transaction.commit() + console.log('Transaction committed successfully.') } - - await Promise.all(fk_retainerPromise) - - // Commit the transaction - await transaction.commit() - console.log('Transaction committed successfully.') } catch (error) { // Rollback transaction on error if (transaction) await transaction.rollback() From 9d1494e83adbafb646c67347126c1bfc3f197558 Mon Sep 17 00:00:00 2001 From: adithya_dinesh Date: Tue, 29 Jul 2025 17:49:54 +0530 Subject: [PATCH 07/55] review comments addressed --- src/database/migrations/20250729064710-org-code-fix.js | 4 ++-- src/routes/index.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/database/migrations/20250729064710-org-code-fix.js b/src/database/migrations/20250729064710-org-code-fix.js index 72a5a3908..a1b623b38 100644 --- a/src/database/migrations/20250729064710-org-code-fix.js +++ b/src/database/migrations/20250729064710-org-code-fix.js @@ -10,12 +10,12 @@ module.exports = { // Start a transaction transaction = await queryInterface.sequelize.transaction() - const ORG_FETCH_QUERY = `SELECT id, name, code FROM organizations WHERE code ~ '\\s+';` + const ORG_FETCH_QUERY = `SELECT id, name, code FROM organizations WHERE code ~ '\\s+' OR code ~ '[A-Z]';` const disableFK = (table, fk_name) => `ALTER TABLE ${table} DROP CONSTRAINT IF EXISTS ${fk_name};` const enableFK = (table, fk_name, fkey, refTable, refKey) => `ALTER TABLE ${table} ADD CONSTRAINT ${fk_name} FOREIGN KEY ${fkey} REFERENCES ${refTable} ${refKey} ON UPDATE NO ACTION ON DELETE CASCADE;` const updateQuery = (table, key) => - `UPDATE ${table} SET ${key} = REGEXP_REPLACE(${key}, '\\s+', '', 'g') WHERE ${key} ~ '\\s+';` + `UPDATE ${table} SET ${key} = LOWER(REGEXP_REPLACE(${key}, '\\s+', '_', 'g')) WHERE ${key} ~ '[A-Z|\\s+]';` // Execute the query to fetch organizations with whitespace const fetchOrg = await queryInterface.sequelize.query(ORG_FETCH_QUERY, { diff --git a/src/routes/index.js b/src/routes/index.js index ab0e329f5..552a20b55 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -70,7 +70,7 @@ module.exports = (app) => { const version = (req.params.version.match(/^v\d+$/) || [])[0] // Match version like v1, v2, etc. const controllerName = (req.params.controller.match(/^[a-zA-Z0-9_-]+$/) || [])[0] // Allow only alphanumeric characters, underscore, and hyphen const file = req.params.file ? (req.params.file.match(/^[a-zA-Z0-9_-]+$/) || [])[0] : null // Same validation as controller, or null if file is not provided - const method = (req.params.method.match(/^[a-zA-Z0-9]+$/) || [])[0] // Allow only alphanumeric characters + const method = (req.params.method.match(/^[a-zA-Z0-9_-]+$/) || [])[0] // Allow only alphanumeric characters try { if (!version || !controllerName || !method || (req.params.file && !file)) { // Invalid input, return an error response From 0317a0bda2d6648f29121319519d049d71436ab2 Mon Sep 17 00:00:00 2001 From: adithya_dinesh Date: Tue, 29 Jul 2025 17:54:55 +0530 Subject: [PATCH 08/55] review comments addressed --- src/database/migrations/20250729064710-org-code-fix.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/database/migrations/20250729064710-org-code-fix.js b/src/database/migrations/20250729064710-org-code-fix.js index a1b623b38..008365c83 100644 --- a/src/database/migrations/20250729064710-org-code-fix.js +++ b/src/database/migrations/20250729064710-org-code-fix.js @@ -72,7 +72,6 @@ module.exports = { raw: true, transaction, }) - fk_retainer.push(enableFK(table, fk_name, fkey, refTable, refKey)) table = 'user_organization_roles' fk_name = 'fk_user_org_roles_user_organizations' From 57084411b6fa2095aa5ff56118c5407f407aa72f Mon Sep 17 00:00:00 2001 From: adithya_dinesh Date: Tue, 29 Jul 2025 17:59:15 +0530 Subject: [PATCH 09/55] review comments addressed --- .../migrations/20250729064710-org-code-fix.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/database/migrations/20250729064710-org-code-fix.js b/src/database/migrations/20250729064710-org-code-fix.js index 008365c83..29ee00427 100644 --- a/src/database/migrations/20250729064710-org-code-fix.js +++ b/src/database/migrations/20250729064710-org-code-fix.js @@ -85,6 +85,18 @@ module.exports = { }) fk_retainer.push(enableFK(table, fk_name, fkey, refTable, refKey)) + table = 'organization_features' + fk_name = 'fk_org_features_organization' + fkey = '(organization_code, tenant_code)' + refTable = 'organizations' + refKey = '(code, tenant_code)' + await queryInterface.sequelize.query(disableFK(table, fk_name), { + type: Sequelize.QueryTypes.RAW, + raw: true, + transaction, + }) + fk_retainer.push(enableFK(table, fk_name, fkey, refTable, refKey)) + // Update tables to remove whitespace let updateTable = 'organizations' let key = 'code' @@ -125,6 +137,13 @@ module.exports = { raw: true, transaction, }) + updateTable = 'organization_features' + key = 'organization_code' + await queryInterface.sequelize.query(updateQuery(updateTable, key), { + type: Sequelize.QueryTypes.UPDATE, + raw: true, + transaction, + }) // Verify the update const fetchOrgs = await queryInterface.sequelize.query(ORG_FETCH_QUERY, { From c29c5af948158e7ef1ffe48eaa469a17f297a1da Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Wed, 30 Jul 2025 00:16:25 +0530 Subject: [PATCH 10/55] updated: entity type and entities flows --- src/controllers/v1/entity-type.js | 12 +- src/controllers/v1/entity.js | 15 +- src/controllers/v1/user.js | 4 +- ...ation-code-to-entities-and-entity-types.js | 188 ++++++++++++++++++ ...0723183753-add-entity-types-constraints.js | 75 +++++++ src/database/models/entities.js | 4 + src/database/models/entityType.js | 8 +- src/database/queries/entities.js | 31 ++- src/database/queries/entityType.js | 15 +- src/services/account.js | 53 ++--- src/services/entities.js | 106 ++++------ src/services/entityType.js | 53 ++--- src/services/tenant.js | 19 +- src/services/user.js | 53 ++--- src/validators/v1/entity-type.js | 3 +- src/validators/v1/entity.js | 25 ++- 16 files changed, 454 insertions(+), 210 deletions(-) create mode 100644 src/database/migrations/20250717071710-add-organization-code-to-entities-and-entity-types.js create mode 100644 src/database/migrations/20250723183753-add-entity-types-constraints.js diff --git a/src/controllers/v1/entity-type.js b/src/controllers/v1/entity-type.js index 91e9493bc..7cfc5866e 100644 --- a/src/controllers/v1/entity-type.js +++ b/src/controllers/v1/entity-type.js @@ -15,6 +15,7 @@ module.exports = class Entity { return await entityTypeService.create( req.body, req.decodedToken.id, + req.decodedToken.organization_code, req.decodedToken.organization_id, req.decodedToken.tenant_code ) @@ -37,7 +38,7 @@ module.exports = class Entity { req.body, req.params.id, req.decodedToken.id, - req.decodedToken.organization_id, + req.decodedToken.organization_code, req.decodedToken.tenant_code ) } catch (error) { @@ -58,13 +59,14 @@ module.exports = class Entity { if (req.body.value) { return await entityTypeService.readUserEntityTypes( req.body, - req.decodedToken.id, - req.decodedToken.organization_id, - req.decodedToken.tenant_code + req.decodedToken.organization_code, + req.decodedToken.tenant_code, + req.decodedToken.organization_id ) } return await entityTypeService.readAllSystemEntityTypes( req.decodedToken.organization_id, + req.decodedToken.organization_code, req.decodedToken.tenant_code ) } catch (error) { @@ -82,7 +84,7 @@ module.exports = class Entity { async delete(req) { try { - return await entityTypeService.delete(req.params.id, req.decodedToken.organization_id) + return await entityTypeService.delete(req.params.id, req.decodedToken.organization_code) } catch (error) { return error } diff --git a/src/controllers/v1/entity.js b/src/controllers/v1/entity.js index c2a135754..133fb0a77 100644 --- a/src/controllers/v1/entity.js +++ b/src/controllers/v1/entity.js @@ -22,7 +22,8 @@ module.exports = class Entity { const createdEntity = await entityService.create( req.body, req.decodedToken.id, - req.decodedToken.tenant_code + req.decodedToken.tenant_code, + req.decodedToken.organization_code ) return createdEntity } catch (error) { @@ -44,7 +45,8 @@ module.exports = class Entity { req.body, req.params.id, req.decodedToken.id, - req.decodedToken.roles + req.decodedToken.organization_code, + req.decodedToken.tenant_code ) return updatedEntity } catch (error) { @@ -63,9 +65,8 @@ module.exports = class Entity { async read(req) { try { if (req.query.id || req.query.value) { - return await entityService.read(req.query, req.decodedToken.id) + return await entityService.read(req.query, req.decodedToken.tenant_code) } - return await entityService.readAll(req.query, null) } catch (error) { return error } @@ -81,7 +82,11 @@ module.exports = class Entity { async delete(req) { try { - const updatedEntity = await entityService.delete(req.params.id, req.decodedToken.id) + const updatedEntity = await entityService.delete( + req.params.id, + req.decodedToken.organization_code, + req.decodedToken.tenant_code + ) return updatedEntity } catch (error) { return error diff --git a/src/controllers/v1/user.js b/src/controllers/v1/user.js index bb67a990b..160a9911b 100644 --- a/src/controllers/v1/user.js +++ b/src/controllers/v1/user.js @@ -24,7 +24,7 @@ module.exports = class User { const updatedUser = await userService.update( params, req.decodedToken.id, - req.decodedToken.organization_id, + req.decodedToken.organization_code, req.decodedToken.tenant_code ) return updatedUser @@ -118,7 +118,7 @@ module.exports = class User { const updateUsersLanguagePreference = await userService.setLanguagePreference( params, req.decodedToken.id, - req.decodedToken.organization_id, + req.decodedToken.organization_code, req.decodedToken.tenant_code ) return updateUsersLanguagePreference diff --git a/src/database/migrations/20250717071710-add-organization-code-to-entities-and-entity-types.js b/src/database/migrations/20250717071710-add-organization-code-to-entities-and-entity-types.js new file mode 100644 index 000000000..f21cd7c22 --- /dev/null +++ b/src/database/migrations/20250717071710-add-organization-code-to-entities-and-entity-types.js @@ -0,0 +1,188 @@ +'use strict' + +module.exports = { + up: async (queryInterface, Sequelize) => { + const transaction = await queryInterface.sequelize.transaction() + try { + // 1. Add organization_code column to entities table + await queryInterface.addColumn( + 'entities', + 'organization_code', + { + type: Sequelize.STRING, + allowNull: true, // temporarily allow null for data backfill + }, + { transaction } + ) + + // 2. Add organization_code column to entity_types table + await queryInterface.addColumn( + 'entity_types', + 'organization_code', + { + type: Sequelize.STRING, + allowNull: true, // temporarily allow null for data backfill + }, + { transaction } + ) + // 3. Backfill organization_code in entity_types from organization_id + // Assuming there's an organizations table with id and code columns + await queryInterface.sequelize.query( + ` + UPDATE entity_types et + SET organization_code = o.code + FROM organizations o + WHERE et.organization_id = o.id + AND et.tenant_code = o.tenant_code + `, + { transaction } + ) + + // 4. Backfill organization_code in entities from entity_types + await queryInterface.sequelize.query( + ` + UPDATE entities e + SET organization_code = et.organization_code + FROM entity_types et + WHERE e.entity_type_id = et.id + AND e.tenant_code = et.tenant_code + `, + { transaction } + ) + // 5. Verify backfill - check if any records still have null organization_code + const [entitiesNullCount] = await queryInterface.sequelize.query( + `SELECT COUNT(*) as count FROM entities WHERE organization_code IS NULL`, + { transaction } + ) + + const [entityTypesNullCount] = await queryInterface.sequelize.query( + `SELECT COUNT(*) as count FROM entity_types WHERE organization_code IS NULL`, + { transaction } + ) + + if (entitiesNullCount[0].count > 0) { + throw new Error( + `Backfill failed: ${entitiesNullCount[0].count} entities still have null organization_code` + ) + } + + if (entityTypesNullCount[0].count > 0) { + throw new Error( + `Backfill failed: ${entityTypesNullCount[0].count} entity_types still have null organization_code` + ) + } + + console.log('✅ Backfill verification passed - all records have organization_code') + + // 6. Set organization_code to NOT NULL in both tables + await queryInterface.changeColumn( + 'entities', + 'organization_code', + { + type: Sequelize.STRING, + allowNull: false, + }, + { transaction } + ) + + await queryInterface.changeColumn( + 'entity_types', + 'organization_code', + { + type: Sequelize.STRING, + allowNull: false, + }, + { transaction } + ) + + // 7. Remove organization_id from entity_types table + //await queryInterface.removeColumn('entity_types', 'organization_id', { transaction }) + + // 8. Remove old unique constraint on entities + await queryInterface.removeIndex('entities', 'unique_entities_value', { + transaction, + }) + + // 9. Add new unique constraint on entities: value + entity_type_id + organization_code +tenant_code + await queryInterface.sequelize.query( + ` + CREATE UNIQUE INDEX unique_entities_value_type_organization_tenant + ON "entities" ("value", "entity_type_id", "organization_code", "tenant_code") + WHERE deleted_at IS NULL; + `, + { transaction } + ) + + await transaction.commit() + console.log('✅ Migration completed successfully') + } catch (err) { + await transaction.rollback() + console.error('❌ Migration failed:', err) + throw err + } + }, + + down: async (queryInterface, Sequelize) => { + const transaction = await queryInterface.sequelize.transaction() + try { + // 2. Remove new unique constraint on entities + await queryInterface.removeConstraint('entities', 'unique_entities_value_type_organization_tenant', { + transaction, + }) + + // 3. Restore old unique constraint on entities (assuming it was only on value) + await queryInterface.addConstraint('entities', { + fields: ['value'], + type: 'unique', + name: 'unique_entities_value', + transaction, + }) + + // 4. Add back organization_id column to entity_types + await queryInterface.addColumn( + 'entity_types', + 'organization_id', + { + type: Sequelize.INTEGER, + allowNull: true, // temporarily allow null for data restoration + }, + { transaction } + ) + + // 5. Restore organization_id in entity_types from organization_code + // Assuming there's an organizations table with id and code columns + await queryInterface.sequelize.query( + ` + UPDATE entity_types et + SET organization_id = o.id + FROM organizations o + WHERE et.organization_code = o.code + AND et.tenant_code = o.tenant_code + `, + { transaction } + ) + + // 6. Set organization_id to NOT NULL in entity_types + await queryInterface.changeColumn( + 'entity_types', + 'organization_id', + { + type: Sequelize.INTEGER, + allowNull: false, + }, + { transaction } + ) + + // 7. Remove organization_code columns + await queryInterface.removeColumn('entities', 'organization_code', { transaction }) + await queryInterface.removeColumn('entity_types', 'organization_code', { transaction }) + + await transaction.commit() + console.log('✅ Migration rollback completed successfully') + } catch (err) { + await transaction.rollback() + console.error('❌ Migration rollback failed:', err.message) + throw err + } + }, +} diff --git a/src/database/migrations/20250723183753-add-entity-types-constraints.js b/src/database/migrations/20250723183753-add-entity-types-constraints.js new file mode 100644 index 000000000..1ac6179e8 --- /dev/null +++ b/src/database/migrations/20250723183753-add-entity-types-constraints.js @@ -0,0 +1,75 @@ +'use strict' + +module.exports = { + up: async (queryInterface, Sequelize) => { + const transaction = await queryInterface.sequelize.transaction() + try { + // Drop existing UNIQUE constraint + await queryInterface.sequelize.query( + ` + ALTER TABLE entities + DROP CONSTRAINT IF EXISTS unique_entities_value_type_tenant; + `, + { transaction } + ) + + // Add partial unique index + await queryInterface.sequelize.query( + ` + CREATE UNIQUE INDEX unique_entities_value_type_tenant + ON entities (value, entity_type_id, tenant_code) + WHERE deleted_at IS NULL; + `, + { transaction } + ) + + // Add unique constraint on entity_types + await queryInterface.addConstraint('entity_types', { + fields: ['id', 'organization_code', 'tenant_code'], + type: 'unique', + name: 'unique_entity_types_id_org_tenant', + transaction, + }) + + // Add foreign key constraint from entities + await queryInterface.addConstraint('entities', { + fields: ['entity_type_id', 'organization_code', 'tenant_code'], + type: 'foreign key', + name: 'fk_entities_to_entity_types_composite', + references: { + table: 'entity_types', + fields: ['id', 'organization_code', 'tenant_code'], + }, + onDelete: 'CASCADE', + onUpdate: 'NO ACTION', + transaction, + }) + + await transaction.commit() + } catch (error) { + await transaction.rollback() + throw error + } + }, + + down: async (queryInterface, Sequelize) => { + const transaction = await queryInterface.sequelize.transaction() + try { + // Remove foreign key constraint + await queryInterface.removeConstraint('entities', 'fk_entities_to_entity_types_composite', { transaction }) + + // Remove unique constraint from entity_types + await queryInterface.removeConstraint('entity_types', 'unique_entity_types_id_org_tenant', { transaction }) + + // Remove partial unique index from entities + await queryInterface.sequelize.query(`DROP INDEX IF EXISTS unique_entities_value_type_tenant;`, { + transaction, + }) + + await transaction.commit() + } catch (error) { + await transaction.rollback() + throw error + } + }, +} diff --git a/src/database/models/entities.js b/src/database/models/entities.js index c0991dfef..8c13b4731 100644 --- a/src/database/models/entities.js +++ b/src/database/models/entities.js @@ -18,6 +18,10 @@ module.exports = (sequelize, DataTypes) => { type: DataTypes.STRING, allowNull: false, }, + organization_code: { + type: DataTypes.STRING, + allowNull: false, + }, created_by: { type: DataTypes.INTEGER, allowNull: true }, updated_by: { type: DataTypes.INTEGER, allowNull: true }, }, diff --git a/src/database/models/entityType.js b/src/database/models/entityType.js index bc527ea93..427825d7d 100644 --- a/src/database/models/entityType.js +++ b/src/database/models/entityType.js @@ -16,7 +16,11 @@ module.exports = (sequelize, DataTypes) => { updated_by: { type: DataTypes.INTEGER, allowNull: true }, allow_filtering: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false }, data_type: { type: DataTypes.STRING, allowNull: false, defaultValue: 'STRING' }, - organization_id: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0, primaryKey: true }, + organization_id: { type: DataTypes.INTEGER, allowNull: false }, + organization_code: { + type: DataTypes.STRING, + allowNull: false, + }, parent_id: { type: DataTypes.INTEGER, allowNull: true }, allow_custom_entities: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: true }, has_entities: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: true }, @@ -31,7 +35,7 @@ module.exports = (sequelize, DataTypes) => { tenant_code: { type: DataTypes.STRING, allowNull: false, - defaultValue: '', + primaryKey: true, }, }, { sequelize, modelName: 'EntityType', tableName: 'entity_types', freezeTableName: true, paranoid: true } diff --git a/src/database/queries/entities.js b/src/database/queries/entities.js index fc8831dc1..59371a91a 100644 --- a/src/database/queries/entities.js +++ b/src/database/queries/entities.js @@ -1,5 +1,6 @@ const Entity = require('../models/index').Entity const { Op } = require('sequelize') +const EntityType = require('../models/index').EntityType module.exports = class UserEntityData { static async createEntity(data) { @@ -22,12 +23,13 @@ module.exports = class UserEntityData { } } - static async updateOneEntity(id, update, userId, options = {}) { + static async updateOneEntity(id, update, organizationCode, tenantCode, options = {}) { try { return await Entity.update(update, { where: { id: id, - created_by: userId, + tenant_code: tenantCode, + organization_code: organizationCode, }, ...options, }) @@ -36,12 +38,13 @@ module.exports = class UserEntityData { } } - static async deleteOneEntityType(id, userId) { + static async deleteOneEntity(id, organizationCode, tenantCode) { try { return await Entity.destroy({ where: { id: id, - created_by: userId, + organization_code: organizationCode, + tenant_code: tenantCode, }, }) } catch (error) { @@ -87,4 +90,24 @@ module.exports = class UserEntityData { throw error } } + + static async findEntityWithOrgCheck(id, tenantCode, organizationID) { + return Entity.findOne({ + where: { + id, + tenant_code: tenantCode, + }, + include: [ + { + model: EntityType, + as: 'entity_type', + where: { + tenant_code: tenantCode, + organization_id: organizationID, + }, + required: true, + }, + ], + }) + } } diff --git a/src/database/queries/entityType.js b/src/database/queries/entityType.js index 5c792d1da..2476588e6 100644 --- a/src/database/queries/entityType.js +++ b/src/database/queries/entityType.js @@ -1,8 +1,5 @@ const EntityType = require('../models/index').EntityType const Entity = require('../models/index').Entity -const { Op } = require('sequelize') -const Sequelize = require('../models/index').sequelize - module.exports = class UserEntityData { static async createEntityType(data) { try { @@ -24,11 +21,11 @@ module.exports = class UserEntityData { } } - static async findAllEntityTypes(orgId, attributes, filter = {}) { + static async findAllEntityTypes(organizationCode, attributes, filter = {}) { try { const entityData = await EntityType.findAll({ where: { - organization_id: orgId, + organization_code: organizationCode, ...filter, }, attributes, @@ -94,12 +91,12 @@ module.exports = class UserEntityData { } } */ - static async updateOneEntityType(id, organizationId, tenantCode, update, options = {}) { + static async updateOneEntityType(id, organizationCode, tenantCode, update, options = {}) { try { return await EntityType.update(update, { where: { id: id, - organization_id: organizationId, + organization_code: organizationCode, tenant_code: tenantCode, }, ...options, @@ -109,12 +106,12 @@ module.exports = class UserEntityData { } } - static async deleteOneEntityType(id, orgId) { + static async deleteOneEntityType(id, organizationCode) { try { return await EntityType.destroy({ where: { id: id, - organization_id: orgId, + organization_code: organizationCode, }, individualHooks: true, }) diff --git a/src/services/account.js b/src/services/account.js index 0f29cd4b9..20b8bd981 100644 --- a/src/services/account.js +++ b/src/services/account.js @@ -346,29 +346,17 @@ module.exports = class AccountHelper { delete bodyData.role - const [defaultOrg, userOrgDetails, modelName] = await Promise.all([ - organizationQueries.findOne( - { code: process.env.DEFAULT_ORGANISATION_CODE, tenant_code: tenantDetail.code }, - { attributes: ['id'] } - ), - organizationQueries.findOne( - { code: organizationCode, tenant_code: tenantDetail.code }, - { attributes: ['id'] } - ), - userQueries.getModelName(), - ]) + const modelName = await userQueries.getModelName() + + const defaultOrganizationCode = process.env.DEFAULT_ORGANISATION_CODE - if (!defaultOrg || !userOrgDetails) { + if (!defaultOrganizationCode || !organizationCode) { throw new Error('Default or user organization not found.') } - - const defaultOrgId = defaultOrg.id - userOrgId = userOrgDetails.id - const [validationData, userModel] = await Promise.all([ entityTypeQueries.findUserEntityTypesAndEntities({ status: 'ACTIVE', - organization_id: { [Op.in]: [userOrgId, defaultOrgId] }, + organization_code: { [Op.in]: [defaultOrganizationCode, organizationCode] }, model_names: { [Op.contains]: [modelName] }, tenant_code: tenantDetail.code, }), @@ -735,7 +723,7 @@ module.exports = class AccountHelper { } // Verify password - const isPasswordCorrect = bcryptJs.compareSync(bodyData.password, user.password) + const isPasswordCorrect = bcryptJs.compare(bodyData.password, user.password) if (!isPasswordCorrect) { return responses.failureResponse({ message: 'IDENTIFIER_OR_PASSWORD_INVALID', @@ -789,24 +777,22 @@ module.exports = class AccountHelper { delete user.password - // Fetch default organization and validation data - let defaultOrg = await organizationQueries.findOne( - { code: process.env.DEFAULT_ORGANISATION_CODE, tenant_code: tenantDetail.code }, - { attributes: ['id'] } - ) - let defaultOrgId = defaultOrg.id const modelName = await userQueries.getModelName() + const orgCodes = user.organizations?.map((org) => org.code).filter(Boolean) || [] + orgCodes.push(process.env.DEFAULT_ORGANISATION_CODE) + let validationData = await entityTypeQueries.findUserEntityTypesAndEntities({ status: 'ACTIVE', - organization_id: { - [Op.in]: [user.organization_id, defaultOrgId], + organization_code: { + [Op.in]: orgCodes, }, tenant_code: tenantDetail.code, model_names: { [Op.contains]: [modelName] }, }) - const prunedEntities = removeDefaultOrgEntityTypes(validationData, user.organization_id) + const prunedEntities = removeDefaultOrgEntityTypes(validationData, user.organizations?.[0]?.id) + user = await utils.processDbResponse(user, prunedEntities) if (user && user.image) { @@ -1481,23 +1467,20 @@ module.exports = class AccountHelper { delete user.otpInfo // Fetch default organization and validation data - let defaultOrg = await organizationQueries.findOne( - { code: process.env.DEFAULT_ORGANISATION_CODE, tenant_code: tenantDetail.code }, - { attributes: ['id'] } - ) - let defaultOrgId = defaultOrg.id + const userOrgCodes = user.organizations?.map((org) => org.code) || [] + const defaultOrganizationCode = process.env.DEFAULT_ORGANISATION_CODE const modelName = await userQueries.getModelName() let validationData = await entityTypeQueries.findUserEntityTypesAndEntities({ status: 'ACTIVE', - organization_id: { - [Op.in]: [user.organization_id, defaultOrgId], + organization_code: { + [Op.in]: [...userOrgCodes, defaultOrganizationCode], }, model_names: { [Op.contains]: [modelName] }, tenant_code: tenantDetail.code, }) - const prunedEntities = removeDefaultOrgEntityTypes(validationData, user.organization_id) + const prunedEntities = removeDefaultOrgEntityTypes(validationData, user.organizations[0].id) user = await utils.processDbResponse(user, prunedEntities) // Handle user image diff --git a/src/services/entities.js b/src/services/entities.js index 77eaf7598..7e684f943 100644 --- a/src/services/entities.js +++ b/src/services/entities.js @@ -1,10 +1,8 @@ // Dependencies const httpStatusCode = require('@generics/http-status') -const common = require('@constants/common') - -const entityTypeQueries = require('@database/queries/entities') +const entityQueries = require('@database/queries/entities') +const entityTypeQueries = require('@database/queries/entityType') const { UniqueConstraintError, ForeignKeyConstraintError } = require('sequelize') -const { Op } = require('sequelize') const responses = require('@helpers/responses') module.exports = class EntityHelper { @@ -17,12 +15,15 @@ module.exports = class EntityHelper { * @returns {JSON} - Entity created response. */ - static async create(bodyData, id, tenantCode) { - bodyData.created_by = id - bodyData.updated_by = id + static async create(bodyData, userId, tenantCode, organizationCode) { + bodyData.created_by = userId + bodyData.updated_by = userId bodyData.tenant_code = tenantCode + bodyData.organization_code = organizationCode + try { - const entity = await entityTypeQueries.createEntity(bodyData) + const entity = await entityQueries.createEntity(bodyData) + return responses.successResponse({ statusCode: httpStatusCode.created, message: 'ENTITY_CREATED_SUCCESSFULLY', @@ -36,6 +37,7 @@ module.exports = class EntityHelper { responseCode: 'CLIENT_ERROR', }) } + if (error instanceof ForeignKeyConstraintError) { return responses.failureResponse({ message: 'ENTITY_TYPE_NOT_FOUND', @@ -43,6 +45,7 @@ module.exports = class EntityHelper { responseCode: 'CLIENT_ERROR', }) } + throw error } } @@ -57,13 +60,19 @@ module.exports = class EntityHelper { * @returns {JSON} - Entity updated response. */ - static async update(bodyData, id, loggedInUserId) { + static async update(bodyData, id, loggedInUserId, organizationCode, tenantCode) { bodyData.updated_by = loggedInUserId try { - const [updateCount, updatedEntity] = await entityTypeQueries.updateOneEntity(id, bodyData, loggedInUserId, { - returning: true, - raw: true, - }) + const [updateCount, updatedEntity] = await entityQueries.updateOneEntity( + id, + bodyData, + organizationCode, + tenantCode, + { + returning: true, + raw: true, + } + ) if (updateCount === 0) { return responses.failureResponse({ @@ -97,71 +106,24 @@ module.exports = class EntityHelper { * @returns {JSON} - Entity read response. */ - static async read(query, userId) { + static async read(query, tenantCode) { try { let filter if (query.id) { filter = { - [Op.or]: [ - { - id: query.id, - created_by: '0', - status: 'ACTIVE', - }, - { id: query.id, created_by: userId, status: 'ACTIVE' }, - ], - } - } else { - filter = { - [Op.or]: [ - { - value: query.value, - created_by: '0', - status: 'ACTIVE', - }, - { value: query.value, created_by: userId, status: 'ACTIVE' }, - ], - } - } - const entities = await entityTypeQueries.findAllEntities(filter) - - if (!entities.length) { - return responses.failureResponse({ - message: 'ENTITY_NOT_FOUND', - statusCode: httpStatusCode.bad_request, - responseCode: 'CLIENT_ERROR', - }) - } - return responses.successResponse({ - statusCode: httpStatusCode.ok, - message: 'ENTITY_FETCHED_SUCCESSFULLY', - result: entities, - }) - } catch (error) { - throw error - } - } - - static async readAll(query, userId) { - try { - let filter - if (query.read_user_entity == true) { - filter = { - [Op.or]: [ - { - created_by: '0', - }, - { - created_by: userId, - }, - ], + id: query.id, + status: 'ACTIVE', + tenant_code: tenantCode, } } else { filter = { - created_by: '0', + value: query.value, + entity_type_id: query.entity_type_id, + tenant_code: tenantCode, + status: 'ACTIVE', } } - const entities = await entityTypeQueries.findAllEntities(filter) + const entities = await entityQueries.findAllEntities(filter) if (!entities.length) { return responses.failureResponse({ @@ -188,9 +150,9 @@ module.exports = class EntityHelper { * @returns {JSON} - Entity deleted response. */ - static async delete(id, userId) { + static async delete(id, organizationCode, tenantCode) { try { - const deleteCount = await entityTypeQueries.deleteOneEntityType(id, userId) + const deleteCount = await entityQueries.deleteOneEntity(id, organizationCode, tenantCode) if (deleteCount === '0') { return responses.failureResponse({ message: 'ENTITY_NOT_FOUND', @@ -228,7 +190,7 @@ module.exports = class EntityHelper { } const attributes = ['id', 'entity_type_id', 'value', 'label', 'status', 'type', 'created_by', 'created_at'] - const entities = await entityTypeQueries.getAllEntities(filter, attributes, pageNo, pageSize, searchText) + const entities = await entityQueries.getAllEntities(filter, attributes, pageNo, pageSize, searchText) if (entities.rows == 0 || entities.count == 0) { return responses.failureResponse({ diff --git a/src/services/entityType.js b/src/services/entityType.js index 566ed0a26..5aa88079d 100644 --- a/src/services/entityType.js +++ b/src/services/entityType.js @@ -17,10 +17,11 @@ module.exports = class EntityHelper { * @returns {JSON} - Created entity type response. */ - static async create(bodyData, id, orgId, tenantCode) { + static async create(bodyData, id, organizationCode, organizationId, tenantCode) { bodyData.created_by = id bodyData.updated_by = id - bodyData.organization_id = orgId + bodyData.organization_code = organizationCode + bodyData.organization_id = organizationId bodyData.tenant_code = tenantCode try { const entityType = await entityTypeQueries.createEntityType(bodyData) @@ -51,12 +52,12 @@ module.exports = class EntityHelper { * @returns {JSON} - Updated Entity Type. */ - static async update(bodyData, id, loggedInUserId, orgId, tenantCode) { - ;(bodyData.updated_by = loggedInUserId), (bodyData.organization_id = orgId) + static async update(bodyData, id, loggedInUserId, organizationCode, tenantCode) { + ;(bodyData.updated_by = loggedInUserId), (bodyData.organization_code = organizationCode) try { const [updateCount, updatedEntityType] = await entityTypeQueries.updateOneEntityType( id, - orgId, + organizationCode, tenantCode, bodyData, { @@ -90,18 +91,18 @@ module.exports = class EntityHelper { } } - static async readAllSystemEntityTypes(orgId, tenantCode) { + static async readAllSystemEntityTypes(organizationId, organizationCode, tenantCode) { try { - const defaultOrg = await organizationQueries.findOne( - { code: process.env.DEFAULT_ORGANISATION_CODE, tenant_code: tenantCode }, - { attributes: ['id'] } - ) - const defaultOrgId = defaultOrg.id - - const attributes = ['value', 'label', 'id', 'organization_id'] + const attributes = ['value', 'label', 'id', 'organization_code'] - const entities = await entityTypeQueries.findAllEntityTypes([orgId, defaultOrgId], attributes) - const prunedEntities = removeDefaultOrgEntityTypes(entities, orgId) + const entities = await entityTypeQueries.findAllEntityTypes( + [organizationCode, process.env.DEFAULT_ORGANISATION_CODE], + attributes, + { + tenant_code: tenantCode, + } + ) + const prunedEntities = removeDefaultOrgEntityTypes(entities, organizationId) if (!prunedEntities.length) { return responses.failureResponse({ @@ -120,32 +121,22 @@ module.exports = class EntityHelper { } } - static async readUserEntityTypes(body, userId, orgId, tenantCode) { + static async readUserEntityTypes(body, organizationCode, tenantCode, organizationId = '') { try { - // Fetch default organization ID (consider caching this) - const defaultOrg = await organizationQueries.findOne( - { code: process.env.DEFAULT_ORGANISATION_CODE, tenant_code: tenantCode }, - { attributes: ['id'] } - ) - if (!defaultOrg) { - throw new Error('Default organization not found') - } - const defaultOrgId = defaultOrg.id - // Include tenant_code in filter for consistency with schema const filter = { value: body.value, status: 'ACTIVE', tenant_code: tenantCode, // Ensure tenant isolation - organization_id: { - [Op.in]: [orgId, defaultOrgId], + organization_code: { + [Op.in]: [process.env.DEFAULT_ORGANISATION_CODE, organizationCode], }, } const entities = await entityTypeQueries.findUserEntityTypesAndEntities(filter) // Deduplicate entity types by value, prioritizing orgId - const prunedEntities = removeDefaultOrgEntityTypes(entities, orgId) + const prunedEntities = removeDefaultOrgEntityTypes(entities, organizationId) return responses.successResponse({ statusCode: httpStatusCode.ok, @@ -165,9 +156,9 @@ module.exports = class EntityHelper { * @returns {JSON} - Entity deleted response. */ - static async delete(id, orgId) { + static async delete(id, organizationCode) { try { - const deleteCount = await entityTypeQueries.deleteOneEntityType(id, orgId) + const deleteCount = await entityTypeQueries.deleteOneEntityType(id, organizationCode) if (deleteCount === 0) { return responses.failureResponse({ message: 'ENTITY_TYPE_NOT_FOUND', diff --git a/src/services/tenant.js b/src/services/tenant.js index 88e7e80ac..bda2e3d9a 100644 --- a/src/services/tenant.js +++ b/src/services/tenant.js @@ -142,24 +142,23 @@ module.exports = class tenantHelper { } const defaultOrgId = defaultOrgCreateResponse.result.id + const defaultOrganizationCode = defaultOrgCreateResponse.result.code // add rollback code for org rollbackStack.push(async () => { await organisationQueries.hardDelete(defaultOrgId) }) // ******* adding default entities and entity types to Default Org CODE BEGINS HERE ******* - let allDefaultEntityTypeValues = await entityTypeQueries.findAllEntityTypes( - process.env.DEFAULT_ORG_ID, - ['value'] - ) + let allDefaultEntityTypeValues = await entityTypeQueries.findAllEntityTypes(defaultOrganizationCode, [ + 'value', + ]) allDefaultEntityTypeValues = allDefaultEntityTypeValues.map((entityType) => entityType.value) if (allDefaultEntityTypeValues.length > 0) { const entityTypeResponse = await entityTypeService.readUserEntityTypes( { value: allDefaultEntityTypeValues, }, - userId, - process.env.DEFAULT_ORG_ID, + defaultOrganizationCode, process.env.DEFAULT_TENANT_CODE ) @@ -187,6 +186,7 @@ module.exports = class tenantHelper { external_entity_type: entityType?.external_entity_type || false, }, userId, + defaultOrganizationCode, defaultOrgId, tenantCreateResponse.code ) @@ -218,7 +218,12 @@ module.exports = class tenantHelper { // Step 2: Create promises for each entity const entitiesCreationPromise = filteredEntities.map((entity) => { - return EntityHelper.create(entity, userId, tenantCreateResponse.code) + return EntityHelper.create( + entity, + userId, + tenantCreateResponse.code, + defaultOrganizationCode + ) }) const createdEntities = await Promise.all(entitiesCreationPromise) diff --git a/src/services/user.js b/src/services/user.js index aabb87ee3..e942cc9f9 100644 --- a/src/services/user.js +++ b/src/services/user.js @@ -35,7 +35,7 @@ module.exports = class UserHelper { * @param {string} searchText - search text. * @returns {JSON} - update user response */ - static async update(bodyData, id, orgId, tenantCode) { + static async update(bodyData, id, orgCode, tenantCode) { try { if (bodyData.hasOwnProperty('email')) { return responses.failureResponse({ @@ -58,16 +58,12 @@ module.exports = class UserHelper { }) } - let defaultOrg = await organizationQueries.findOne( - { code: process.env.DEFAULT_ORGANISATION_CODE, tenant_code: tenantCode }, - { attributes: ['id'] } - ) - let defaultOrgId = defaultOrg.id + let defaultOrganizationCode = process.env.DEFAULT_ORGANISATION_CODE const filter = { status: 'ACTIVE', - organization_id: { - [Op.in]: [orgId, defaultOrgId], + organization_code: { + [Op.in]: [orgCode, defaultOrganizationCode], }, tenant_code: tenantCode, model_names: { [Op.contains]: [await userQueries.getModelName()] }, @@ -348,22 +344,19 @@ module.exports = class UserHelper { //user.user_roles = roles - let defaultOrg = await organizationQueries.findOne( - { code: process.env.DEFAULT_ORGANISATION_CODE, tenant_code: tenantCode }, - { attributes: ['id'] } - ) - let defaultOrgId = defaultOrg.id - let userOrg = user.organizations[0].id + let defaultOrganizationCode = process.env.DEFAULT_ORGANISATION_CODE + + let userOrg = user.organizations[0].code let validationData = await entityTypeQueries.findUserEntityTypesAndEntities({ status: 'ACTIVE', - organization_id: { - [Op.in]: [userOrg, defaultOrgId], + organization_code: { + [Op.in]: [userOrg, defaultOrganizationCode], }, tenant_code: tenantCode, model_names: { [Op.contains]: [await userQueries.getModelName()] }, }) - const prunedEntities = removeDefaultOrgEntityTypes(validationData, user.organization_id) + const prunedEntities = removeDefaultOrgEntityTypes(validationData, user.organizations[0].id) const permissionsByModule = await this.getPermissions(user.organizations[0].roles) user.permissions = permissionsByModule @@ -456,22 +449,19 @@ module.exports = class UserHelper { if (user.image) { user.image = await utils.getDownloadableUrl(user.image) } - let defaultOrg = await organizationQueries.findOne( - { code: process.env.DEFAULT_ORGANISATION_CODE, tenant_code: tenantCode }, - { attributes: ['id'] } - ) - let defaultOrgId = defaultOrg.id - let userOrg = user.organizations[0].id + let defaultOrganizationCode = process.env.DEFAULT_ORGANISATION_CODE + + let userOrganizationCode = user.organizations[0].code let validationData = await entityTypeQueries.findUserEntityTypesAndEntities({ status: 'ACTIVE', - organization_id: { - [Op.in]: [userOrg, defaultOrgId], + organization_code: { + [Op.in]: [userOrganizationCode, defaultOrganizationCode], }, tenant_code: tenantCode, model_names: { [Op.contains]: [await userQueries.getModelName()] }, }) - const prunedEntities = removeDefaultOrgEntityTypes(validationData, user.organization_id) + const prunedEntities = removeDefaultOrgEntityTypes(validationData, user.organizations[0].id) const processDbResponse = await utils.processDbResponse(user, prunedEntities) return responses.successResponse({ @@ -527,7 +517,7 @@ module.exports = class UserHelper { * @param {Object} bodyData - it contains user preferred language * @returns {JSON} - updated user preferred languages response */ - static async setLanguagePreference(bodyData, id, orgId, tenantCode) { + static async setLanguagePreference(bodyData, id, organizationCode, tenantCode) { try { let skipRequiredValidation = true const user = await userQueries.findOne({ id: id, tenant_code: tenantCode }) @@ -538,15 +528,12 @@ module.exports = class UserHelper { responseCode: 'UNAUTHORIZED', }) } - let defaultOrg = await organizationQueries.findOne( - { code: process.env.DEFAULT_ORGANISATION_CODE, tenant_code: tenantCode }, - { attributes: ['id'] } - ) - let defaultOrgId = defaultOrg.id + let defaultOrganizationCode = process.env.DEFAULT_ORGANISATION_CODE + let userModel = await userQueries.getColumns() const filter = { status: common.ACTIVE_STATUS, - organization_id: { [Op.in]: [orgId, defaultOrgId] }, + organization_code: { [Op.in]: [organizationCode, defaultOrganizationCode] }, model_names: { [Op.contains]: [userModel] }, value: 'preferred_language', tenant_code: tenantCode, diff --git a/src/validators/v1/entity-type.js b/src/validators/v1/entity-type.js index 46a796d8b..35f4b9c82 100644 --- a/src/validators/v1/entity-type.js +++ b/src/validators/v1/entity-type.js @@ -72,8 +72,7 @@ module.exports = { .withMessage('model_names must be an array with at least one element') req.checkBody('model_names.*') - .isIn(['Session', 'MentorExtension', 'UserExtension']) - .withMessage('model_names must be in Session,MentorExtension,UserExtension') + isIn(['User']).withMessage('model_names must be in User') }, read: (req) => { diff --git a/src/validators/v1/entity.js b/src/validators/v1/entity.js index 3a255af37..9872b88ad 100644 --- a/src/validators/v1/entity.js +++ b/src/validators/v1/entity.js @@ -74,22 +74,41 @@ module.exports = { read: (req) => { req.checkQuery('id') - .trim() .optional() + .trim() .notEmpty() .withMessage('id param is empty') .isNumeric() .withMessage('id param is invalid, must be an integer') req.checkQuery('value') - .trim() .optional() + .trim() .notEmpty() .withMessage('value field is empty') .matches(/^[A-Za-z0-9 ]+$/) .withMessage('value is invalid, must not contain spaces') - }, + req.checkQuery('entity_type_id') + .optional() + .trim() + .notEmpty() + .withMessage('entity_type_id is empty') + .isNumeric() + .withMessage('entity_type_id must be numeric') + + // Custom validator to ensure either `id` OR (`value` + `entity_type_id`) is present + req.checkQuery('').custom(() => { + const id = req.query.id + const value = req.query.value + const entityTypeId = req.query.entity_type_id + + if (id) return true + if (value && entityTypeId) return true + + throw new Error('Either id OR (value and entity_type_id) must be provided') + }) + }, delete: (req) => { req.checkParams('id').notEmpty().withMessage('id param is empty') }, From 1c9708ecb694fcf44ecf56688627f15ed4af20ac Mon Sep 17 00:00:00 2001 From: adithya_dinesh Date: Wed, 30 Jul 2025 14:31:23 +0530 Subject: [PATCH 11/55] coderabit review comments addressed --- .../migrations/20250729064710-org-code-fix.js | 14 -------------- src/validators/v1/organization.js | 2 +- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/src/database/migrations/20250729064710-org-code-fix.js b/src/database/migrations/20250729064710-org-code-fix.js index 29ee00427..8b155916e 100644 --- a/src/database/migrations/20250729064710-org-code-fix.js +++ b/src/database/migrations/20250729064710-org-code-fix.js @@ -50,18 +50,6 @@ module.exports = { }) fk_retainer.push(enableFK(table, fk_name, fkey, refTable, refKey)) - table = 'organization_user_invites' - fk_name = 'fk_org_user_invites_org_code' - fkey = '(organization_code, tenant_code)' - refTable = 'organizations' - refKey = '(code, tenant_code)' - await queryInterface.sequelize.query(disableFK(table, fk_name), { - type: Sequelize.QueryTypes.RAW, - raw: true, - transaction, - }) - fk_retainer.push(enableFK(table, fk_name, fkey, refTable, refKey)) - table = 'organization_user_invites' fk_name = 'fk_org_user_invites_organization_id' fkey = '(organization_code, tenant_code)' @@ -151,7 +139,6 @@ module.exports = { raw: true, transaction, }) - console.log(fetchOrgs) // Re-enable foreign key constraints let fk_retainerPromise = [] @@ -169,7 +156,6 @@ module.exports = { // Commit the transaction await transaction.commit() - console.log('Transaction committed successfully.') } } catch (error) { // Rollback transaction on error diff --git a/src/validators/v1/organization.js b/src/validators/v1/organization.js index fd80a1708..f81a6d428 100644 --- a/src/validators/v1/organization.js +++ b/src/validators/v1/organization.js @@ -16,7 +16,7 @@ module.exports = { .trim() .notEmpty() .withMessage('code field is empty') - .matches(/^[a-z0-9]+$/) + .matches(/^[a-z0-9_]+$/) .withMessage('code is invalid. Only lowercase alphanumeric characters allowed') req.checkBody('tenant_code').trim().notEmpty().withMessage('tenant_code field is empty') req.checkBody('registration_codes') From 7da1e65f1117e3b338ac667c7420d3a55deedcea Mon Sep 17 00:00:00 2001 From: Nevil Date: Wed, 30 Jul 2025 14:55:44 +0530 Subject: [PATCH 12/55] add: default config --- .coderabbit.yaml | 175 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 .coderabbit.yaml diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 000000000..1d6443f45 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,175 @@ +language: en-US +tone_instructions: '' +early_access: false +enable_free_tier: true +reviews: + profile: chill + request_changes_workflow: false + high_level_summary: true + high_level_summary_placeholder: '@coderabbitai summary' + high_level_summary_in_walkthrough: false + auto_title_placeholder: '@coderabbitai' + auto_title_instructions: '' + review_status: true + commit_status: true + fail_commit_status: false + collapse_walkthrough: false + changed_files_summary: true + sequence_diagrams: true + estimate_code_review_effort: true + assess_linked_issues: true + related_issues: true + related_prs: true + suggested_labels: true + auto_apply_labels: false + suggested_reviewers: true + auto_assign_reviewers: false + poem: true + labeling_instructions: [] + path_filters: [] + path_instructions: [] + abort_on_close: true + disable_cache: false + auto_review: + enabled: true + auto_incremental_review: true + ignore_title_keywords: [] + labels: [] + drafts: false + base_branches: [] + finishing_touches: + docstrings: + enabled: true + unit_tests: + enabled: true + pre_merge_checks: + docstrings: + mode: warning + threshold: 80 + title: + mode: warning + requirements: '' + description: + mode: warning + issue_assessment: + mode: warning + tools: + ast-grep: + rule_dirs: [] + util_dirs: [] + essential_rules: true + packages: [] + shellcheck: + enabled: true + ruff: + enabled: true + markdownlint: + enabled: true + github-checks: + enabled: true + timeout_ms: 90000 + languagetool: + enabled: true + enabled_rules: [] + disabled_rules: [] + enabled_categories: [] + disabled_categories: [] + enabled_only: false + level: default + biome: + enabled: true + hadolint: + enabled: true + swiftlint: + enabled: true + phpstan: + enabled: true + level: default + phpmd: + enabled: true + phpcs: + enabled: true + golangci-lint: + enabled: true + yamllint: + enabled: true + gitleaks: + enabled: true + checkov: + enabled: true + detekt: + enabled: true + eslint: + enabled: true + flake8: + enabled: true + rubocop: + enabled: true + buf: + enabled: true + regal: + enabled: true + actionlint: + enabled: true + pmd: + enabled: true + cppcheck: + enabled: true + semgrep: + enabled: true + circleci: + enabled: true + clippy: + enabled: true + sqlfluff: + enabled: true + prismaLint: + enabled: true + pylint: + enabled: true + oxc: + enabled: true + shopifyThemeCheck: + enabled: true + luacheck: + enabled: true + brakeman: + enabled: true + dotenvLint: + enabled: true + htmlhint: + enabled: true + checkmake: + enabled: true +chat: + auto_reply: true + integrations: + jira: + usage: auto + linear: + usage: auto +knowledge_base: + opt_out: false + web_search: + enabled: true + code_guidelines: + enabled: true + filePatterns: [] + learnings: + scope: auto + issues: + scope: auto + jira: + usage: auto + project_keys: [] + linear: + usage: auto + team_keys: [] + pull_requests: + scope: auto +code_generation: + docstrings: + language: en-US + path_instructions: [] + unit_tests: + path_instructions: [] From 33fe6c8b569893095104ac0a12d20ea3ad1d629e Mon Sep 17 00:00:00 2001 From: Nevil Date: Wed, 30 Jul 2025 14:58:42 +0530 Subject: [PATCH 13/55] update: coderabbit config --- .coderabbit.yaml | 37 +++++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 1d6443f45..7a7a654aa 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -1,3 +1,5 @@ +# yaml-language-server: $schema=https://storage.googleapis.com/coderabbit_public_assets/schema.v2.json + language: en-US tone_instructions: '' early_access: false @@ -26,17 +28,44 @@ reviews: auto_assign_reviewers: false poem: true labeling_instructions: [] - path_filters: [] - path_instructions: [] + + #Exclude non-source code directories from reviews. + path_filters: + - "!deployment/**" + - "!dev-ops/**" + - "!release-notes/**" + - "!src/api-doc/**" + - "!src/assets/**" + - "!src/configs/**" + - "!src/public/**" + - "!src/temp/**" + - "!**/*.md" + + # Add specific review instructions for critical paths. + path_instructions: + - path: "src/services/**" + instructions: "This is core business logic. Please check for correctness, efficiency, and potential edge cases." + - path: "src/controllers/**" + instructions: "These are API controllers. Focus on request/response handling, security (auth middleware usage), and proper status codes." + - path: "src/database/queries/**" + instructions: "Review database queries for performance. Check for N+1 problems and ensure indexes can be used." + - path: "src/validators/**" + instructions: "Validate all incoming data thoroughly. Check for missing or incomplete validation rules." + - path: "src/middlewares/**" + instructions: "This is security-sensitive middleware. Scrutinize for potential vulnerabilities." + abort_on_close: true disable_cache: false auto_review: enabled: true auto_incremental_review: true - ignore_title_keywords: [] + ignore_title_keywords: ["WIP", "skip-review"] labels: [] drafts: false - base_branches: [] + # Target reviews to development branches. + base_branches: + - "develop" + finishing_touches: docstrings: enabled: true From 16eb153e67efb8598a49144791a162c4afaf0cd5 Mon Sep 17 00:00:00 2001 From: Nevil Date: Wed, 30 Jul 2025 15:43:03 +0530 Subject: [PATCH 14/55] removed: tools --- .coderabbit.yaml | 88 ++++++++++-------------------------------------- 1 file changed, 18 insertions(+), 70 deletions(-) diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 7a7a654aa..d611ec6b2 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -83,93 +83,41 @@ reviews: issue_assessment: mode: warning tools: - ast-grep: - rule_dirs: [] - util_dirs: [] - essential_rules: true - packages: [] - shellcheck: - enabled: true - ruff: - enabled: true - markdownlint: - enabled: true - github-checks: - enabled: true - timeout_ms: 90000 - languagetool: - enabled: true - enabled_rules: [] - disabled_rules: [] - enabled_categories: [] - disabled_categories: [] - enabled_only: false - level: default - biome: - enabled: true - hadolint: - enabled: true - swiftlint: - enabled: true - phpstan: - enabled: true - level: default - phpmd: - enabled: true - phpcs: + eslint: enabled: true - golangci-lint: + semgrep: enabled: true - yamllint: + dotenvLint: enabled: true - gitleaks: + sqlfluff: enabled: true checkov: enabled: true - detekt: - enabled: true - eslint: - enabled: true - flake8: - enabled: true - rubocop: + gitleaks: enabled: true - buf: + hadolint: enabled: true - regal: + shellcheck: enabled: true actionlint: enabled: true - pmd: - enabled: true - cppcheck: - enabled: true - semgrep: - enabled: true circleci: enabled: true - clippy: - enabled: true - sqlfluff: - enabled: true - prismaLint: - enabled: true - pylint: - enabled: true - oxc: - enabled: true - shopifyThemeCheck: - enabled: true - luacheck: - enabled: true - brakeman: + github-checks: enabled: true - dotenvLint: + markdownlint: enabled: true - htmlhint: + yamllint: enabled: true - checkmake: + biome: enabled: true + languagetool: + enabled: false # optional + ast-grep: + essential_rules: true + rule_dirs: [] + util_dirs: [] + packages: [] chat: auto_reply: true integrations: From 17058065500fb85ddee8e7ec25a7103c6fc20a70 Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Thu, 31 Jul 2025 12:11:30 +0530 Subject: [PATCH 15/55] fixed: an issue with func param name --- src/utils/notification.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/utils/notification.js b/src/utils/notification.js index 4b7a44a86..1712b6334 100644 --- a/src/utils/notification.js +++ b/src/utils/notification.js @@ -7,13 +7,17 @@ const NOTIFICATION_MODE = process.env.NOTIFICATION_MODE const { sendNotification } = require('../requests/notification') const common = require('@constants/common') -async function sendEmailNotification({ emailId, templateCode, variables, tenantCode, organization_code = '' }) { +async function sendEmailNotification({ emailId, templateCode, variables, tenantCode, organizationCode = '' }) { try { if (!emailId) return - const templateData = - (await notificationTemplateQueries.findOneEmailTemplate(templateCode, organization_code, tenantCode)) || {} - if (Object.keys(templateData).length <= 0) { + const templateData = await notificationTemplateQueries.findOneEmailTemplate( + templateCode, + organizationCode, + tenantCode + ) + + if (!templateData) { console.warn(`Email template not found for code: ${templateCode}`) return } @@ -47,13 +51,13 @@ async function sendEmailNotification({ emailId, templateCode, variables, tenantC } } -async function sendSMSNotification({ phoneNumber, templateCode, variables, tenantCode, organization_code = '' }) { +async function sendSMSNotification({ phoneNumber, templateCode, variables, tenantCode, organizationCode = '' }) { try { if (!phoneNumber) return const templateData = await notificationTemplateQueries.findOneSMSTemplate( templateCode, - organization_code, + organizationCode, tenantCode ) if (!templateData) { From f2a82be0dc896432ae2a7f629e8b19659e2e19fe Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Thu, 31 Jul 2025 12:12:14 +0530 Subject: [PATCH 16/55] fixed: return in db queries --- src/database/queries/notificationTemplate.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/database/queries/notificationTemplate.js b/src/database/queries/notificationTemplate.js index d2a7258ec..7aed3e729 100644 --- a/src/database/queries/notificationTemplate.js +++ b/src/database/queries/notificationTemplate.js @@ -2,7 +2,6 @@ const NotificationTemplate = require('@database/models/index').NotificationTemplate const { Op } = require('sequelize') const common = require('@constants/common') -const organizationQueries = require('@database/queries/organization') exports.create = async (data) => { try { @@ -20,7 +19,7 @@ exports.findOne = async (filter, options = {}) => { raw: true, }) } catch (error) { - return error + throw error } } @@ -98,7 +97,7 @@ exports.findOneEmailTemplate = async (code, orgCode = null, tenantCode) => { } return templateData } catch (error) { - return error + throw error } } @@ -116,7 +115,7 @@ exports.getEmailHeader = async (header) => { }) return headerData } catch (error) { - return error + throw error } } @@ -134,7 +133,7 @@ exports.getEmailFooter = async (footer) => { }) return headerData } catch (error) { - return error + throw error } } @@ -148,7 +147,7 @@ exports.updateTemplate = async (filter, update, options = {}) => { return template } catch (error) { - return error + throw error } } @@ -178,7 +177,7 @@ exports.findAllNotificationTemplates = async (filter, options = {}) => { return templates } catch (error) { - return error + throw error } } From e5388899fdf3a829ffdf341c998d26b901598c8c Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Thu, 31 Jul 2025 12:12:50 +0530 Subject: [PATCH 17/55] fix: issue with app url in role req email --- src/services/org-admin.js | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/services/org-admin.js b/src/services/org-admin.js index a444284ec..5cf1fd400 100644 --- a/src/services/org-admin.js +++ b/src/services/org-admin.js @@ -23,6 +23,7 @@ const { eventBroadcaster } = require('@helpers/eventBroadcaster') const { Queue } = require('bullmq') const { Op } = require('sequelize') const UserCredentialQueries = require('@database/queries/userCredential') +const tenantDomainQueries = require('@database/queries/tenantDomain') const emailEncryption = require('@utils/emailEncryption') const responses = require('@helpers/responses') const notificationUtils = require('@utils/notification') @@ -366,7 +367,6 @@ module.exports = class OrgAdminHelper { tenant_code: tokenInformation.tenant_code, }) - console.log(isApproved, 'isApproved') if (isApproved) { await updateRoleForApprovedRequest( requestDetails, @@ -376,9 +376,13 @@ module.exports = class OrgAdminHelper { ) } - console.log(shouldSendEmail, 'shouldSendEmail') if (shouldSendEmail) { - await sendRoleRequestStatusEmail(user, bodyData.status, tokenInformation.organization_code) + await sendRoleRequestStatusEmail( + user, + bodyData.status, + tokenInformation.organization_code, + tokenInformation.tenant_code + ) } return responses.successResponse({ @@ -607,23 +611,28 @@ function updateRoleForApprovedRequest(requestDetails, user, tenantCode, orgCode) }) } -async function sendRoleRequestStatusEmail(userDetails, status, orgCode) { +async function sendRoleRequestStatusEmail(userDetails, status, organizationCode, tenantCode) { try { const plaintextEmailId = emailEncryption.decrypt(userDetails.email) if (status === common.ACCEPTED_STATUS) { if (plaintextEmailId) { + const tenantDomain = await tenantDomainQueries.findOne( + { tenant_code: tenantCode }, + { attributes: ['domain'] } + ) + notificationUtils.sendEmailNotification({ emailId: plaintextEmailId, templateCode: process.env.MENTOR_REQUEST_ACCEPTED_EMAIL_TEMPLATE_CODE, variables: { name: userDetails.name, appName: process.env.APP_NAME, - orgName: _.find(userDetails.organizations, { code: orgCode })?.name || '', - portalURL: process.env.PORTAL_URL, + orgName: _.find(userDetails.organizations, { code: organizationCode })?.name || '', + portalURL: tenantDomain, }, tenantCode: userDetails.tenant_code, - organization_code: orgCode || null, + organization_code: organizationCode || null, }) } } else if (status === common.REJECTED_STATUS) { @@ -640,10 +649,10 @@ async function sendRoleRequestStatusEmail(userDetails, status, orgCode) { }) } } - console.log({ name: userDetails.name, appName: process.env.APP_NAME, orgName: organization.name }, 'payload') return { success: true } } catch (error) { + console.error(error) return error } } From 59606a3730106561961a80ac40137025a5121478 Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Thu, 31 Jul 2025 13:58:28 +0530 Subject: [PATCH 18/55] added: await to bcryptJs --- src/services/account.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/account.js b/src/services/account.js index 0e3443e64..09bfe915f 100644 --- a/src/services/account.js +++ b/src/services/account.js @@ -727,7 +727,7 @@ module.exports = class AccountHelper { } // Verify password - const isPasswordCorrect = bcryptJs.compare(bodyData.password, user.password) + const isPasswordCorrect = await bcryptJs.compare(bodyData.password, user.password) if (!isPasswordCorrect) { return responses.failureResponse({ message: 'IDENTIFIER_OR_PASSWORD_INVALID', From 11ff426a5c1cf2c2c5d2cb9a12614ed43e92e3c5 Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Thu, 31 Jul 2025 14:02:19 +0530 Subject: [PATCH 19/55] updated: Align parameter ordering across entityTypeService methods --- src/controllers/v1/entity-type.js | 4 ++-- src/services/entityType.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/controllers/v1/entity-type.js b/src/controllers/v1/entity-type.js index 7cfc5866e..bfb565cc6 100644 --- a/src/controllers/v1/entity-type.js +++ b/src/controllers/v1/entity-type.js @@ -65,9 +65,9 @@ module.exports = class Entity { ) } return await entityTypeService.readAllSystemEntityTypes( - req.decodedToken.organization_id, req.decodedToken.organization_code, - req.decodedToken.tenant_code + req.decodedToken.tenant_code, + req.decodedToken.organization_id ) } catch (error) { return error diff --git a/src/services/entityType.js b/src/services/entityType.js index 5aa88079d..955685af5 100644 --- a/src/services/entityType.js +++ b/src/services/entityType.js @@ -91,7 +91,7 @@ module.exports = class EntityHelper { } } - static async readAllSystemEntityTypes(organizationId, organizationCode, tenantCode) { + static async readAllSystemEntityTypes(organizationCode, tenantCode, organizationId) { try { const attributes = ['value', 'label', 'id', 'organization_code'] From eb47e49bb820e4f7125af17a434d4d42845feab2 Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Thu, 31 Jul 2025 14:09:28 +0530 Subject: [PATCH 20/55] =?UTF-8?q?updated:=20the=20transitional=20dual?= =?UTF-8?q?=E2=80=90identifier=20strategy=20in=20the=20EntityType=20model?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/database/models/entityType.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/database/models/entityType.js b/src/database/models/entityType.js index 427825d7d..2f03fb4c9 100644 --- a/src/database/models/entityType.js +++ b/src/database/models/entityType.js @@ -16,6 +16,9 @@ module.exports = (sequelize, DataTypes) => { updated_by: { type: DataTypes.INTEGER, allowNull: true }, allow_filtering: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false }, data_type: { type: DataTypes.STRING, allowNull: false, defaultValue: 'STRING' }, + // NOTE: `organization_id` is temporarily retained only for the backfill + // and restore migration process. It is planned to be removed once that + // process is complete. Use `organization_code` for all ongoing references. organization_id: { type: DataTypes.INTEGER, allowNull: false }, organization_code: { type: DataTypes.STRING, From f2cb602c9dd77b0ec79e30a4f7fa59ba0c15daeb Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Thu, 31 Jul 2025 14:11:34 +0530 Subject: [PATCH 21/55] added: organization_code in the unique index on entities --- .../migrations/20250723183753-add-entity-types-constraints.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/database/migrations/20250723183753-add-entity-types-constraints.js b/src/database/migrations/20250723183753-add-entity-types-constraints.js index 1ac6179e8..60194f026 100644 --- a/src/database/migrations/20250723183753-add-entity-types-constraints.js +++ b/src/database/migrations/20250723183753-add-entity-types-constraints.js @@ -17,7 +17,7 @@ module.exports = { await queryInterface.sequelize.query( ` CREATE UNIQUE INDEX unique_entities_value_type_tenant - ON entities (value, entity_type_id, tenant_code) + ON entities (value, entity_type_id,organization_code, tenant_code) WHERE deleted_at IS NULL; `, { transaction } From 728ebc718f5c381b72855008224a783655e0f6d3 Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Thu, 31 Jul 2025 14:16:35 +0530 Subject: [PATCH 22/55] fixed: incorrect string comparison with numeric value --- src/services/entities.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/services/entities.js b/src/services/entities.js index 7e684f943..f517d4af1 100644 --- a/src/services/entities.js +++ b/src/services/entities.js @@ -153,7 +153,8 @@ module.exports = class EntityHelper { static async delete(id, organizationCode, tenantCode) { try { const deleteCount = await entityQueries.deleteOneEntity(id, organizationCode, tenantCode) - if (deleteCount === '0') { + + if (deleteCount === 0) { return responses.failureResponse({ message: 'ENTITY_NOT_FOUND', statusCode: httpStatusCode.bad_request, From b7d11d503d7aa32a8fe21143de3d46d8b7db824e Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Fri, 1 Aug 2025 11:52:31 +0530 Subject: [PATCH 23/55] updated: inheritEntityType endpoint --- src/controllers/v1/org-admin.js | 3 +- src/locales/en.json | 2 +- src/services/org-admin.js | 58 +++++++++++++++++---------------- 3 files changed, 33 insertions(+), 30 deletions(-) diff --git a/src/controllers/v1/org-admin.js b/src/controllers/v1/org-admin.js index 78e2c152c..287852060 100644 --- a/src/controllers/v1/org-admin.js +++ b/src/controllers/v1/org-admin.js @@ -193,9 +193,10 @@ module.exports = class OrgAdmin { responseCode: 'CLIENT_ERROR', }) } - let entityTypeDetails = orgAdminService.inheritEntityType( + let entityTypeDetails = await orgAdminService.inheritEntityType( req.body.entity_type_value, req.body.target_entity_type_label, + req.decodedToken.organization_code, req.decodedToken.organization_id, req.decodedToken.id ) diff --git a/src/locales/en.json b/src/locales/en.json index 8bb8d1895..5a5f4b231 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -91,7 +91,7 @@ "EMAIL_OR_ID_REQUIRED": "Either 'id' or 'email' fields is required", "REQUEST_NOT_FOUND": "Role Change Request not found.", "FAILED_TO_ASSIGN_AS_ADMIN": "User belongs to different organization", - "USER_IS_FROM_DEFAULT_ORG": "User is from default org", + "USER_IS_FROM_DEFAULT_ORG": "Users from the default organization are not permitted to proceed with this action.", "NOTIFICATION_TEMPLATE_ALREADY_EXISTS": "Notification template already exists", "NOTIFICATION_TEMPLATE_CREATED_SUCCESSFULLY": "Notification template created successfully", "NOTIFICATION_TEMPLATE_UPDATED_SUCCESSFULLY": "Notification template updated successfully", diff --git a/src/services/org-admin.js b/src/services/org-admin.js index 5cf1fd400..48db7685f 100644 --- a/src/services/org-admin.js +++ b/src/services/org-admin.js @@ -462,40 +462,37 @@ module.exports = class OrgAdminHelper { } /** - * @description - Inherit new entity type from an existing default org's entityType. + * @description - Inherit new entity type from an existing default org's entityType. * @method - * @name - inheritEntityType - * @param {String} entityValue - Entity type value - * @param {String} entityLabel - Entity type label - * @param {Integer} userOrgId - User org id - * @param {Integer} userId - Userid - * @returns {Promise} - A Promise that resolves to a response object. + * @name - inheritEntityType + * @param {String} entityValue - Entity type value + * @param {String} entityLabel - Entity type label + * @param {String} userOrganizationCode - User's organization code + * @param {Integer} userOrganizationId - User's organization ID + * @param {Integer} userId - User ID + * @returns {Promise} - A Promise that resolves to a response object. */ - - static async inheritEntityType(entityValue, entityLabel, userOrgId, userId) { + static async inheritEntityType(entityValue, entityLabel, userOrganizationCode, userOrganizationId, userId) { try { - let defaultOrgId = await organizationQueries.findOne( - { code: process.env.DEFAULT_ORGANISATION_CODE }, - { attributes: ['id'] } - ) - defaultOrgId = defaultOrgId.id - if (defaultOrgId === userOrgId) { + // Prevent inheriting from the default org + if (process.env.DEFAULT_ORGANISATION_CODE === userOrganizationCode) { return responses.failureResponse({ message: 'USER_IS_FROM_DEFAULT_ORG', statusCode: httpStatusCode.bad_request, responseCode: 'CLIENT_ERROR', }) } - // Fetch entity type data using defaultOrgId and entityValue + + // Fetch entity type data from default org const filter = { value: entityValue, - organization_id: defaultOrgId, + organization_code: process.env.DEFAULT_ORGANISATION_CODE, allow_filtering: true, } - let entityTypeDetails = await entityTypeQueries.findOneEntityType(filter) + const entityTypeDetails = await entityTypeQueries.findOneEntityType(filter) - // If no matching data found return failure response + // If no matching data found, return failure if (!entityTypeDetails) { return responses.failureResponse({ message: 'ENTITY_TYPE_NOT_FOUND', @@ -504,23 +501,28 @@ module.exports = class OrgAdminHelper { }) } - // Build data for inheriting entityType - entityTypeDetails.parent_id = entityTypeDetails.organization_id - entityTypeDetails.label = entityLabel - entityTypeDetails.organization_id = userOrgId - entityTypeDetails.created_by = userId - entityTypeDetails.updated_by = userId - delete entityTypeDetails.id + // Prepare new entity type object (clone and modify) + const newEntityType = { + ...entityTypeDetails, + parent_id: entityTypeDetails.id, + label: entityLabel, + organization_code: userOrganizationCode, + organization_id: userOrganizationId, + created_by: userId, + updated_by: userId, + } + delete newEntityType.id // Create new inherited entity type - let inheritedEntityType = await entityTypeQueries.createEntityType(entityTypeDetails) + const inheritedEntityType = await entityTypeQueries.createEntityType(newEntityType) + return responses.successResponse({ statusCode: httpStatusCode.created, message: 'ENTITY_TYPE_CREATED_SUCCESSFULLY', result: inheritedEntityType, }) } catch (error) { - console.log(error) + console.error('Error in inheritEntityType:', error) throw error } } From c5a8d3f1192ddbc9dab9d16d3db1d19461ba0e7a Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Fri, 1 Aug 2025 12:45:09 +0530 Subject: [PATCH 24/55] updated: entity_types unique index --- .../20250723183753-add-entity-types-constraints.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/database/migrations/20250723183753-add-entity-types-constraints.js b/src/database/migrations/20250723183753-add-entity-types-constraints.js index 60194f026..37b4a0042 100644 --- a/src/database/migrations/20250723183753-add-entity-types-constraints.js +++ b/src/database/migrations/20250723183753-add-entity-types-constraints.js @@ -7,6 +7,13 @@ module.exports = { // Drop existing UNIQUE constraint await queryInterface.sequelize.query( ` + ALTER TABLE entity_types + DROP CONSTRAINT IF EXISTS unique_value_org_id_tenant_code; + `, + { transaction } + ) + await queryInterface.sequelize.query( + ` ALTER TABLE entities DROP CONSTRAINT IF EXISTS unique_entities_value_type_tenant; `, @@ -16,8 +23,8 @@ module.exports = { // Add partial unique index await queryInterface.sequelize.query( ` - CREATE UNIQUE INDEX unique_entities_value_type_tenant - ON entities (value, entity_type_id,organization_code, tenant_code) + CREATE UNIQUE INDEX unique_value_org_code_tenant_code + ON entity_types (value,organization_code, tenant_code) WHERE deleted_at IS NULL; `, { transaction } From 6b551b63399a86d42a1ad78b592253cb09ea8c49 Mon Sep 17 00:00:00 2001 From: adithya_dinesh Date: Fri, 1 Aug 2025 16:07:54 +0530 Subject: [PATCH 25/55] minor fix for QA DB --- .../migrations/20250729064710-org-code-fix.js | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/database/migrations/20250729064710-org-code-fix.js b/src/database/migrations/20250729064710-org-code-fix.js index 8b155916e..094bff12a 100644 --- a/src/database/migrations/20250729064710-org-code-fix.js +++ b/src/database/migrations/20250729064710-org-code-fix.js @@ -1,5 +1,36 @@ const { Sequelize } = require('sequelize') +/* +BEGIN; + +-- Delete from user_organization_roles +DELETE FROM user_organization_roles WHERE organization_code = 'sot' RETURNING *; + +-- Delete from user_organizations and capture user_id for users deletion +WITH deleted_user_orgs AS ( + DELETE FROM user_organizations WHERE organization_code = 'sot' RETURNING user_id +) +-- Delete from users using user_id from user_organizations +DELETE FROM users WHERE id IN (SELECT user_id FROM deleted_user_orgs) RETURNING *; + +-- Delete from organizations +DELETE FROM organizations WHERE code = 'sot' RETURNING *; + +-- Delete from organization_user_invites +DELETE FROM organization_user_invites WHERE organization_code = 'sot' RETURNING *; + +-- Delete from notification_templates +DELETE FROM notification_templates WHERE organization_code = 'sot' RETURNING *; + +-- Delete from organization_features +DELETE FROM organization_features WHERE organization_code = 'sot' RETURNING *; + +-- Delete from organization_registration_codes +DELETE FROM organization_registration_codes WHERE organization_code = 'sot' RETURNING *; + +COMMIT; +*/ + module.exports = { async up(queryInterface, Sequelize) { let transaction @@ -60,6 +91,17 @@ module.exports = { raw: true, transaction, }) + table = 'organization_user_invites' + fk_name = 'fk_org_user_invites_org_code' + fkey = '(organization_code, tenant_code)' + refTable = 'organizations' + refKey = '(code, tenant_code)' + await queryInterface.sequelize.query(disableFK(table, fk_name), { + type: Sequelize.QueryTypes.RAW, + raw: true, + transaction, + }) + fk_retainer.push(enableFK(table, fk_name, fkey, refTable, refKey)) table = 'user_organization_roles' fk_name = 'fk_user_org_roles_user_organizations' From 867e2228011afa96782aaff22143d22c03f0b049 Mon Sep 17 00:00:00 2001 From: adithya_dinesh Date: Fri, 1 Aug 2025 18:08:18 +0530 Subject: [PATCH 26/55] added Script to hard - delete organization and related data --- .../migrations/20250729064710-org-code-fix.js | 31 --- .../deleted-org-data-clean-up/clean.js | 256 ++++++++++++++++++ 2 files changed, 256 insertions(+), 31 deletions(-) create mode 100644 src/scripts/deleted-org-data-clean-up/clean.js diff --git a/src/database/migrations/20250729064710-org-code-fix.js b/src/database/migrations/20250729064710-org-code-fix.js index 094bff12a..aa4889c2c 100644 --- a/src/database/migrations/20250729064710-org-code-fix.js +++ b/src/database/migrations/20250729064710-org-code-fix.js @@ -1,36 +1,5 @@ const { Sequelize } = require('sequelize') -/* -BEGIN; - --- Delete from user_organization_roles -DELETE FROM user_organization_roles WHERE organization_code = 'sot' RETURNING *; - --- Delete from user_organizations and capture user_id for users deletion -WITH deleted_user_orgs AS ( - DELETE FROM user_organizations WHERE organization_code = 'sot' RETURNING user_id -) --- Delete from users using user_id from user_organizations -DELETE FROM users WHERE id IN (SELECT user_id FROM deleted_user_orgs) RETURNING *; - --- Delete from organizations -DELETE FROM organizations WHERE code = 'sot' RETURNING *; - --- Delete from organization_user_invites -DELETE FROM organization_user_invites WHERE organization_code = 'sot' RETURNING *; - --- Delete from notification_templates -DELETE FROM notification_templates WHERE organization_code = 'sot' RETURNING *; - --- Delete from organization_features -DELETE FROM organization_features WHERE organization_code = 'sot' RETURNING *; - --- Delete from organization_registration_codes -DELETE FROM organization_registration_codes WHERE organization_code = 'sot' RETURNING *; - -COMMIT; -*/ - module.exports = { async up(queryInterface, Sequelize) { let transaction diff --git a/src/scripts/deleted-org-data-clean-up/clean.js b/src/scripts/deleted-org-data-clean-up/clean.js new file mode 100644 index 000000000..3a576e6f0 --- /dev/null +++ b/src/scripts/deleted-org-data-clean-up/clean.js @@ -0,0 +1,256 @@ +const { Sequelize } = require('sequelize') +let Table = require('cli-table') +require('dotenv').config({ path: '../../.env' }) + +// Environment setup +const nodeEnv = process.env.NODE_ENV || 'development' + +let databaseUrl + +switch (nodeEnv) { + case 'production': + databaseUrl = process?.env?.PROD_DATABASE_URL || process.env.DEV_DATABASE_URL + break + case 'test': + databaseUrl = process?.env?.TEST_DATABASE_URL || process.env.DEV_DATABASE_URL + break + default: + databaseUrl = process.env.DEV_DATABASE_URL +} + +console.info('Database selected: ', databaseUrl.split('/').at(-1)) + +// Function to prompt user and get input +function promptUser(question) { + return new Promise((resolve) => { + process.stdout.write(question) + process.stdin.once('data', (data) => { + resolve(data.toString().trim()) + }) + }) +} +// Initialize Sequelize +const sequelize = new Sequelize(databaseUrl, { + dialect: 'postgres', + logging: process.env.NODE_ENV === 'development' ? console.log : false, +}) + +;(async () => { + try { + const organizationCode = await promptUser( + '---------> Please enter the organization code to delete (e.g., sot): ' + ) + + // Test database connection + await sequelize.authenticate() + console.log('Database connection established successfully.') + REPORT_QUERY = `WITH user_org_roles AS ( + SELECT 'user_organization_roles' AS table_name, + json_agg(row_to_json(uor)) AS data, + count(*) AS row_count + FROM user_organization_roles uor + WHERE organization_code = '${organizationCode}' +), +user_orgs AS ( + SELECT 'user_organizations' AS table_name, + json_agg(row_to_json(uo)) AS data, + count(*) AS row_count + FROM user_organizations uo + WHERE organization_code = '${organizationCode}' +), +users_to_delete AS ( + SELECT 'users' AS table_name, + json_agg(row_to_json(u)) AS data, + count(*) AS row_count + FROM users u + WHERE u.id IN ( + SELECT user_id + FROM user_organizations + WHERE organization_code = '${organizationCode}' + ) +), +orgs AS ( + SELECT 'organizations' AS table_name, + json_agg(row_to_json(o)) AS data, + count(*) AS row_count + FROM organizations o + WHERE code = '${organizationCode}' +), +invites AS ( + SELECT 'organization_user_invites' AS table_name, + json_agg(row_to_json(oui)) AS data, + count(*) AS row_count + FROM organization_user_invites oui + WHERE organization_code = '${organizationCode}' +), +notifications AS ( + SELECT 'notification_templates' AS table_name, + json_agg(row_to_json(nt)) AS data, + count(*) AS row_count + FROM notification_templates nt + WHERE organization_code = '${organizationCode}' +), +features AS ( + SELECT 'organization_features' AS table_name, + json_agg(row_to_json(of)) AS data, + count(*) AS row_count + FROM organization_features of + WHERE organization_code = '${organizationCode}' +), +reg_codes AS ( + SELECT 'organization_registration_codes' AS table_name, + json_agg(row_to_json(orc)) AS data, + count(*) AS row_count + FROM organization_registration_codes orc + WHERE organization_code = '${organizationCode}' +) +SELECT table_name, row_count +FROM user_org_roles +UNION ALL +SELECT table_name, row_count +FROM user_orgs +UNION ALL +SELECT table_name, row_count +FROM users_to_delete +UNION ALL +SELECT table_name, row_count +FROM orgs +UNION ALL +SELECT table_name, row_count +FROM invites +UNION ALL +SELECT table_name, row_count +FROM notifications +UNION ALL +SELECT table_name, row_count +FROM features +UNION ALL +SELECT table_name, row_count +FROM reg_codes;` + const report = await sequelize.query(REPORT_QUERY, { + type: Sequelize.QueryTypes.RAW, + raw: true, + }) + + let table = new Table({ + head: ['Table Name', 'Row Count'], + colWidths: [30, 15], + }) + + // Populate the table with data + report[0].forEach((item) => { + table.push([item.table_name, item.row_count]) + }) + + // Print the table + console.log('\n\n\nBelow is number of rows attached to the organization code: ', organizationCode) + console.log(table.toString()) + + const confirmation = await promptUser( + '---------> Are you sure you want to delete all data related to this organization? (yes/no): ' + ) + // Start transaction + if (confirmation.toLowerCase() == 'yes') { + await sequelize.transaction(async (t) => { + // Delete from user_organization_roles + const DELETE_USER_ORG_ROLES_QUERY = + 'DELETE FROM user_organization_roles WHERE organization_code = :organizationCode RETURNING *' + const deletedUserOrgRoles = await sequelize.query(DELETE_USER_ORG_ROLES_QUERY, { + replacements: { organizationCode }, + type: Sequelize.QueryTypes.DELETE, + raw: true, + transaction: t, + }) + console.info('Deleted user_organization_roles: ', deletedUserOrgRoles) + + // Delete from user_organizations and capture user_ids + const DELETE_USER_ORGS_QUERY = + 'DELETE FROM user_organizations WHERE organization_code = :organizationCode RETURNING user_id' + const deletedUserOrgs = await sequelize.query(DELETE_USER_ORGS_QUERY, { + replacements: { organizationCode }, + type: Sequelize.QueryTypes.DELETE, + raw: true, + transaction: t, + }) + const userIdsToDelete = deletedUserOrgs.map((row) => row.user_id) + console.info('Deleted user_organizations, user_ids: ', userIdsToDelete) + + // Delete from users + if (userIdsToDelete.length > 0) { + const DELETE_USERS_QUERY = + 'DELETE FROM users WHERE id IN (:userIdsToDelete) AND NOT EXISTS (SELECT 1 FROM user_organizations WHERE user_organizations.user_id = users.id ) RETURNING *' + const deletedUsers = await sequelize.query(DELETE_USERS_QUERY, { + replacements: { userIdsToDelete }, + type: Sequelize.QueryTypes.DELETE, + raw: true, + transaction: t, + }) + console.info('Deleted users: ', deletedUsers) + } + + // Delete from organizations + const DELETE_ORGS_QUERY = 'DELETE FROM organizations WHERE code = :organizationCode RETURNING *' + const deletedOrgs = await sequelize.query(DELETE_ORGS_QUERY, { + replacements: { organizationCode }, + type: Sequelize.QueryTypes.DELETE, + raw: true, + transaction: t, + }) + console.info('Deleted organizations: ', deletedOrgs) + + // Delete from organization_user_invites + const DELETE_INVITES_QUERY = + 'DELETE FROM organization_user_invites WHERE organization_code = :organizationCode RETURNING *' + const deletedInvites = await sequelize.query(DELETE_INVITES_QUERY, { + replacements: { organizationCode }, + type: Sequelize.QueryTypes.DELETE, + raw: true, + transaction: t, + }) + console.info('Deleted organization_user_invites: ', deletedInvites) + + // Delete from notification_templates + const DELETE_NOTIFICATIONS_QUERY = + 'DELETE FROM notification_templates WHERE organization_code = :organizationCode RETURNING *' + const deletedNotifications = await sequelize.query(DELETE_NOTIFICATIONS_QUERY, { + replacements: { organizationCode }, + type: Sequelize.QueryTypes.DELETE, + raw: true, + transaction: t, + }) + console.info('Deleted notification_templates: ', deletedNotifications) + + // Delete from organization_features + const DELETE_FEATURES_QUERY = + 'DELETE FROM organization_features WHERE organization_code = :organizationCode RETURNING *' + const deletedFeatures = await sequelize.query(DELETE_FEATURES_QUERY, { + replacements: { organizationCode }, + type: Sequelize.QueryTypes.DELETE, + raw: true, + transaction: t, + }) + console.info('Deleted organization_features: ', deletedFeatures) + + // Delete from organization_registration_codes + const DELETE_REG_CODES_QUERY = + 'DELETE FROM organization_registration_codes WHERE organization_code = :organizationCode RETURNING *' + const deletedRegCodes = await sequelize.query(DELETE_REG_CODES_QUERY, { + replacements: { organizationCode }, + type: Sequelize.QueryTypes.DELETE, + raw: true, + transaction: t, + }) + console.info('Deleted organization_registration_codes: ', deletedRegCodes) + // await t.commit(); + }) + + console.log('Transaction completed successfully.') + } else { + console.log('Transaction aborted by user.') + } + } catch (error) { + console.error(`Error executing transaction: ${error}`) + } finally { + sequelize.close() + } +})() From 7f101d7d52ffb3e8c462710b54b63e846ee3296c Mon Sep 17 00:00:00 2001 From: adithya_dinesh Date: Fri, 1 Aug 2025 18:10:31 +0530 Subject: [PATCH 27/55] added Script to hard - delete organization and related data --- src/scripts/deleted-org-data-clean-up/clean.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/scripts/deleted-org-data-clean-up/clean.js b/src/scripts/deleted-org-data-clean-up/clean.js index 3a576e6f0..6b07d74e4 100644 --- a/src/scripts/deleted-org-data-clean-up/clean.js +++ b/src/scripts/deleted-org-data-clean-up/clean.js @@ -252,5 +252,6 @@ FROM reg_codes;` console.error(`Error executing transaction: ${error}`) } finally { sequelize.close() + process.exit() } })() From 79287026f86d0a4a0400052b354a82b9cd15be59 Mon Sep 17 00:00:00 2001 From: adithya_dinesh Date: Fri, 1 Aug 2025 18:35:26 +0530 Subject: [PATCH 28/55] minod changes --- .../deleted-org-data-clean-up/clean.js | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/scripts/deleted-org-data-clean-up/clean.js b/src/scripts/deleted-org-data-clean-up/clean.js index 6b07d74e4..02b28f7d7 100644 --- a/src/scripts/deleted-org-data-clean-up/clean.js +++ b/src/scripts/deleted-org-data-clean-up/clean.js @@ -18,8 +18,6 @@ switch (nodeEnv) { databaseUrl = process.env.DEV_DATABASE_URL } -console.info('Database selected: ', databaseUrl.split('/').at(-1)) - // Function to prompt user and get input function promptUser(question) { return new Promise((resolve) => { @@ -40,6 +38,16 @@ const sequelize = new Sequelize(databaseUrl, { const organizationCode = await promptUser( '---------> Please enter the organization code to delete (e.g., sot): ' ) + if (!organizationCode || organizationCode.trim() === '') { + console.error('Error: Organization code cannot be empty') + process.exit(1) + } + + // Validate format (alphanumeric and underscores only) + if (!/^[a-z0-9_]+$/.test(organizationCode)) { + console.error('Error: Organization code must contain only lowercase letters, numbers, and underscores') + process.exit(1) + } // Test database connection await sequelize.authenticate() @@ -136,9 +144,10 @@ FROM reg_codes;` head: ['Table Name', 'Row Count'], colWidths: [30, 15], }) - + let countChecker = 0 // Populate the table with data report[0].forEach((item) => { + countChecker += item.row_count table.push([item.table_name, item.row_count]) }) @@ -146,9 +155,12 @@ FROM reg_codes;` console.log('\n\n\nBelow is number of rows attached to the organization code: ', organizationCode) console.log(table.toString()) - const confirmation = await promptUser( - '---------> Are you sure you want to delete all data related to this organization? (yes/no): ' - ) + const confirmation = + countChecker > 0 + ? await promptUser( + '---------> Are you sure you want to delete all data related to this organization? (yes/no): ' + ) + : 'no' // Start transaction if (confirmation.toLowerCase() == 'yes') { await sequelize.transaction(async (t) => { @@ -246,7 +258,7 @@ FROM reg_codes;` console.log('Transaction completed successfully.') } else { - console.log('Transaction aborted by user.') + console.log(`Transaction aborted${countChecker == 0 ? '! No records to delete.' : ' by the user'} `) } } catch (error) { console.error(`Error executing transaction: ${error}`) From 6e61a4b7ea8380dde032d08d276cf04289cecd1f Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Mon, 4 Aug 2025 12:48:29 +0530 Subject: [PATCH 29/55] refactor: AdminHelper.create for improved admin user creation workflow --- src/services/admin.js | 182 ++++++++++++++++++++++++++++++++---------- 1 file changed, 138 insertions(+), 44 deletions(-) diff --git a/src/services/admin.js b/src/services/admin.js index ee6a2756e..93c558bec 100644 --- a/src/services/admin.js +++ b/src/services/admin.js @@ -6,27 +6,38 @@ */ // Dependencies +// Third-party libraries +const _ = require('lodash') +const { Op } = require('sequelize') +// Constants and generics const common = require('@constants/common') const httpStatusCode = require('@generics/http-status') const utils = require('@generics/utils') -const _ = require('lodash') -const userQueries = require('@database/queries/users') -const roleQueries = require('@database/queries/user-role') + +// Database queries const organizationQueries = require('@database/queries/organization') +const roleQueries = require('@database/queries/user-role') +const tenantQueries = require('@database/queries/tenants') +const UserCredentialQueries = require('@database/queries/userCredential') const userOrganizationQueries = require('@database/queries/userOrganization') const userOrganizationRoleQueries = require('@database/queries/userOrganizationRole') -const { eventBroadcaster } = require('@helpers/eventBroadcaster') -const { Op } = require('sequelize') -const Sequelize = require('@database/models/index').sequelize -const UserCredentialQueries = require('@database/queries/userCredential') +const userQueries = require('@database/queries/users') + +// Services const adminService = require('../generics/materializedViews') +const userSessionsService = require('@services/user-sessions') + +// Helpers and utilities +const { broadcastUserEvent } = require('@helpers/eventBroadcasterMain') const emailEncryption = require('@utils/emailEncryption') +const { eventBroadcaster } = require('@helpers/eventBroadcaster') +const { generateUniqueUsername } = require('@utils/usernameGenerator') const responses = require('@helpers/responses') -const userSessionsService = require('@services/user-sessions') const userHelper = require('@helpers/userHelper') + +// DTOs const UserTransformDTO = require('@dtos/userDTO') -const { broadcastUserEvent } = require('@helpers/eventBroadcasterMain') module.exports = class AdminHelper { /** @@ -78,68 +89,151 @@ module.exports = class AdminHelper { } /** - * create admin users + * Creates a new admin user. + * + * @async * @method * @name create - * @param {Object} bodyData - user create information - * @param {string} bodyData.email - email. - * @param {string} bodyData.password - email. - * @returns {JSON} - returns created user information + * @param {Object} bodyData - Data for creating the user. + * @param {string} bodyData.email - Email of the user. + * @param {string} bodyData.password - Password for the user account. + * @param {string} [bodyData.username] - Optional username (generated if not provided). + * @param {string} [bodyData.name] - Optional name (used for username generation if username not provided). + * @returns {Promise} - Response containing the created user data or error. + * + * @throws {Error} If default tenant or organization is not found or if creation fails. */ + static async create(bodyData) { try { + // Handle email encryption const plaintextEmailId = bodyData.email.toLowerCase() const encryptedEmailId = emailEncryption.encrypt(plaintextEmailId) - const user = await UserCredentialQueries.findOne({ email: encryptedEmailId }) - if (user) { + // Get default tenant details + const tenantDetail = await tenantQueries.findOne({ + code: process.env.DEFAULT_TENANT_CODE, + status: common.ACTIVE_STATUS, + }) + + if (!tenantDetail) { return responses.failureResponse({ - message: 'ADMIN_USER_ALREADY_EXISTS', + message: 'DEFAULT_TENANT_NOT_FOUND', statusCode: httpStatusCode.not_acceptable, responseCode: 'CLIENT_ERROR', }) } - let role = await roleQueries.findOne({ title: common.ADMIN_ROLE }) - if (!role) { + // Check if admin user already exists + const existingUser = await userQueries.findOne( + { + email: encryptedEmailId, + password: { [Op.ne]: null }, + tenant_code: tenantDetail.code, + }, + { + attributes: ['id'], + } + ) + + if (existingUser) { return responses.failureResponse({ - message: 'ROLE_NOT_FOUND', + message: 'ADMIN_USER_ALREADY_EXISTS', statusCode: httpStatusCode.not_acceptable, responseCode: 'CLIENT_ERROR', }) } - bodyData.roles = [role.id] - bodyData.organization_id = process.env.DEFAULT_ORG_ID - bodyData.tenant_code = process.env.DEFAULT_TENANT_CODE - - // if (!bodyData.organization_id) { - // let organization = await organizationQueries.findOne( - // { code: process.env.DEFAULT_ORGANISATION_CODE }, - // { attributes: ['id'] } - // ) - // bodyData.organization_id = organization.id - // } - bodyData.password = utils.hashPassword(bodyData.password) - bodyData.email = encryptedEmailId - const createdUser = await userQueries.create(bodyData) - const userCredentialsBody = { - email: bodyData.email, - password: bodyData.password, - organization_id: process.env.DEFAULT_ORG_ID, - user_id: createdUser.id, - } - const userData = await UserCredentialQueries.create(userCredentialsBody) - if (!userData?.id) { + // Find admin role + const role = await roleQueries.findOne({ + title: common.ADMIN_ROLE, + tenant_code: tenantDetail.code, + }) + + if (!role) { return responses.failureResponse({ - message: userData, + message: 'ADMIN_ROLE_NOT_FOUND', statusCode: httpStatusCode.not_acceptable, responseCode: 'CLIENT_ERROR', }) } + + // Get default organization code + const defaultOrganizationCode = process.env.DEFAULT_ORGANISATION_CODE + if (!defaultOrganizationCode) { + throw new Error('Default organization code not found.') + } + + // Prepare user data + bodyData.email = encryptedEmailId + bodyData.password = utils.hashPassword(bodyData.password) + bodyData.tenant_code = tenantDetail.code + bodyData.roles = [role.id] + + // Generate username if not provided + if (!bodyData.username) { + bodyData.username = await generateUniqueUsername(bodyData.name) + } + + // Create user + const createdUser = await userQueries.create(bodyData) + + // Create user-organization relationship + await userOrganizationQueries.create({ + user_id: createdUser.id, + organization_code: defaultOrganizationCode, + tenant_code: tenantDetail.code, + }) + + // Create user-organization-role relationship + await userOrganizationRoleQueries.create({ + tenant_code: tenantDetail.code, + user_id: createdUser.id, + organization_code: defaultOrganizationCode, + role_id: role.id, + }) + + // Get created user with organization details + const user = await userQueries.findUserWithOrganization( + { id: createdUser.id, tenant_code: tenantDetail.code }, + { + attributes: { + exclude: ['password'], + }, + } + ) + + // Process response data + const processedUser = user + processedUser.email = plaintextEmailId + + // Emit user creation event + const eventBody = UserTransformDTO.eventBodyDTO({ + entity: 'user', + eventType: 'create', + entityId: processedUser?.id, + args: { + created_by: processedUser.id, + name: processedUser?.name, + username: processedUser?.username, + email: processedUser.email, + phone: processedUser?.phone, + organizations: processedUser?.organizations, + tenant_code: processedUser?.tenant_code, + created_at: processedUser?.created_at || new Date(), + updated_at: processedUser?.updated_at || new Date(), + status: createdUser?.status || common.ACTIVE_STATUS, + deleted: false, + id: processedUser.id, + }, + }) + + broadcastUserEvent('userEvents', { requestBody: eventBody, isInternal: true }) + return responses.successResponse({ statusCode: httpStatusCode.created, - message: 'USER_CREATED_SUCCESSFULLY', + message: 'ADMIN_USER_CREATED_SUCCESSFULLY', + result: { user }, }) } catch (error) { console.log(error) From 214a8d90e7627ebfcaa01c844a568664f5fef676 Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Mon, 4 Aug 2025 13:03:11 +0530 Subject: [PATCH 30/55] removed: defaultOrganizationCode validation --- src/services/admin.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/services/admin.js b/src/services/admin.js index 93c558bec..d60944bee 100644 --- a/src/services/admin.js +++ b/src/services/admin.js @@ -160,9 +160,6 @@ module.exports = class AdminHelper { // Get default organization code const defaultOrganizationCode = process.env.DEFAULT_ORGANISATION_CODE - if (!defaultOrganizationCode) { - throw new Error('Default organization code not found.') - } // Prepare user data bodyData.email = encryptedEmailId From 911bcccf513b9182f8381990228460f776ccb960 Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Mon, 4 Aug 2025 14:09:59 +0530 Subject: [PATCH 31/55] updated: admin create to use transaction --- src/services/admin.js | 131 ++++++++++++++++++++---------------------- 1 file changed, 62 insertions(+), 69 deletions(-) diff --git a/src/services/admin.js b/src/services/admin.js index d60944bee..d731c8efd 100644 --- a/src/services/admin.js +++ b/src/services/admin.js @@ -23,6 +23,7 @@ const UserCredentialQueries = require('@database/queries/userCredential') const userOrganizationQueries = require('@database/queries/userOrganization') const userOrganizationRoleQueries = require('@database/queries/userOrganizationRole') const userQueries = require('@database/queries/users') +const { sequelize } = require('@database/models/index') // Services const adminService = require('../generics/materializedViews') @@ -105,8 +106,11 @@ module.exports = class AdminHelper { */ static async create(bodyData) { + let transaction + try { - // Handle email encryption + transaction = await sequelize.transaction() + const plaintextEmailId = bodyData.email.toLowerCase() const encryptedEmailId = emailEncryption.encrypt(plaintextEmailId) @@ -115,50 +119,27 @@ module.exports = class AdminHelper { code: process.env.DEFAULT_TENANT_CODE, status: common.ACTIVE_STATUS, }) + if (!tenantDetail) throw new Error('DEFAULT_TENANT_NOT_FOUND') - if (!tenantDetail) { - return responses.failureResponse({ - message: 'DEFAULT_TENANT_NOT_FOUND', - statusCode: httpStatusCode.not_acceptable, - responseCode: 'CLIENT_ERROR', - }) - } - - // Check if admin user already exists const existingUser = await userQueries.findOne( { email: encryptedEmailId, password: { [Op.ne]: null }, tenant_code: tenantDetail.code, }, - { - attributes: ['id'], - } + { attributes: ['id'], transaction } ) + if (existingUser) throw new Error('ADMIN_USER_ALREADY_EXISTS') - if (existingUser) { - return responses.failureResponse({ - message: 'ADMIN_USER_ALREADY_EXISTS', - statusCode: httpStatusCode.not_acceptable, - responseCode: 'CLIENT_ERROR', - }) - } - - // Find admin role - const role = await roleQueries.findOne({ - title: common.ADMIN_ROLE, - tenant_code: tenantDetail.code, - }) - - if (!role) { - return responses.failureResponse({ - message: 'ADMIN_ROLE_NOT_FOUND', - statusCode: httpStatusCode.not_acceptable, - responseCode: 'CLIENT_ERROR', - }) - } + const role = await roleQueries.findOne( + { + title: common.ADMIN_ROLE, + tenant_code: tenantDetail.code, + }, + { transaction } + ) + if (!role) throw new Error('ADMIN_ROLE_NOT_FOUND') - // Get default organization code const defaultOrganizationCode = process.env.DEFAULT_ORGANISATION_CODE // Prepare user data @@ -176,52 +157,45 @@ module.exports = class AdminHelper { const createdUser = await userQueries.create(bodyData) // Create user-organization relationship - await userOrganizationQueries.create({ - user_id: createdUser.id, - organization_code: defaultOrganizationCode, - tenant_code: tenantDetail.code, - }) + await userOrganizationQueries.create( + { + user_id: createdUser.id, + organization_code: defaultOrganizationCode, + tenant_code: tenantDetail.code, + }, + { transaction } + ) // Create user-organization-role relationship - await userOrganizationRoleQueries.create({ - tenant_code: tenantDetail.code, - user_id: createdUser.id, - organization_code: defaultOrganizationCode, - role_id: role.id, - }) + await userOrganizationRoleQueries.create( + { + tenant_code: tenantDetail.code, + user_id: createdUser.id, + organization_code: defaultOrganizationCode, + role_id: role.id, + }, + { transaction } + ) + + await transaction.commit() - // Get created user with organization details const user = await userQueries.findUserWithOrganization( { id: createdUser.id, tenant_code: tenantDetail.code }, - { - attributes: { - exclude: ['password'], - }, - } + { attributes: { exclude: ['password'] } } ) - // Process response data - const processedUser = user - processedUser.email = plaintextEmailId + const processedUser = { ...user, email: plaintextEmailId } - // Emit user creation event const eventBody = UserTransformDTO.eventBodyDTO({ entity: 'user', eventType: 'create', - entityId: processedUser?.id, + entityId: processedUser.id, args: { + ...processedUser, created_by: processedUser.id, - name: processedUser?.name, - username: processedUser?.username, - email: processedUser.email, - phone: processedUser?.phone, - organizations: processedUser?.organizations, - tenant_code: processedUser?.tenant_code, + deleted: false, created_at: processedUser?.created_at || new Date(), updated_at: processedUser?.updated_at || new Date(), - status: createdUser?.status || common.ACTIVE_STATUS, - deleted: false, - id: processedUser.id, }, }) @@ -230,11 +204,30 @@ module.exports = class AdminHelper { return responses.successResponse({ statusCode: httpStatusCode.created, message: 'ADMIN_USER_CREATED_SUCCESSFULLY', - result: { user }, + result: { user: processedUser }, }) } catch (error) { - console.log(error) - throw error + if (transaction && !transaction.finished) { + try { + await transaction.rollback() + } catch (rollbackErr) { + console.error('Rollback failed:', rollbackErr) + } + } + + switch (error.message) { + case 'DEFAULT_TENANT_NOT_FOUND': + case 'ADMIN_USER_ALREADY_EXISTS': + case 'ADMIN_ROLE_NOT_FOUND': + return responses.failureResponse({ + message: error.message, + statusCode: httpStatusCode.not_acceptable, + responseCode: 'CLIENT_ERROR', + }) + default: + console.error('Unexpected error in admin.create:', error) + throw error + } } } From 2a8b00ed040f794f546de3f02c9e93ad418dee63 Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Mon, 4 Aug 2025 14:10:48 +0530 Subject: [PATCH 32/55] added: options to create queries --- src/database/queries/userOrganization.js | 8 ++++---- src/database/queries/userOrganizationRole.js | 6 +++--- src/database/queries/users.js | 7 ++++--- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/database/queries/userOrganization.js b/src/database/queries/userOrganization.js index a7c0fe9f8..67c48a8c9 100644 --- a/src/database/queries/userOrganization.js +++ b/src/database/queries/userOrganization.js @@ -1,17 +1,17 @@ 'use strict' const { UserOrganization, Organization, UserOrganizationRole, sequelize } = require('@database/models/index') -const { Op } = require('sequelize') -exports.create = async (data) => { +exports.create = async (data, options = {}) => { try { - const createdUserOrg = await UserOrganization.create(data) + const createdUserOrg = await UserOrganization.create(data, options) return createdUserOrg.get({ plain: true }) } catch (error) { console.error(error) - return error + throw error } } + exports.changeUserOrganization = async ({ userId, tenantCode, oldOrgCode, newOrgCode }) => { const transaction = await sequelize.transaction() diff --git a/src/database/queries/userOrganizationRole.js b/src/database/queries/userOrganizationRole.js index 9263b9ea3..f13290fa0 100644 --- a/src/database/queries/userOrganizationRole.js +++ b/src/database/queries/userOrganizationRole.js @@ -6,13 +6,13 @@ const { Op } = require('sequelize') * Create a new UserOrganizationRole record * @param {Object} data - Data for creation */ -exports.create = async (data) => { +exports.create = async (data, options = {}) => { try { - const result = await UserOrganizationRole.create(data) + const result = await UserOrganizationRole.create(data, options) return result.get({ plain: true }) } catch (error) { console.error('Create Error:', error) - return error + throw error } } diff --git a/src/database/queries/users.js b/src/database/queries/users.js index 0aacefbaa..718115865 100644 --- a/src/database/queries/users.js +++ b/src/database/queries/users.js @@ -21,11 +21,12 @@ exports.getModelName = async () => { throw error } } -exports.create = async (data) => { + +exports.create = async (data, options = {}) => { try { - return await database.User.create(data) + return await database.User.create(data, options) } catch (error) { - console.log(error) + console.error(error) throw error } } From 52b1cfdaafbbc612509a05919bf43398e1772a80 Mon Sep 17 00:00:00 2001 From: adithya_dinesh Date: Mon, 4 Aug 2025 14:35:11 +0530 Subject: [PATCH 33/55] minor fixes --- .../deleted-org-data-clean-up/clean.js | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/scripts/deleted-org-data-clean-up/clean.js b/src/scripts/deleted-org-data-clean-up/clean.js index 02b28f7d7..ceaef7f3c 100644 --- a/src/scripts/deleted-org-data-clean-up/clean.js +++ b/src/scripts/deleted-org-data-clean-up/clean.js @@ -42,10 +42,9 @@ const sequelize = new Sequelize(databaseUrl, { console.error('Error: Organization code cannot be empty') process.exit(1) } - - // Validate format (alphanumeric and underscores only) - if (!/^[a-z0-9_]+$/.test(organizationCode)) { - console.error('Error: Organization code must contain only lowercase letters, numbers, and underscores') + const tenantCode = await promptUser('---------> Please enter the tenant code to delete (e.g., shikshalokam): ') + if (!tenantCode || tenantCode.trim() === '') { + console.error('Error: tenant code cannot be empty') process.exit(1) } @@ -57,14 +56,14 @@ const sequelize = new Sequelize(databaseUrl, { json_agg(row_to_json(uor)) AS data, count(*) AS row_count FROM user_organization_roles uor - WHERE organization_code = '${organizationCode}' + WHERE organization_code = '${organizationCode}' AND tenant_code = '${tenantCode}' ), user_orgs AS ( SELECT 'user_organizations' AS table_name, json_agg(row_to_json(uo)) AS data, count(*) AS row_count FROM user_organizations uo - WHERE organization_code = '${organizationCode}' + WHERE organization_code = '${organizationCode}' AND tenant_code = '${tenantCode}' ), users_to_delete AS ( SELECT 'users' AS table_name, @@ -74,7 +73,7 @@ users_to_delete AS ( WHERE u.id IN ( SELECT user_id FROM user_organizations - WHERE organization_code = '${organizationCode}' + WHERE organization_code = '${organizationCode}' AND tenant_code = '${tenantCode}' ) ), orgs AS ( @@ -82,35 +81,35 @@ orgs AS ( json_agg(row_to_json(o)) AS data, count(*) AS row_count FROM organizations o - WHERE code = '${organizationCode}' + WHERE code = '${organizationCode}' AND tenant_code = '${tenantCode}' ), invites AS ( SELECT 'organization_user_invites' AS table_name, json_agg(row_to_json(oui)) AS data, count(*) AS row_count FROM organization_user_invites oui - WHERE organization_code = '${organizationCode}' + WHERE organization_code = '${organizationCode}' AND tenant_code = '${tenantCode}' ), notifications AS ( SELECT 'notification_templates' AS table_name, json_agg(row_to_json(nt)) AS data, count(*) AS row_count FROM notification_templates nt - WHERE organization_code = '${organizationCode}' + WHERE organization_code = '${organizationCode}' AND tenant_code = '${tenantCode}' ), features AS ( SELECT 'organization_features' AS table_name, json_agg(row_to_json(of)) AS data, count(*) AS row_count FROM organization_features of - WHERE organization_code = '${organizationCode}' + WHERE organization_code = '${organizationCode}' AND tenant_code = '${tenantCode}' ), reg_codes AS ( SELECT 'organization_registration_codes' AS table_name, json_agg(row_to_json(orc)) AS data, count(*) AS row_count FROM organization_registration_codes orc - WHERE organization_code = '${organizationCode}' + WHERE organization_code = '${organizationCode}' AND tenant_code = '${tenantCode}' ) SELECT table_name, row_count FROM user_org_roles From 0a967392180c4349fffd0d4084af65cd4d4727e0 Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Mon, 4 Aug 2025 15:07:04 +0530 Subject: [PATCH 34/55] fixed: inconsistent error handling patterns across functions --- src/database/queries/userOrganization.js | 6 +++--- src/database/queries/userOrganizationRole.js | 6 +++--- src/services/admin.js | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/database/queries/userOrganization.js b/src/database/queries/userOrganization.js index 67c48a8c9..1904c662c 100644 --- a/src/database/queries/userOrganization.js +++ b/src/database/queries/userOrganization.js @@ -100,7 +100,7 @@ exports.findOne = async (filter, options = {}) => { }) } catch (error) { console.error(error) - return error + throw error } } @@ -124,7 +124,7 @@ exports.findAll = async (filter = {}, options = {}) => { }) } catch (error) { console.error(error) - return error + throw error } } @@ -140,7 +140,7 @@ exports.update = async (filter, updates) => { } catch (error) { await transaction.rollback() console.error(error) - return error + throw error } } diff --git a/src/database/queries/userOrganizationRole.js b/src/database/queries/userOrganizationRole.js index f13290fa0..7ec270467 100644 --- a/src/database/queries/userOrganizationRole.js +++ b/src/database/queries/userOrganizationRole.js @@ -30,7 +30,7 @@ exports.findOne = async (filter, options = {}) => { }) } catch (error) { console.error('FindOne Error:', error) - return error + throw error } } @@ -48,7 +48,7 @@ exports.findAll = async (filter, options = {}) => { }) } catch (error) { console.error('FindAll Error:', error) - return error + throw error } } @@ -65,7 +65,7 @@ exports.update = async (values, filter) => { return affectedCount } catch (error) { console.error('Update Error:', error) - return error + throw error } } diff --git a/src/services/admin.js b/src/services/admin.js index d731c8efd..b088777ee 100644 --- a/src/services/admin.js +++ b/src/services/admin.js @@ -154,7 +154,7 @@ module.exports = class AdminHelper { } // Create user - const createdUser = await userQueries.create(bodyData) + const createdUser = await userQueries.create(bodyData, { transaction }) // Create user-organization relationship await userOrganizationQueries.create( From 7a3d85d9f5b2ce4d7530f91b298a1026389f9425 Mon Sep 17 00:00:00 2001 From: adithya_dinesh Date: Mon, 4 Aug 2025 15:47:54 +0530 Subject: [PATCH 35/55] minor fixes --- .../deleted-org-data-clean-up/clean.js | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/src/scripts/deleted-org-data-clean-up/clean.js b/src/scripts/deleted-org-data-clean-up/clean.js index ceaef7f3c..dcf339aaf 100644 --- a/src/scripts/deleted-org-data-clean-up/clean.js +++ b/src/scripts/deleted-org-data-clean-up/clean.js @@ -165,9 +165,9 @@ FROM reg_codes;` await sequelize.transaction(async (t) => { // Delete from user_organization_roles const DELETE_USER_ORG_ROLES_QUERY = - 'DELETE FROM user_organization_roles WHERE organization_code = :organizationCode RETURNING *' + 'DELETE FROM user_organization_roles WHERE organization_code = :organizationCode AND tenant_code = :tenantCode RETURNING *' const deletedUserOrgRoles = await sequelize.query(DELETE_USER_ORG_ROLES_QUERY, { - replacements: { organizationCode }, + replacements: { organizationCode, tenantCode }, type: Sequelize.QueryTypes.DELETE, raw: true, transaction: t, @@ -176,9 +176,9 @@ FROM reg_codes;` // Delete from user_organizations and capture user_ids const DELETE_USER_ORGS_QUERY = - 'DELETE FROM user_organizations WHERE organization_code = :organizationCode RETURNING user_id' + 'DELETE FROM user_organizations WHERE organization_code = :organizationCode AND tenant_code = :tenantCode RETURNING user_id' const deletedUserOrgs = await sequelize.query(DELETE_USER_ORGS_QUERY, { - replacements: { organizationCode }, + replacements: { organizationCode, tenantCode }, type: Sequelize.QueryTypes.DELETE, raw: true, transaction: t, @@ -200,9 +200,10 @@ FROM reg_codes;` } // Delete from organizations - const DELETE_ORGS_QUERY = 'DELETE FROM organizations WHERE code = :organizationCode RETURNING *' + const DELETE_ORGS_QUERY = + 'DELETE FROM organizations WHERE code = :organizationCode AND tenant_code = :tenantCode RETURNING *' const deletedOrgs = await sequelize.query(DELETE_ORGS_QUERY, { - replacements: { organizationCode }, + replacements: { organizationCode, tenantCode }, type: Sequelize.QueryTypes.DELETE, raw: true, transaction: t, @@ -211,9 +212,9 @@ FROM reg_codes;` // Delete from organization_user_invites const DELETE_INVITES_QUERY = - 'DELETE FROM organization_user_invites WHERE organization_code = :organizationCode RETURNING *' + 'DELETE FROM organization_user_invites WHERE organization_code = :organizationCode AND tenant_code = :tenantCode RETURNING *' const deletedInvites = await sequelize.query(DELETE_INVITES_QUERY, { - replacements: { organizationCode }, + replacements: { organizationCode, tenantCode }, type: Sequelize.QueryTypes.DELETE, raw: true, transaction: t, @@ -222,9 +223,9 @@ FROM reg_codes;` // Delete from notification_templates const DELETE_NOTIFICATIONS_QUERY = - 'DELETE FROM notification_templates WHERE organization_code = :organizationCode RETURNING *' + 'DELETE FROM notification_templates WHERE organization_code = :organizationCode AND tenant_code = :tenantCode RETURNING *' const deletedNotifications = await sequelize.query(DELETE_NOTIFICATIONS_QUERY, { - replacements: { organizationCode }, + replacements: { organizationCode, tenantCode }, type: Sequelize.QueryTypes.DELETE, raw: true, transaction: t, @@ -233,9 +234,9 @@ FROM reg_codes;` // Delete from organization_features const DELETE_FEATURES_QUERY = - 'DELETE FROM organization_features WHERE organization_code = :organizationCode RETURNING *' + 'DELETE FROM organization_features WHERE organization_code = :organizationCode AND tenant_code = :tenantCode RETURNING *' const deletedFeatures = await sequelize.query(DELETE_FEATURES_QUERY, { - replacements: { organizationCode }, + replacements: { organizationCode, tenantCode }, type: Sequelize.QueryTypes.DELETE, raw: true, transaction: t, @@ -244,9 +245,9 @@ FROM reg_codes;` // Delete from organization_registration_codes const DELETE_REG_CODES_QUERY = - 'DELETE FROM organization_registration_codes WHERE organization_code = :organizationCode RETURNING *' + 'DELETE FROM organization_registration_codes WHERE organization_code = :organizationCode AND tenant_code = :tenantCode RETURNING *' const deletedRegCodes = await sequelize.query(DELETE_REG_CODES_QUERY, { - replacements: { organizationCode }, + replacements: { organizationCode, tenantCode }, type: Sequelize.QueryTypes.DELETE, raw: true, transaction: t, From 1306701ee4a5e0331f7e29452222cad7c99b1eb7 Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Mon, 4 Aug 2025 16:23:04 +0530 Subject: [PATCH 36/55] updated: acceptTermsAndCondition to work with tenant code --- src/controllers/v1/account.js | 2 +- src/locales/en.json | 2 +- src/services/account.js | 27 ++++++++++++++++----------- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/controllers/v1/account.js b/src/controllers/v1/account.js index 37f77552c..ab01de633 100644 --- a/src/controllers/v1/account.js +++ b/src/controllers/v1/account.js @@ -184,7 +184,7 @@ module.exports = class Account { try { const result = await accountService.acceptTermsAndCondition( req.decodedToken.id, - req.decodedToken.organization_id + req.decodedToken.tenant_code ) return result } catch (error) { diff --git a/src/locales/en.json b/src/locales/en.json index 5a5f4b231..0c44ebe9e 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -2,7 +2,7 @@ "EMAIL_INVALID": "Email Id is invalid.", "PASSWORD_INVALID": "Password is invalid.", "USER_ALREADY_EXISTS": "User already exists.", - "USER_DOESNOT_EXISTS": "Please enter correct email ID", + "USER_DOESNOT_EXISTS": "User does not exist.", "USERNAME_OR_PASSWORD_IS_INVALID": "Please enter the correct login ID and Password.", "EMAIL_ID_NOT_REGISTERED": "Please enter the correct login ID and Password.", "USER_CREATED_SUCCESSFULLY": "Sign-up successful, Please wait while logging in.", diff --git a/src/services/account.js b/src/services/account.js index 09bfe915f..a7cdb626a 100644 --- a/src/services/account.js +++ b/src/services/account.js @@ -1675,17 +1675,22 @@ module.exports = class AccountHelper { } /** - * Accept term and condition + * Accept terms and conditions for a user * @method * @name acceptTermsAndCondition - * @param {string} userId - userId. - * @returns {JSON} - returns accept the term success response + * @param {string|number} userId - The ID of the user accepting terms and conditions + * @param {string} tenantCode - The tenant code associated with the user + * @returns {Promise} Promise that resolves to a JSON response object containing success/failure status + * @throws {Error} Throws error if database operation fails */ - static async acceptTermsAndCondition(userId, orgId) { + static async acceptTermsAndCondition(userId, tenantCode) { try { - const user = await userQueries.findByPk(userId) + const [affectedRows] = await userQueries.updateUser( + { id: userId, tenant_code: tenantCode }, + { has_accepted_terms_and_conditions: true } + ) - if (!user) { + if (affectedRows === 0) { return responses.failureResponse({ message: 'USER_DOESNOT_EXISTS', statusCode: httpStatusCode.bad_request, @@ -1693,11 +1698,11 @@ module.exports = class AccountHelper { }) } - await userQueries.updateUser( - { id: userId, organization_id: orgId }, - { has_accepted_terms_and_conditions: true } - ) - await utilsHelper.redisDel(common.redisUserPrefix + userId.toString()) + // Clear Redis cache asynchronously (fire and forget) + const redisUserKey = `${common.redisUserPrefix}${tenantCode}_${userId}` + utilsHelper.redisDel(redisUserKey).catch((err) => { + console.error('Redis delete error:', err) + }) return responses.successResponse({ statusCode: httpStatusCode.ok, From 2f4fd2646de6432f48041ba4c0130564f8ab12f8 Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Mon, 4 Aug 2025 16:47:52 +0530 Subject: [PATCH 37/55] added: tenant code to generateToken --- src/services/account.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/account.js b/src/services/account.js index a7cdb626a..cf4ea4db4 100644 --- a/src/services/account.js +++ b/src/services/account.js @@ -911,6 +911,7 @@ module.exports = class AccountHelper { const userSessionData = await userSessionsService.findUserSession( { id: decodedToken.data.session_id, + tenant_code: decodedToken.data.tenant_code, }, { attributes: ['refresh_token'], From 32ea386087e7330b537736778a403d9495f8b195 Mon Sep 17 00:00:00 2001 From: adithya_dinesh Date: Mon, 4 Aug 2025 17:37:04 +0530 Subject: [PATCH 38/55] Tenant code filter added to organization/read internal api --- src/controllers/v1/organization.js | 3 ++- src/services/organization.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/controllers/v1/organization.js b/src/controllers/v1/organization.js index e0453c32f..e56eb6ea5 100644 --- a/src/controllers/v1/organization.js +++ b/src/controllers/v1/organization.js @@ -153,7 +153,8 @@ module.exports = class Organization { try { const result = await orgService.read( req.query.organisation_id ? req.query.organisation_id : '', - req.query.organisation_code ? req.query.organisation_code : '' + req.query.organisation_code ? req.query.organisation_code : '', + req.query.tenant_code ? req.query.tenant_code : '' ) return result } catch (error) { diff --git a/src/services/organization.js b/src/services/organization.js index 643a71b97..377dc0ecf 100644 --- a/src/services/organization.js +++ b/src/services/organization.js @@ -399,7 +399,7 @@ module.exports = class OrganizationsHelper { * @returns {JSON} - Organization creation details. */ - static async read(organisationId, organisationCode) { + static async read(organisationId, organisationCode, tenantCode = null) { try { let filter = {} // Build filter based on incoming query @@ -407,6 +407,7 @@ module.exports = class OrganizationsHelper { filter.id = parseInt(organisationId) } else { filter.code = organisationCode + if (tenantCode.trim()) filter.tenant_code = tenantCode } const organisationDetails = await organizationQueries.findOne(filter) From 785daa3d4a06c41fc392477d81514502b563d099 Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Wed, 6 Aug 2025 12:23:52 +0530 Subject: [PATCH 39/55] updated: getBulkInvitesFilesList to work with tenant_code --- src/database/models/fileUpload.js | 1 - src/database/queries/fileUpload.js | 39 +++++++++++------------------- src/services/org-admin.js | 15 +++++++----- src/validators/v1/org-admin.js | 2 +- 4 files changed, 24 insertions(+), 33 deletions(-) diff --git a/src/database/models/fileUpload.js b/src/database/models/fileUpload.js index 4aee7c075..8f1f210fb 100644 --- a/src/database/models/fileUpload.js +++ b/src/database/models/fileUpload.js @@ -44,7 +44,6 @@ module.exports = (sequelize, DataTypes) => { tenant_code: { type: DataTypes.STRING, allowNull: false, - defaultValue: '', }, }, { sequelize, modelName: 'FileUpload', tableName: 'file_uploads', freezeTableName: true, paranoid: true } diff --git a/src/database/queries/fileUpload.js b/src/database/queries/fileUpload.js index 624e06136..0ac939a1d 100644 --- a/src/database/queries/fileUpload.js +++ b/src/database/queries/fileUpload.js @@ -1,5 +1,5 @@ 'use strict' -const FileUpload = require('../models/index').FileUpload +const FileUpload = require('@database/models/index').FileUpload exports.create = async (data) => { try { @@ -7,7 +7,7 @@ exports.create = async (data) => { const result = createFileUpload.get({ plain: true }) return result } catch (error) { - return error + throw error } } @@ -19,7 +19,7 @@ exports.findOne = async (filter, options = {}) => { raw: true, }) } catch (error) { - return error + throw error } } @@ -33,38 +33,27 @@ exports.update = async (filter, update, options = {}) => { return res } catch (error) { - return error + throw error } } -exports.listUploads = async (page, limit, status, organization_id) => { +exports.listUploads = async ({ page, limit, filter = {} } = {}) => { try { - let filterQuery = { - where: {}, + const result = await FileUpload.findAndCountAll({ + where: filter, attributes: { exclude: ['created_at', 'updated_at', 'deleted_at', 'updated_by'], }, - offset: parseInt((page - 1) * limit, 10), - limit: parseInt(limit, 10), - } - - if (organization_id) { - filterQuery.where.organization_id = organization_id - } - - if (status) { - filterQuery.where.status = status - } + offset: (page - 1) * limit, + limit, + raw: true, + }) - const result = await FileUpload.findAndCountAll(filterQuery) - const transformedResult = { + return { count: result.count, - data: result.rows.map((row) => { - return row.get({ plain: true }) - }), + data: result.rows, } - return transformedResult } catch (error) { - return error + throw error } } diff --git a/src/services/org-admin.js b/src/services/org-admin.js index 48db7685f..2ead572d3 100644 --- a/src/services/org-admin.js +++ b/src/services/org-admin.js @@ -191,12 +191,15 @@ module.exports = class OrgAdminHelper { */ static async getBulkInvitesFilesList(req) { try { - let listFileUpload = await fileUploadQueries.listUploads( - req.pageNo, - req.pageSize, - req.query.status ? req.query.status : null, - req.decodedToken.organization_id - ) + const listFileUpload = await fileUploadQueries.listUploads({ + page: req.pageNo, + limit: req.pageSize, + filter: { + ...(req.query.status && { status: req.query.status }), + organization_id: req.decodedToken.organization_id, + tenant_code: req.decodedToken.tenant_code, + }, + }) if (listFileUpload.count > 0) { await Promise.all( diff --git a/src/validators/v1/org-admin.js b/src/validators/v1/org-admin.js index cfa062558..52d44e437 100644 --- a/src/validators/v1/org-admin.js +++ b/src/validators/v1/org-admin.js @@ -16,7 +16,7 @@ module.exports = { req.checkBody('upload_type') .notEmpty() .withMessage('upload_type is required') - .isIn() + .isIn(upload_type) .withMessage(`upload_type must be from : ${upload_type.join(',')}`) }, getRequestDetails: (req) => { From fd0833b6cf7078b129eb9eef9acc8a817f471da6 Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Wed, 6 Aug 2025 14:34:31 +0530 Subject: [PATCH 40/55] feat: add 'theming' column to organizations and 'configs' column to users --- ...6090006-add-theming-and-configs-columns.js | 20 +++++++++++++++++++ src/database/models/organization.js | 4 ++++ src/database/models/users.js | 4 ++++ 3 files changed, 28 insertions(+) create mode 100644 src/database/migrations/20250806090006-add-theming-and-configs-columns.js diff --git a/src/database/migrations/20250806090006-add-theming-and-configs-columns.js b/src/database/migrations/20250806090006-add-theming-and-configs-columns.js new file mode 100644 index 000000000..15479bacd --- /dev/null +++ b/src/database/migrations/20250806090006-add-theming-and-configs-columns.js @@ -0,0 +1,20 @@ +'use strict' + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn('organizations', 'theming', { + type: Sequelize.JSON, + allowNull: true, + }) + + await queryInterface.addColumn('users', 'configs', { + type: Sequelize.JSONB, + allowNull: true, + }) + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn('organizations', 'theming') + await queryInterface.removeColumn('users', 'configs') + }, +} diff --git a/src/database/models/organization.js b/src/database/models/organization.js index 3e96f7c9f..fc2140f84 100644 --- a/src/database/models/organization.js +++ b/src/database/models/organization.js @@ -38,6 +38,10 @@ module.exports = (sequelize, DataTypes) => { in_domain_visibility: { type: DataTypes.STRING, }, + theming: { + type: DataTypes.JSON, + allowNull: true, + }, tenant_code: { type: DataTypes.STRING, allowNull: false, diff --git a/src/database/models/users.js b/src/database/models/users.js index bf19e75b9..a4d612680 100644 --- a/src/database/models/users.js +++ b/src/database/models/users.js @@ -62,6 +62,10 @@ module.exports = (sequelize, DataTypes) => { type: DataTypes.STRING, defaultValue: 'en', }, + configs: { + type: DataTypes.JSONB, + allowNull: true, + }, tenant_code: { type: DataTypes.STRING, allowNull: false, From bbbf733a3ce02ee132b2081a5bd1f93a4809ba87 Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Wed, 6 Aug 2025 18:01:05 +0530 Subject: [PATCH 41/55] feat: add tenant code to role apis --- src/controllers/v1/user-role.js | 96 +++++++++++++++--------- src/database/queries/user-role.js | 4 +- src/locales/en.json | 15 ++-- src/services/user-role.js | 117 ++++++++++++++++++------------ 4 files changed, 144 insertions(+), 88 deletions(-) diff --git a/src/controllers/v1/user-role.js b/src/controllers/v1/user-role.js index 21da3569a..7bd0dc4ec 100644 --- a/src/controllers/v1/user-role.js +++ b/src/controllers/v1/user-role.js @@ -10,23 +10,28 @@ const roleService = require('@services/user-role') module.exports = class userRole { /** - * Create roles. + * Create a new role. * @method * @name create - * @param {Object} req - Request data. - * @param {Object} req.body - Request body contains role creation details. + * @param {Object} req - Request object. + * @param {Object} req.body - Request body containing role details. * @param {String} req.body.title - Title of the role. - * @param {Integer} req.body.userType - User type of the role. + * @param {Number} req.body.userType - User type for the role. * @param {String} req.body.status - Role status. - * @param {String} req.body.visibility - Visibility of the role. - * @param {String} req.body.translations - Translation for roles. - * @param {Integer} req.body.organization_id - Organization ID for the role. - * @returns {JSON} - Response contains role creation details. + * @param {String} req.body.visibility - Role visibility. + * @param {String} req.body.translations - Role translations. + * @param {Number} req.body.organization_id - Organization ID. + * @param {Object} req.decodedToken - Authenticated user token. + * @returns {Object} - Created role details. */ async create(req) { try { - const createRole = await roleService.create(req.body, req.decodedToken.organization_id) + const createRole = await roleService.create( + req.body, + req.decodedToken.organization_id, + req.decodedToken.tenant_code + ) return createRole } catch (error) { return error @@ -34,23 +39,30 @@ module.exports = class userRole { } /** - * Update roles. + * Update a role. * @method * @name update - * @param {Object} req - Request data. - * @param {Object} req.body - Request body contains role update details. + * @param {Object} req - Request object. + * @param {Object} req.body - Request body containing updated role details. * @param {String} req.body.title - Title of the role. - * @param {Integer} req.body.userType - User type of the role. + * @param {Number} req.body.userType - User type for the role. * @param {String} req.body.status - Role status. - * @param {String} req.body.visibility - Visibility of the role. - * @param {String} req.body.translations - Translation for roles. - * @param {Integer} req.body.organization_id - Organization ID for the role. - * @returns {JSON} - Response contains role update details. + * @param {String} req.body.visibility - Role visibility. + * @param {String} req.body.translations - Role translations. + * @param {Number} req.body.organization_id - Organization ID. + * @param {String} req.params.id - Role ID to be updated. + * @param {Object} req.decodedToken - Authenticated user token. + * @returns {Object} - Updated role details. */ async update(req) { try { - const updateRole = await roleService.update(req.params.id, req.body, req.decodedToken.organization_id) + const updateRole = await roleService.update( + req.params.id, + req.body, + req.decodedToken.organization_id, + req.decodedToken.tenant_code + ) return updateRole } catch (error) { return error @@ -58,32 +70,39 @@ module.exports = class userRole { } /** - * Delete role. + * Delete a role. * @method * @name delete - * @param {Object} req - Request data. - * @returns {JSON} - Role deletion response. + * @param {Object} req - Request object. + * @param {String} req.params.id - Role ID to be deleted. + * @param {Object} req.decodedToken - Authenticated user token. + * @returns {Object} - Deletion response. */ - async delete(req) { try { - return await roleService.delete(req.params.id, req.decodedToken.organization_id) + return await roleService.delete( + req.params.id, + req.decodedToken.organization_id, + req.decodedToken.tenant_code + ) } catch (error) { return error } } /** - * Get all available roles. + * Get a paginated list of roles with optional filters and search. * @method - * @name defaultlist - * @param {Array(String)} req.body.filters - Filters. + * @name list + * @param {Object} req - Request object. + * @param {Object} req.query - Query parameters. + * @param {Array} [req.query.filters] - Filters. + * @param {String} [req.query.language] - Language code. * @param {String} req.pageNo - Page number. * @param {String} req.pageSize - Page size limit. * @param {String} req.searchText - Search text. - * @param {Integer} req.decodedToken.organization_id - user organization_id. - * @query {String} req.query.language - Language Code - * @returns {JSON} - Role list. + * @param {Object} req.decodedToken - Authenticated user token. + * @returns {Object} - List of roles. */ async list(req) { try { @@ -94,7 +113,7 @@ module.exports = class userRole { req.searchText, req.decodedToken.organization_id, req.decodedToken.tenant_code, - req.query.language ? req.query.language : '' + req.query.language || '' ) return roleList } catch (error) { @@ -103,21 +122,30 @@ module.exports = class userRole { } /** - * Get all available roles. + * @deprecated + * This method is deprecated. Use `list()` instead. * @method * @name default - * @param {Array(String)} req.body.filters - Filters. + * @param {Object} req - Request object. + * @param {Array} req.body.filters - Filters. * @param {String} req.pageNo - Page number. * @param {String} req.pageSize - Page size limit. * @param {String} req.searchText - Search text. - * @returns {JSON} - Role list. + * @returns {Object} - List of roles. */ + /* async default(req) { try { - const defaultRoleList = await roleService.defaultList(req.query, req.pageNo, req.pageSize, req.searchText) + const defaultRoleList = await roleService.defaultList( + req.query, + req.pageNo, + req.pageSize, + req.searchText + ) return defaultRoleList } catch (error) { return error } } + */ } diff --git a/src/database/queries/user-role.js b/src/database/queries/user-role.js index ec62f37f3..a64644c96 100644 --- a/src/database/queries/user-role.js +++ b/src/database/queries/user-role.js @@ -64,9 +64,9 @@ exports.findAllRoles = async (filter, attributes, options) => { } } -exports.updateRole = async (filter, updatedata) => { +exports.updateRole = async (filter, updateData) => { try { - return await UserRole.update(updatedata, { + return await UserRole.update(updateData, { where: filter, returning: true, }) diff --git a/src/locales/en.json b/src/locales/en.json index 0c44ebe9e..21726f8aa 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -103,13 +103,14 @@ "ROLE_CHANGE_APPROVED": "Your request for becoming a mentor has been approved. Check email or login again to start your journey.", "MATERIALIZED_VIEW_GENERATED_SUCCESSFULLY": "Materialized views generated successfully", "MATERIALIZED_VIEW_REFRESH_INITIATED_SUCCESSFULLY": "Materialized views refresh initiated successfully", - "ROLE_CREATED_SUCCESSFULLY": "Roles added successfully", - "ROLE_UPDATED_SUCCESSFULLY": "Roles updated successfully", - "ROLE_DELETED_SUCCESSFULLY": "Module deleted successfully", - "ROLES_FETCHED_SUCCESSFULLY": "Roles fetched successfully", - "ROLE_NOT_UPDATED": "Roles not updated", - "ROLE_ALREADY_DELETED_OR_ROLE_NOT_PRESENT": "Roles already exists OR Roles not present", - "ROLE_NOT_DELETED": "Roles not deleted", + "ROLE_CREATED_SUCCESSFULLY": "Role created successfully.", + "ROLE_UPDATED_SUCCESSFULLY": "Role updated successfully.", + "ROLE_DELETED_SUCCESSFULLY": "Role deleted successfully.", + "ROLES_FETCHED_SUCCESSFULLY": "Roles fetched successfully.", + "ROLE_IS_NOT_UNIQUE": "A role with this title already exists. Please choose a different title.", + "ROLE_NOT_UPDATED": "Role could not be updated.", + "ROLE_ALREADY_DELETED_OR_ROLE_NOT_PRESENT": "The role has already been deleted or does not exist.", + "ROLE_NOT_DELETED": "Role could not be deleted.", "ROLES_HAS_EMPTY_LIST": "Empty roles list", "COLUMN_DOES_NOT_EXISTS": "Role column does not exists", "PERMISSION_DENIED": "You do not have the required permissions to access this resource. Please contact your administrator for assistance.", diff --git a/src/services/user-role.js b/src/services/user-role.js index 764f5c826..12032da20 100644 --- a/src/services/user-role.js +++ b/src/services/user-role.js @@ -17,58 +17,70 @@ const utils = require('@generics/utils') module.exports = class userRoleHelper { /** - * Create roles. + * Create a new role. * @method * @name create - * @param {Object} req - Request data. - * @param {Object} req.body - Request body contains role creation details. - * @param {String} req.body.title - Title of the role. - * @param {Integer} req.body.userType - User type of the role. - * @param {String} req.body.status - Role status. - * @param {String} req.body.translations - Translation for roles. - * @param {String} req.body.visibility - Visibility of the role. - * @param {Integer} req.body.organization_id - Organization ID for the role. - * @returns {JSON} - Response contains role creation details. + * @param {Object} bodyData - Role creation data. + * @param {string} bodyData.title - Title of the role. + * @param {number} bodyData.userType - User type of the role. + * @param {string} bodyData.status - Status of the role. + * @param {string} bodyData.translations - Translations for the role. + * @param {string} bodyData.visibility - Visibility of the role. + * @param {number} userOrganizationId - ID of the organization creating the role. + * @param {string} tenantCode - Tenant code of the requestor. + * @returns {Promise} - Created role response. */ - static async create(bodyData, userOrganizationId) { + + static async create(bodyData, userOrganizationId, tenantCode) { try { bodyData.organization_id = userOrganizationId + bodyData.tenant_code = tenantCode const roles = await roleQueries.create(bodyData) return responses.successResponse({ statusCode: httpStatusCode.created, message: 'ROLE_CREATED_SUCCESSFULLY', result: { + id: roles.id, title: roles.title, user_type: roles.user_type, status: roles.status, visibility: roles.visibility, organization_id: roles.organization_id, + tenant_code: roles.tenant_code, translations: roles.translations, }, }) } catch (error) { + if (error.name === common.SEQUELIZE_UNIQUE_CONSTRAINT_ERROR) { + return responses.failureResponse({ + statusCode: httpStatusCode.conflict, + responseCode: 'CLIENT_ERROR', + message: 'ROLE_IS_NOT_UNIQUE', + }) + } throw error } } /** - * Update roles. + * Update a role by ID. * @method * @name update - * @param {Object} req - Request data. - * @param {Object} req.body - Request body contains role update details. - * @param {String} req.body.title - Title of the role. - * @param {Integer} req.body.userType - User type of the role. - * @param {String} req.body.status - Role status. - * @param {String} req.body.visibility - Visibility of the role. - * @param {String} req.body.translations - Translation for roles. - * @param {Integer} req.body.organization_id - Organization ID for the role. - * @returns {JSON} - Response contains role update details. + * @param {number} id - Role ID to be updated. + * @param {Object} bodyData - Role update data. + * @param {string} bodyData.title - Title of the role. + * @param {number} bodyData.userType - User type of the role. + * @param {string} bodyData.status - Status of the role. + * @param {string} bodyData.visibility - Visibility of the role. + * @param {string} bodyData.translations - Translations for the role. + * @param {number} userOrganizationId - ID of the organization. + * @param {string} tenantCode - Tenant code of the requestor. + * @returns {Promise} - Updated role response. */ - static async update(id, bodyData, userOrganizationId) { + static async update(id, bodyData, userOrganizationId, tenantCode) { try { - const filter = { id: id, organization_id: userOrganizationId } + const filter = { id: id, organization_id: userOrganizationId, tenant_code: tenantCode } const [updateCount, updateRole] = await roleQueries.updateRole(filter, bodyData) if (updateCount == 0) { return responses.failureResponse({ @@ -90,25 +102,35 @@ module.exports = class userRoleHelper { }, }) } catch (error) { + if (error.name === common.SEQUELIZE_UNIQUE_CONSTRAINT_ERROR) { + return responses.failureResponse({ + statusCode: httpStatusCode.conflict, + responseCode: 'CLIENT_ERROR', + message: 'ROLE_IS_NOT_UNIQUE', + }) + } throw error } } /** - * Delete role. + * Delete a role by ID. * @method * @name delete - * @param {Object} req - Request data. - * @returns {JSON} - Role deletion response. + * @param {number} id - Role ID to be deleted. + * @param {number} userOrganizationId - ID of the organization. + * @param {string} tenantCode - Tenant code of the requestor. + * @returns {Promise} - Deletion result response. */ - static async delete(id, userOrganizationId) { + + static async delete(id, userOrganizationId, tenantCode) { try { - const filter = { id: id, organization_id: userOrganizationId } + const filter = { id: id, organization_id: userOrganizationId, tenant_code: tenantCode } const deleteRole = await roleQueries.deleteRole(filter) if (deleteRole === 0) { return responses.failureResponse({ - message: 'ROLE_NOT_DELETED', + message: 'ROLE_ALREADY_DELETED_OR_ROLE_NOT_PRESENT', statusCode: httpStatusCode.bad_request, responseCode: 'CLIENT_ERROR', }) @@ -125,15 +147,17 @@ module.exports = class userRoleHelper { } /** - * Get all available roles. + * List roles for an organization and default roles. * @method * @name list - * @param {Array(String)} req.body.filters - Filters. - * @param {String} req.pageNo - Page number. - * @param {String} req.pageSize - Page size limit. - * @param {String} req.searchText - Search text. - * @param {Integer} req.decodedToken.organization_id - user organization_id. - * @returns {JSON} - Role list. + * @param {Object} filters - Filter parameters. + * @param {number} page - Current page number. + * @param {number} limit - Number of records per page. + * @param {string} search - Search text for role title. + * @param {number} userOrganizationId - Organization ID of the requester. + * @param {string} tenantCode - Tenant code of the requester. + * @param {string} [language] - Preferred language for labels. + * @returns {Promise} - Paginated list of roles. */ static async list(filters, page, limit, search, userOrganizationId, tenantCode, language) { @@ -165,6 +189,7 @@ module.exports = class userRoleHelper { 'status', 'organization_id', 'translations', + 'tenant_code', ] const roles = await roleQueries.findAllRoles(filter, attributes, options) @@ -200,16 +225,18 @@ module.exports = class userRoleHelper { } /** - * Get all available roles. + * @deprecated + * This method is deprecated. Use `list()` instead. * @method - * @name defaultlist - * @param {Array(String)} req.body.filters - Filters. - * @param {String} req.pageNo - Page number. - * @param {String} req.pageSize - Page size limit. - * @param {String} req.searchText - Search text. - * @returns {JSON} - Role list. + * @name defaultList + * @param {Object} filters - Filter parameters. + * @param {number} page - Current page number. + * @param {number} limit - Number of records per page. + * @param {string} search - Search text for role title. + * @returns {Promise} - Paginated list of default organization roles. */ - static async defaultList(filters, page, limit, search) { + + /* static async defaultList(filters, page, limit, search) { try { delete filters.search const offset = common.getPaginationOffset(page, limit) @@ -250,5 +277,5 @@ module.exports = class userRoleHelper { } catch (error) { throw error } - } + } */ } From f8c9a774ba56715b8827642cea6305fe1f29de33 Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Wed, 6 Aug 2025 19:36:25 +0530 Subject: [PATCH 42/55] update: search api to work with tenant code and new FKs --- src/database/queries/users.js | 96 +++++++++++++++++++++++++++++++++++ src/services/account.js | 87 +++++++++++++++++-------------- src/validators/v1/account.js | 6 +++ 3 files changed, 152 insertions(+), 37 deletions(-) diff --git a/src/database/queries/users.js b/src/database/queries/users.js index 718115865..d2ac84458 100644 --- a/src/database/queries/users.js +++ b/src/database/queries/users.js @@ -388,6 +388,102 @@ exports.listUsersFromView = async ( throw error } } +exports.searchUsersWithOrganization = async ({ + roleIds, + organization_id, + page, + limit, + search, + userIds, + emailIds, + excluded_user_ids, + tenantCode, +}) => { + try { + const offset = (page - 1) * limit + + // Base filter for user + const userWhere = {} + + // Filter by userIds / exclude + if (userIds && Array.isArray(userIds)) { + userWhere.id = { [Op.in]: userIds } + if (excluded_user_ids && excluded_user_ids.length > 0) { + userWhere.id = { + [Op.and]: [{ [Op.in]: userIds }, { [Op.notIn]: excluded_user_ids }], + } + } + } else if (excluded_user_ids && excluded_user_ids.length > 0) { + userWhere.id = { [Op.notIn]: excluded_user_ids } + } + + // Filter by search text or email + if (emailIds && emailIds.length > 0) { + userWhere.email = { [Op.in]: emailIds } + } else if (search) { + userWhere.name = { [Op.iLike]: `%${search}%` } + } + + const users = await database.User.findAndCountAll({ + where: userWhere, + limit: limit, + offset: offset, + order: [['name', 'ASC']], + include: [ + { + model: database.UserOrganization, + as: 'user_organizations', + required: true, + where: { + tenant_code: tenantCode, + ...(organization_id && { organization_id }), + }, + include: [ + { + model: database.Organization, + as: 'organization', + required: false, + where: { + status: 'ACTIVE', + tenant_code: tenantCode, + }, + attributes: ['id', 'name', 'code'], + }, + { + model: database.UserOrganizationRole, + as: 'roles', + required: roleIds && roleIds.length > 0, + where: { + tenant_code: tenantCode, + role_id: { [Op.in]: roleIds }, + }, + attributes: ['role_id'], + include: [ + { + model: database.UserRole, + as: 'role', + required: false, + where: { tenant_code: tenantCode }, + attributes: ['id', 'title', 'label'], + }, + ], + }, + ], + }, + ], + raw: false, + distinct: true, // Needed for correct count when using include + }) + + return { + count: users.count, + data: users.rows, + } + } catch (error) { + console.error('Error in searchUsersWithOrganization:', error) + throw error + } +} exports.changeOrganization = async (id, currentOrgId, newOrgId, updateBody = {}) => { const transaction = await Sequelize.transaction() diff --git a/src/services/account.js b/src/services/account.js index a7cdb626a..1dffd6368 100644 --- a/src/services/account.js +++ b/src/services/account.js @@ -1827,24 +1827,32 @@ module.exports = class AccountHelper { } /** - * Account List - * @method - * @name list method post - * @param {Object} req -request data. - * @param {Array} userIds -contains userIds. - * @returns {JSON} - all accounts data - * User list. - * @method - * @name list method get - * @param {Boolean} userType - mentor/mentee. - * @param {Number} page - page No. - * @param {Number} limit - page limit. - * @param {String} search - search field. - * @returns {JSON} - List of users + * Search and list users based on filters such as role, email, search text, etc. + * + * Handles both POST-based user ID filtering and GET-based search queries. + * + * @async + * @method search + * @param {Object} params - Parameters for the search. + * @param {Object} params.query - Query parameters from the request. + * @param {string} params.query.tenant_code - Tenant identifier for scoping the search. + * @param {string} params.query.type - Role type(s) to filter by (comma-separated, e.g., "mentor,mentee" or "all"). + * @param {string} [params.query.organization_id] - Organization ID for filtering users. + * @param {string} [params.searchText] - Text input for searching users (can include emails or names). + * @param {number} [params.pageNo] - Page number for pagination. + * @param {number} [params.pageSize] - Number of users per page. + * @param {Object} [params.body] - POST body parameters. + * @param {Array} [params.body.user_ids] - Specific user IDs to include in search. + * @param {Array} [params.body.excluded_user_ids] - User IDs to exclude from search. + * + * @returns {Promise} JSON response with user list and count. */ + static async search(params) { try { - let roleQuery = {} + let roleQuery = { + tenant_code: params.query.tenant_code, + } if (params.query.type.toLowerCase() === common.TYPE_ALL) { roleQuery.status = common.ACTIVE_STATUS } else { @@ -1869,29 +1877,18 @@ module.exports = class AccountHelper { } }) - let users = await userQueries.listUsersFromView( - roleIds ? roleIds : [], - params.query.organization_id ? params.query.organization_id : '', - params.pageNo, - params.pageSize, - emailIds.length == 0 ? params.searchText : false, - params.body.user_ids ? params.body.user_ids : false, - emailIds.length > 0 ? emailIds : false, - params.body.excluded_user_ids ? params.body.excluded_user_ids : false - ) - - /* Required to resolve all promises first before preparing response object else sometime - it will push unresolved promise object if you put this logic in below for loop */ + let users = await userQueries.searchUsersWithOrganization({ + roleIds, + organization_id: params.query.organization_id, + page: params.pageNo, + limit: params.pageSize, + search: emailIds.length == 0 ? params.searchText : false, + userIds: params.body.user_ids || false, + emailIds: emailIds.length > 0 ? emailIds : false, + excluded_user_ids: params.body.excluded_user_ids || false, + tenantCode: params.query.tenant_code, + }) - await Promise.all( - users.data.map(async (user) => { - /* Assigned image url from the stored location */ - if (user.image) { - user.image = await utilsHelper.getDownloadableUrl(user.image) - } - return user - }) - ) if (users.count == 0) { return responses.successResponse({ statusCode: httpStatusCode.ok, @@ -1903,6 +1900,22 @@ module.exports = class AccountHelper { }) } + /* Required to resolve all promises first before preparing response object else sometime + it will push unresolved promise object if you put this logic in below for loop */ + // Decrypt email and add image URL + await Promise.all( + users.data.map(async (user) => { + /* Assigned image url from the stored location */ + if (user.image) { + user.image = await utilsHelper.getDownloadableUrl(user.image) + } + if (user.email) { + user.email = emailEncryption.decrypt(user.email) + } + return user + }) + ) + return responses.successResponse({ statusCode: httpStatusCode.ok, message: 'USER_LIST', diff --git a/src/validators/v1/account.js b/src/validators/v1/account.js index 205dfe1d0..ab1bef776 100644 --- a/src/validators/v1/account.js +++ b/src/validators/v1/account.js @@ -240,6 +240,11 @@ module.exports = { }, search: (req) => { + req.checkQuery('tenant_code') + .notEmpty() + .withMessage('tenant_code can not be null') + .isString() + .withMessage('tenant_code type value') req.checkQuery('type') .notEmpty() .withMessage('type can not be null') @@ -247,6 +252,7 @@ module.exports = { .notIn([common.ADMIN_ROLE, common.MENTEE_ROLE, common.MENTEE_ROLE, common.ORG_ADMIN_ROLE, common.TYPE_ALL]) .withMessage('Invalid type value') req.checkQuery('organization_id').isNumeric().withMessage('organization_id must be an Id') + req.checkBody('user_ids') .isArray() .withMessage('user_ids must be an array') From 4af2bff5bb76b51a057f63372ca2cff8d8daa2d7 Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Thu, 7 Aug 2025 13:24:12 +0530 Subject: [PATCH 43/55] add: role id as optional param --- src/database/queries/users.js | 2 +- src/services/account.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/database/queries/users.js b/src/database/queries/users.js index d2ac84458..c98c4d69f 100644 --- a/src/database/queries/users.js +++ b/src/database/queries/users.js @@ -455,7 +455,7 @@ exports.searchUsersWithOrganization = async ({ required: roleIds && roleIds.length > 0, where: { tenant_code: tenantCode, - role_id: { [Op.in]: roleIds }, + ...(roleIds && roleIds.length > 0 && { role_id: { [Op.in]: roleIds } }), }, attributes: ['role_id'], include: [ diff --git a/src/services/account.js b/src/services/account.js index 1dffd6368..5b55899f1 100644 --- a/src/services/account.js +++ b/src/services/account.js @@ -1883,9 +1883,9 @@ module.exports = class AccountHelper { page: params.pageNo, limit: params.pageSize, search: emailIds.length == 0 ? params.searchText : false, - userIds: params.body.user_ids || false, + userIds: params.body?.user_ids || false, emailIds: emailIds.length > 0 ? emailIds : false, - excluded_user_ids: params.body.excluded_user_ids || false, + excluded_user_ids: params.body?.excluded_user_ids || false, tenantCode: params.query.tenant_code, }) From d9a6e20b3307db5a976c48f4a3b2f831157ffb92 Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Fri, 8 Aug 2025 16:35:58 +0530 Subject: [PATCH 44/55] feat(org-admin): update deactivateUser endpoint to support tenant code --- src/controllers/v1/org-admin.js | 8 --- src/database/queries/users.js | 66 ++++++++++++++++++ src/helpers/userHelper.js | 60 +++++++++++++++++ src/services/org-admin.js | 116 ++++++++++++++++++++++---------- src/validators/v1/org-admin.js | 28 ++++++-- 5 files changed, 232 insertions(+), 46 deletions(-) diff --git a/src/controllers/v1/org-admin.js b/src/controllers/v1/org-admin.js index 287852060..d39cf2167 100644 --- a/src/controllers/v1/org-admin.js +++ b/src/controllers/v1/org-admin.js @@ -161,14 +161,6 @@ module.exports = class OrgAdmin { }) } - if (!req.body.id && !req.body.email) { - throw responses.failureResponse({ - message: 'EMAIL_OR_ID_REQUIRED', - statusCode: httpStatusCode.bad_request, - responseCode: 'CLIENT_ERROR', - }) - } - const result = await orgAdminService.deactivateUser(req.body, req.decodedToken) return result diff --git a/src/database/queries/users.js b/src/database/queries/users.js index c98c4d69f..51246a988 100644 --- a/src/database/queries/users.js +++ b/src/database/queries/users.js @@ -519,3 +519,69 @@ exports.changeOrganization = async (id, currentOrgId, newOrgId, updateBody = {}) throw error } } + +/** + * Deactivates users within a specific organization and tenant based on the provided filter. + * + * This function: + * - First fetches all users matching the `filter` and ensures they belong to the given `organization_code` and `tenant_code`. + * - Updates the matched users' records with the given `updateData`. + * - Optionally returns the list of matched user records if `returnUpdatedUsers` is set to true. + * + * Note: + * - Users not associated with the given organization/tenant are excluded. + * - This function currently assumes each user belongs to only one organization in this context. + * + * @async + * @param {Object} filter - Sequelize-compatible filter criteria for selecting users. + * @param {string} organization_code - Code of the organization to scope user lookup. + * @param {string} tenant_code - Tenant code for further scoping the user lookup. + * @param {Object} updateData - Fields to update for the matched users (e.g., status, updated_by). + * @param {boolean} [returnUpdatedUsers=false] - Whether to return the list of matched user records. + * + * @returns {Promise<[number, Object[]]>} A tuple: + * - First item: Number of rows affected by the update. + * - Second item: Array of user records if `returnUpdatedUsers` is true, otherwise an empty array. + * + * @throws {Error} Throws if there is any issue during the query or update. + */ + +exports.deactivateUserInOrg = async ( + filter, + organization_code, + tenant_code, + updateData, + returnUpdatedUsers = false +) => { + try { + const users = await database.User.findAll({ + where: filter, + include: [ + { + model: database.UserOrganization, + as: 'user_organizations', + required: true, + where: { + organization_code, + tenant_code, + }, + attributes: [], + }, + ], + attributes: ['id'], + }) + + const userIds = users.map((u) => u.id) + + if (userIds.length === 0) return [0, []] + + const [rowsAffected] = await database.User.update(updateData, { + where: { id: { [Op.in]: userIds } }, + }) + + return [rowsAffected, returnUpdatedUsers ? users : []] + } catch (error) { + console.error('Error in deactivateUserInOrg:', error) + throw error + } +} diff --git a/src/helpers/userHelper.js b/src/helpers/userHelper.js index 58b93cd50..d3c160988 100644 --- a/src/helpers/userHelper.js +++ b/src/helpers/userHelper.js @@ -7,6 +7,7 @@ const userSessionsService = require('@services/user-sessions') const Sequelize = require('@database/models/index').sequelize const REDIS_USER_PREFIX = common.redisUserPrefix const DELETED_STATUS = common.DELETED_STATUS +const userSessionsQueries = require('@database/queries/user-sessions') function generateUpdateParams(userId) { return { @@ -94,6 +95,65 @@ const userHelper = { transactionOptions ) }, + + /** + * Removes all active sessions for the specified user(s) within a tenant. + * + * This function performs the following: + * - Accepts one or more user IDs and ensures they belong to the given tenant. + * - Retrieves all active sessions (where `ended_at` is null). + * - Deletes associated session keys from Redis. + * - Marks the sessions as ended in the database by setting `ended_at` to the current timestamp. + * + * @async + * @param {number|number[]} userIds - A single user ID or an array of user IDs whose sessions should be removed. + * @param {string} tenantCode - The tenant code used to scope session lookup. + * + * @returns {Promise<{ success: boolean, removedCount: number }>} Object indicating success status and number of sessions removed. + * + * @throws {Error} If any step in the removal process fails (Redis or DB update). + */ + + async removeAllUserSessions(userIds, tenantCode) { + try { + const userIdArray = Array.isArray(userIds) ? userIds : [userIds] + + if (userIdArray.length === 0) { + return { success: true, removedCount: 0 } + } + + // Find all active sessions for the user(s) + const sessions = await userSessionsQueries.findAll( + { + user_id: userIdArray, + tenant_code: tenantCode, + ended_at: null, + }, + { attributes: ['id'] } + ) + + const sessionIds = sessions.map((s) => s.id) + + if (sessionIds.length === 0) { + return { success: true, removedCount: 0 } + } + + // Delete from Redis + await Promise.all(sessionIds.map((id) => utils.redisDel(id.toString()))) + + // Mark sessions as ended in DB + const currentTime = Math.floor(Date.now() / 1000) + const updateResult = await userSessionsQueries.update({ id: sessionIds }, { ended_at: currentTime }) + + if (updateResult instanceof Error) { + throw updateResult + } + + return { success: true, removedCount: sessionIds.length } + } catch (error) { + throw new Error(`Failed to remove sessions: ${error.message}`) + } + }, } module.exports = userHelper diff --git a/src/services/org-admin.js b/src/services/org-admin.js index 2ead572d3..0dc060fd5 100644 --- a/src/services/org-admin.js +++ b/src/services/org-admin.js @@ -22,11 +22,12 @@ const userOrganizationRoleQueries = require('@database/queries/userOrganizationR const { eventBroadcaster } = require('@helpers/eventBroadcaster') const { Queue } = require('bullmq') const { Op } = require('sequelize') -const UserCredentialQueries = require('@database/queries/userCredential') + const tenantDomainQueries = require('@database/queries/tenantDomain') const emailEncryption = require('@utils/emailEncryption') const responses = require('@helpers/responses') const notificationUtils = require('@utils/notification') +const userHelper = require('@helpers/userHelper') module.exports = class OrgAdminHelper { /** @@ -400,46 +401,85 @@ module.exports = class OrgAdminHelper { } /** - * Deactivate User - * @method - * @name deactivateUser - * @param {Number} id - user id - * @param {Object} loggedInUserId - logged in user id - * @returns {JSON} - Deactivated user data + * Deactivates users by their IDs or email addresses within a specific organization and tenant. + * + * This function performs the following: + * - Deactivates users by matching user IDs (if provided) under the same tenant and organization. + * - Deactivates users by matching emails (if provided) under the same tenant and organization. + * - Broadcasts an event to handle cleanup of upcoming sessions for deactivated users. + * - Removes all active sessions for the affected users. + * + * Note: + * - This function currently does **not** support users associated with multiple organizations. + * It only considers users under the organization specified in `tokenInformation.organization_code`. + * + * @async + * @param {Object} bodyData - Request payload containing user identifiers. + * @param {number[]} [bodyData.ids] - Optional array of user IDs to deactivate. + * @param {string[]} [bodyData.emails] - Optional array of user emails to deactivate. + * + * @param {Object} tokenInformation - Authenticated user's context information. + * @param {string} tokenInformation.organization_code - The organization code for scoping the operation. + * @param {string} tokenInformation.tenant_code - The tenant code to match users within the same tenant. + * @param {number} tokenInformation.id - The ID of the user initiating the deactivation (used as `updated_by`). + * + * @returns {Promise} Response object with success or failure details. + * - If successful, includes the user IDs that were deactivated. + * - If no users were updated, returns a failure response with appropriate status. + * + * @throws {Error} Throws error on unexpected failure during the deactivation process. */ + static async deactivateUser(bodyData, tokenInformation) { try { - let filterQuery = { - organization_id: tokenInformation.organization_id, - } + const { ids = [], emails = [] } = bodyData - for (let item in bodyData) { - filterQuery[item] = { - [Op.in]: bodyData[item], - } + let totalRowsAffected = 0 + const updatedByIds = [] + const updatedByEmails = [] + + // Deactivate by IDs + if (ids.length) { + const [rowsAffected, updatedUsers] = await userQueries.deactivateUserInOrg( + { + id: { [Op.in]: ids }, + tenant_code: tokenInformation.tenant_code, + }, + tokenInformation.organization_code, + tokenInformation.tenant_code, + { + status: common.INACTIVE_STATUS, + updated_by: tokenInformation.id, + }, + true // pass flag to return matched users (optional) + ) + console.log(rowsAffected, updatedUsers) + totalRowsAffected += rowsAffected + updatedByIds.push(...updatedUsers.map((user) => user.id)) } - let userIds = [] - if (bodyData.email) { - const encryptedEmailIds = bodyData.email.map((email) => emailEncryption.encrypt(email.toLowerCase())) - const userCredentials = await UserCredentialQueries.findAll( - { email: { [Op.in]: encryptedEmailIds } }, + // Deactivate by Emails + if (emails.length) { + const [rowsAffected, updatedUsers] = await userQueries.deactivateUserInOrg( { - attributes: ['user_id'], - } + email: { [Op.in]: emails.map((email) => emailEncryption.encrypt(email.toLowerCase())) }, + tenant_code: tokenInformation.tenant_code, + }, + tokenInformation.organization_code, + tokenInformation.tenant_code, + { + status: common.INACTIVE_STATUS, + updated_by: tokenInformation.id, + }, + true // return updated users ) - userIds = _.map(userCredentials, 'user_id') - delete filterQuery.email - filterQuery.id = userIds - } else { - userIds = bodyData.id + + totalRowsAffected += rowsAffected + updatedByEmails.push(...updatedUsers.map((user) => user.id)) } - let [rowsAffected] = await userQueries.updateUser(filterQuery, { - status: common.INACTIVE_STATUS, - updated_by: tokenInformation.id, - }) - if (rowsAffected == 0) { + // If nothing was deactivated + if (totalRowsAffected === 0) { return responses.failureResponse({ message: 'STATUS_UPDATE_FAILED', statusCode: httpStatusCode.bad_request, @@ -447,19 +487,27 @@ module.exports = class OrgAdminHelper { }) } - //check and deactivate upcoming sessions + // Broadcast event + const allUserIds = [...new Set([...updatedByIds, ...updatedByEmails])] + + userHelper.removeAllUserSessions(allUserIds, tokenInformation.tenant_code) + eventBroadcaster('deactivateUpcomingSession', { requestBody: { - user_ids: userIds, + user_ids: allUserIds, }, }) return responses.successResponse({ statusCode: httpStatusCode.ok, message: 'USER_DEACTIVATED', + result: { + updated_by_ids: updatedByIds, + updated_by_emails: updatedByEmails, + }, }) } catch (error) { - console.log(error) + console.error('Error in deactivateUser:', error) throw error } } diff --git a/src/validators/v1/org-admin.js b/src/validators/v1/org-admin.js index 52d44e437..a6deeca56 100644 --- a/src/validators/v1/org-admin.js +++ b/src/validators/v1/org-admin.js @@ -28,10 +28,30 @@ module.exports = { req.checkBody('status').notEmpty().withMessage('status field is empty') }, deactivateUser: (req) => { - const field = req.body.email ? 'email' : req.body.id ? 'id' : null - if (field) { - req.checkBody(field).isArray().notEmpty().withMessage(` ${field} must be an array and should not be empty.`) - } + req.checkBody('emails').optional().isArray().withMessage('The "emails" field must be an array, if provided.') + + req.checkBody('emails.*') + .optional() + .isEmail() + .withMessage('Each item in the "emails" array must be a valid email address.') + + req.checkBody('ids').optional().isArray().withMessage('The "ids" field must be an array, if provided.') + + req.checkBody('ids.*') + .optional() + .isNumeric() + .withMessage('Each item in the "ids" array must be a numeric value.') + + req.checkBody(['emails', 'ids']).custom(() => { + const ids = req.body.ids + const emails = req.body.emails + + if (!emails && !ids) { + throw new Error('At least one of "emails" or "ids" must be provided.') + } + + return true + }) }, inheritEntityType: (req) => { // Validate incoming request body From fc8f3d06817e7af9de646b5c6188565585535adf Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Fri, 8 Aug 2025 20:55:03 +0530 Subject: [PATCH 45/55] improved validation --- src/validators/v1/org-admin.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/validators/v1/org-admin.js b/src/validators/v1/org-admin.js index a6deeca56..fe76893d4 100644 --- a/src/validators/v1/org-admin.js +++ b/src/validators/v1/org-admin.js @@ -46,8 +46,8 @@ module.exports = { const ids = req.body.ids const emails = req.body.emails - if (!emails && !ids) { - throw new Error('At least one of "emails" or "ids" must be provided.') + if ((!emails || emails.length === 0) && (!ids || ids.length === 0)) { + throw new Error('Provide at least one non-empty "emails" or "ids" array.') } return true From a9538e3c18be29e6daf1b4ca20508d829f74ee4e Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Fri, 8 Aug 2025 20:55:54 +0530 Subject: [PATCH 46/55] add: await to removeAllUserSessions --- src/services/org-admin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/org-admin.js b/src/services/org-admin.js index 0dc060fd5..3ae9964dd 100644 --- a/src/services/org-admin.js +++ b/src/services/org-admin.js @@ -490,7 +490,7 @@ module.exports = class OrgAdminHelper { // Broadcast event const allUserIds = [...new Set([...updatedByIds, ...updatedByEmails])] - userHelper.removeAllUserSessions(allUserIds, tokenInformation.tenant_code) + await userHelper.removeAllUserSessions(allUserIds, tokenInformation.tenant_code) eventBroadcaster('deactivateUpcomingSession', { requestBody: { From 908ef73b42a568dc9c053563df512004702f5534 Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Fri, 8 Aug 2025 20:59:01 +0530 Subject: [PATCH 47/55] add: tenantCode to user query --- src/database/queries/users.js | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/database/queries/users.js b/src/database/queries/users.js index 51246a988..fef160021 100644 --- a/src/database/queries/users.js +++ b/src/database/queries/users.js @@ -546,13 +546,7 @@ exports.changeOrganization = async (id, currentOrgId, newOrgId, updateBody = {}) * @throws {Error} Throws if there is any issue during the query or update. */ -exports.deactivateUserInOrg = async ( - filter, - organization_code, - tenant_code, - updateData, - returnUpdatedUsers = false -) => { +exports.deactivateUserInOrg = async (filter, organizationCode, tenantCode, updateData, returnUpdatedUsers = false) => { try { const users = await database.User.findAll({ where: filter, @@ -562,8 +556,8 @@ exports.deactivateUserInOrg = async ( as: 'user_organizations', required: true, where: { - organization_code, - tenant_code, + organization_code: organizationCode, + tenant_code: tenantCode, }, attributes: [], }, @@ -576,7 +570,7 @@ exports.deactivateUserInOrg = async ( if (userIds.length === 0) return [0, []] const [rowsAffected] = await database.User.update(updateData, { - where: { id: { [Op.in]: userIds } }, + where: { id: { [Op.in]: userIds, tenant_code: tenantCode } }, }) return [rowsAffected, returnUpdatedUsers ? users : []] From 8e2c8f05dc8ac801ae5791579908f718f7e5c98d Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Fri, 8 Aug 2025 21:02:22 +0530 Subject: [PATCH 48/55] fix: deactivateUserInOrg --- src/database/queries/users.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/database/queries/users.js b/src/database/queries/users.js index fef160021..75a004fd7 100644 --- a/src/database/queries/users.js +++ b/src/database/queries/users.js @@ -570,7 +570,7 @@ exports.deactivateUserInOrg = async (filter, organizationCode, tenantCode, updat if (userIds.length === 0) return [0, []] const [rowsAffected] = await database.User.update(updateData, { - where: { id: { [Op.in]: userIds, tenant_code: tenantCode } }, + where: { id: { [Op.in]: userIds }, tenant_code: tenantCode }, }) return [rowsAffected, returnUpdatedUsers ? users : []] From bce9b4f733856ff2a2e62e423d85b422ab2e32ec Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Tue, 12 Aug 2025 13:21:45 +0530 Subject: [PATCH 49/55] fix: password policy message --- src/envVariables.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/envVariables.js b/src/envVariables.js index 6d38bf056..dbb6ee3e2 100644 --- a/src/envVariables.js +++ b/src/envVariables.js @@ -209,7 +209,7 @@ let enviromentVariables = { message: 'Required password policy message', optional: true, default: - 'Password must have at least one uppercase letter, one number, one special character, and be at least 10 characters long', + 'Password must have at least two uppercase letters, two numbers, three special characters, and be at least 11 characters long.', }, DOWNLOAD_URL_EXPIRATION_DURATION: { message: 'Required downloadable url expiration time', From a96b870a7fd404d0f263cf3b332bf27132276b0b Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Tue, 12 Aug 2025 14:29:49 +0530 Subject: [PATCH 50/55] feat: deactivateOrg to work with tenant code --- src/controllers/v1/admin.js | 32 +++++++++++++--- src/services/admin.js | 76 ++++++++++++++++++++++--------------- 2 files changed, 71 insertions(+), 37 deletions(-) diff --git a/src/controllers/v1/admin.js b/src/controllers/v1/admin.js index a7f9128cc..b18b4c3ff 100644 --- a/src/controllers/v1/admin.js +++ b/src/controllers/v1/admin.js @@ -120,12 +120,28 @@ module.exports = class Admin { } /** - * Deactivate Org - * @method - * @name deactivateOrg - * @param {String} req.params.id - org Id. - * @returns {JSON} - deactivated org response + * Deactivate an organization by its ID. + * + * Validates that the requesting user has admin access before deactivating + * the specified organization and all associated users. + * + * @async + * @method deactivateOrg + * @param {Object} req - Express request object. + * @param {string} req.params.id - The unique identifier (ID or code) of the organization to deactivate. + * @param {Object} req.decodedToken - Decoded JWT token of the authenticated user. + * @param {string[]} req.decodedToken.roles - Roles assigned to the logged-in user. + * @param {number} req.decodedToken.id - ID of the logged-in user. + * @param {Object} req.headers - HTTP request headers. + * @param {string} req.headers.tenant-id - Tenant code associated with the request. + * @returns {Promise} Response object from `adminService.deactivateOrg`, + * containing status, message, and deactivated user count. + * + * @throws {Object} Failure response if: + * - The user is not an admin. + * - The organization cannot be deactivated. */ + async deactivateOrg(req) { try { if (!utilsHelper.validateRoleAccess(req.decodedToken.roles, common.ADMIN_ROLE)) { @@ -136,7 +152,11 @@ module.exports = class Admin { }) } - const result = await adminService.deactivateOrg(req.params.id, req.decodedToken.id) + const result = await adminService.deactivateOrg( + req.params.id, + req.headers?.['tenant-id'], + req.decodedToken.id + ) return result } catch (error) { return error diff --git a/src/services/admin.js b/src/services/admin.js index b088777ee..4b515d6a2 100644 --- a/src/services/admin.js +++ b/src/services/admin.js @@ -568,19 +568,32 @@ module.exports = class AdminHelper { } /** - * Deactivate Organization - * @method - * @name deactivateOrg - * @param {Number} id - org id - * @param {Object} loggedInUserId - logged in user id - * @returns {JSON} - Deactivated user count + * Deactivate an organization and all its associated users. + * + * This method: + * 1. Updates the organization's status to inactive. + * 2. Deactivates all users belonging to the organization. + * 3. Ends all active sessions for the deactivated users. + * 4. Broadcasts an event to end any upcoming sessions. + * + * @async + * @function deactivateOrg + * @param {string} organizationCode - The unique code identifying the organization. + * @param {string} tenantCode - The tenant code to which the organization belongs. + * @param {number} loggedInUserId - The ID of the user performing the deactivation. + * @returns {Promise} Success or failure response object containing: + * - {number} result.deactivated_users - The number of users deactivated. + * + * @throws {Error} Will throw an error if the organization status update fails or if a database error occurs. */ - static async deactivateOrg(id, loggedInUserId) { + + static async deactivateOrg(organizationCode, tenantCode, loggedInUserId) { try { - //deactivate org - let rowsAffected = await organizationQueries.update( + // 1. Deactivate org + const orgRowsAffected = await organizationQueries.update( { - id, + code: organizationCode, + tenant_code: tenantCode, }, { status: common.INACTIVE_STATUS, @@ -588,7 +601,7 @@ module.exports = class AdminHelper { } ) - if (rowsAffected == 0) { + if (orgRowsAffected === 0) { return responses.failureResponse({ message: 'STATUS_UPDATE_FAILED', statusCode: httpStatusCode.bad_request, @@ -596,42 +609,43 @@ module.exports = class AdminHelper { }) } - //deactivate all users in org - const [modifiedCount] = await userQueries.updateUser( + // 2. Deactivate all users in the org using the same helper as deactivateUser + const [userRowsAffected, updatedUsers] = await userQueries.deactivateUserInOrg( { - organization_id: id, + tenant_code: tenantCode, }, + organizationCode, + tenantCode, { status: common.INACTIVE_STATUS, updated_by: loggedInUserId, - } - ) - - const users = await userQueries.findAll( - { - organization_id: id, }, - { - attributes: ['id'], - } + true // so we can get the user IDs ) - const userIds = _.map(users, 'id') - eventBroadcaster('deactivateUpcomingSession', { - requestBody: { - user_ids: userIds, - }, - }) + // 3. Broadcast & remove sessions if users were found + if (userRowsAffected > 0) { + const userIds = updatedUsers.map((u) => u.id) + // End all active sessions for those users + await userHelper.removeAllUserSessions(userIds, tenantCode) + + // Broadcast to end upcoming sessions + eventBroadcaster('deactivateUpcomingSession', { + requestBody: { user_ids: userIds }, + }) + } + + // 4. Return success return responses.successResponse({ statusCode: httpStatusCode.ok, message: 'ORG_DEACTIVATED', result: { - deactivated_users: modifiedCount, + deactivated_users: userRowsAffected, }, }) } catch (error) { - console.log(error) + console.error('Error in deactivateOrg:', error) throw error } } From ee0c85514418306b26ddf1c92baef7f7a3d9db98 Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Tue, 12 Aug 2025 19:50:45 +0530 Subject: [PATCH 51/55] fix: admin login to work with tenant flow --- src/services/admin.js | 199 ++++++++++++++++++++++++++---------------- 1 file changed, 122 insertions(+), 77 deletions(-) diff --git a/src/services/admin.js b/src/services/admin.js index 4b515d6a2..b1f633357 100644 --- a/src/services/admin.js +++ b/src/services/admin.js @@ -9,6 +9,7 @@ // Third-party libraries const _ = require('lodash') const { Op } = require('sequelize') +const bcryptJs = require('bcryptjs') // Constants and generics const common = require('@constants/common') @@ -232,103 +233,144 @@ module.exports = class AdminHelper { } /** - * login admin user - * @method - * @name login - * @param {Object} bodyData - user login data. - * @param {string} bodyData.email - email. - * @param {string} bodyData.password - email. - * @param {string} deviceInformation - device information. - * @returns {JSON} - returns login response + * Handles the login process for admin users. + * + * This method validates user credentials and enforces additional checks + * (such as admin role verification and active session limits) before + * generating access/refresh tokens and creating a user session. + * + * Steps performed: + * 1. Normalize and validate the identifier (email, phone, or username). + * 2. Construct the user lookup query using the DEFAULT_TENANT_CODE. + * 3. Retrieve the user with their associated organizations and roles. + * 4. Verify the user's password using bcrypt. + * 5. Ensure the user has the required admin role (`common.ADMIN_ROLE`). + * 6. Enforce allowed active session limits (if configured). + * 7. Create a new user session record. + * 8. Enrich token payload with user and organization details. + * 9. Generate access and refresh tokens. + * 10. Remove sensitive data and process user image URLs. + * 11. Update session details in Redis for active tracking. + * 12. Return tokens and user details in the success response. + * + * @async + * @param {Object} bodyData - Login request payload. + * @param {string} [bodyData.identifier] - The user's email, phone, or username. + * @param {string} [bodyData.email] - The user's email (alternative to `identifier`). + * @param {string} [bodyData.phone_code] - Phone country code (required if using phone login). + * @param {string} bodyData.password - The user's password. + * @param {Object} deviceInformation - Metadata about the device used for login. + * + * @returns {Promise} A success response containing: + * - `access_token` {string} - JWT for API access. + * - `refresh_token` {string} - JWT for refreshing sessions. + * - `user` {Object} - Sanitized user object with organization details. + * + * @throws {Error} Rethrows unexpected errors for global error handling. + * + * @example + * const loginResponse = await AuthService.login( + * { identifier: 'admin@example.com', password: 'StrongPass123!' }, + * { ip: '192.168.1.1', device: 'Chrome on Windows' } + * ); + * console.log(loginResponse.result.access_token); */ + static async login(bodyData, deviceInformation) { try { - const plaintextEmailId = bodyData.email.toLowerCase() - const encryptedEmailId = emailEncryption.encrypt(plaintextEmailId) - const userCredentials = await UserCredentialQueries.findOne({ email: encryptedEmailId }) - if (!userCredentials) { - return responses.failureResponse({ - message: 'USER_DOESNOT_EXISTS', - statusCode: httpStatusCode.bad_request, + // helper for consistent failure responses + const failure = (message, status = httpStatusCode.bad_request) => + responses.failureResponse({ + message, + statusCode: status, responseCode: 'CLIENT_ERROR', }) + + // 1) Identifier handling: accept `identifier` or fallback to `email` + const rawIdentifier = (bodyData.identifier || bodyData.email || '').toString().trim() + const identifier = rawIdentifier.toLowerCase() + if (!identifier) return failure('IDENTIFIER_REQUIRED', httpStatusCode.bad_request) + + // identifier type helpers + const isEmail = (str) => /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/.test(str) + const isPhone = (str) => /^\+?[1-9]\d{1,14}$/.test(str) + const isUsername = (str) => /^[a-zA-Z0-9_]{3,30}$/.test(str) + + // 2) Build query (skip domain checks; use DEFAULT_TENANT_CODE) + const query = { + [Op.or]: [], + password: { [Op.ne]: null }, + status: common.ACTIVE_STATUS, + tenant_code: process.env.DEFAULT_TENANT_CODE, } - let user = await userQueries.findOne({ - id: userCredentials.user_id, - // organization_id: userCredentials.organization_id, - }) - if (!user.id) { - return responses.failureResponse({ - message: 'USER_DOESNOT_EXISTS', - statusCode: httpStatusCode.bad_request, - responseCode: 'CLIENT_ERROR', + if (isEmail(identifier)) { + query[Op.or].push({ email: emailEncryption.encrypt(identifier) }) + } else if (isPhone(identifier)) { + // expects bodyData.phone_code when phone login is used + query[Op.or].push({ + phone: emailEncryption.encrypt(identifier), + phone_code: bodyData.phone_code, }) + } else { + query[Op.or].push({ username: identifier }) } - const isPasswordCorrect = utils.comparePassword(bodyData.password, user.password) + + // 3) Find user (reuse the helper that returns org associations like user/login) + const userInstance = await userQueries.findUserWithOrganization(query, {}, true) + let user = userInstance ? userInstance.toJSON() : null + + if (!user) return failure('IDENTIFIER_OR_PASSWORD_INVALID', httpStatusCode.bad_request) + + //Password verification (bcrypt async compare) + const isPasswordCorrect = await bcryptJs.compare(bodyData.password, user.password) if (!isPasswordCorrect) { - return responses.failureResponse({ - message: 'PASSWORD_INVALID', - statusCode: httpStatusCode.bad_request, - responseCode: 'CLIENT_ERROR', - }) + return failure('IDENTIFIER_OR_PASSWORD_INVALID', httpStatusCode.bad_request) } - let roles = await roleQueries.findAll( - { id: user.roles }, - { - attributes: { - exclude: ['created_at', 'updated_at', 'deleted_at'], - }, - } + // Check for admin role + const hasAdminRole = user.user_organizations?.some((org) => + org.roles?.some((r) => r.role?.title?.toLowerCase() === common.ADMIN_ROLE) ) - if (!roles) { - return responses.failureResponse({ - message: 'ROLE_NOT_FOUND', - statusCode: httpStatusCode.not_acceptable, - responseCode: 'CLIENT_ERROR', - }) + + if (!hasAdminRole) { + return failure('IDENTIFIER_OR_PASSWORD_INVALID', httpStatusCode.bad_request) + } + // 4) Active session limit enforcement (if configured) + if (process.env.ALLOWED_ACTIVE_SESSIONS != null) { + const activeSessionCount = await userSessionsService.activeUserSessionCounts(user.id) + if (activeSessionCount >= Number(process.env.ALLOWED_ACTIVE_SESSIONS)) { + return failure('ACTIVE_SESSION_LIMIT_EXCEEDED', httpStatusCode.not_acceptable) + } } - // create user session entry and add session_id to token data + // 6) Create user session const userSessionDetails = await userSessionsService.createUserSession( - user.id, // userid - '', // refresh token - '', // Access token - deviceInformation + user.id, + '', // refresh token placeholder + '', // access token placeholder + deviceInformation, + user.tenant_code ) - /** - * Based on user organisation id get user org parent Id value - * If parent org id is present then set it to tenant of user - * if not then set user organisation id to tenant - */ - - // let tenantDetails = await organizationQueries.findOne( - // { id: user.organization_id }, - // { attributes: ['parent_id'] } - // ) + // 7) Token payload enrichment - follow same shape as user/login + // Ensure organizations exist; if not, create a default org object from env - // const tenant_id = - // tenantDetails && tenantDetails.parent_id !== null ? tenantDetails.parent_id : user.organization_id + user = UserTransformDTO.transform(user) const tokenDetail = { data: { id: user.id, name: user.name, session_id: userSessionDetails.result.id, - organizations: [ - { - id: process.env.DEFAULT_ORG_ID, - roles: roles, - }, - ], - tenant_code: process.env.DEFAULT_TENANT_CODE, + organization_ids: user.organizations.map((o) => String(o.id)), + organization_codes: user.organizations.map((o) => String(o.code)), + organizations: user.organizations, + tenant_code: user.tenant_code, }, } - user.user_roles = roles - + // 8) Generate tokens (same helper as user/login) const accessToken = utils.generateToken( tokenDetail, process.env.ACCESS_TOKEN_SECRET, @@ -340,20 +382,23 @@ module.exports = class AdminHelper { common.refreshTokenExpiry ) - /** - * This function call will do below things - * 1: create redis entry for the session - * 2: update user-session with token and refresh_token - */ + // 9) Remove sensitive fields and post-process user image + delete user.password + if (user && user.image) { + user.image = await utils.getDownloadableUrl(user.image) + } + + // 10) Return original identifier and result + user.identifier = identifier + const result = { access_token: accessToken, refresh_token: refreshToken, user } + + // 11) Update session and set Redis data (same flow as user/login) await userSessionsService.updateUserSessionAndsetRedisData( userSessionDetails.result.id, accessToken, refreshToken ) - delete user.password - const result = { access_token: accessToken, refresh_token: refreshToken, user } - return responses.successResponse({ statusCode: httpStatusCode.ok, message: 'LOGGED_IN_SUCCESSFULLY', From 5d6117e3c16d70d2e6afe68f7d6eada41cba8743 Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Tue, 12 Aug 2025 20:14:59 +0530 Subject: [PATCH 52/55] fix: issue with createUserSession --- src/services/account.js | 4 ---- src/services/organization.js | 2 ++ src/services/user-sessions.js | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/services/account.js b/src/services/account.js index 984ae1fe0..180ae1d10 100644 --- a/src/services/account.js +++ b/src/services/account.js @@ -560,10 +560,6 @@ module.exports = class AccountHelper { } const result = { access_token: accessToken, refresh_token: refreshToken, user } - const templateData = await notificationTemplateQueries.findOneEmailTemplate( - process.env.REGISTRATION_EMAIL_TEMPLATE_CODE, - user.organization_id - ) if (plaintextEmailId) { notificationUtils.sendEmailNotification({ diff --git a/src/services/organization.js b/src/services/organization.js index 377dc0ecf..01aa26790 100644 --- a/src/services/organization.js +++ b/src/services/organization.js @@ -56,6 +56,8 @@ module.exports = class OrganizationsHelper { domain: domain, organization_id: createdOrganization.id, created_by: loggedInUserId, + updated_by: loggedInUserId, + tenant_code: bodyData.tenant_code, } await orgDomainQueries.create(domainCreationData) }) diff --git a/src/services/user-sessions.js b/src/services/user-sessions.js index 49d865efa..3569e6f23 100644 --- a/src/services/user-sessions.js +++ b/src/services/user-sessions.js @@ -41,7 +41,7 @@ module.exports = class UserSessionsHelper { if (accessToken !== '') { userSessionDetails.token = accessToken } - if (accessToken !== '') { + if (refreshToken !== '') { userSessionDetails.refresh_token = refreshToken } From 4cfd65514b046c2a188b25466110ffa00a814626 Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Tue, 12 Aug 2025 20:21:53 +0530 Subject: [PATCH 53/55] feat: Remove all 'admin' roles from user.user_organizations --- src/services/account.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/services/account.js b/src/services/account.js index 984ae1fe0..8da41dcb0 100644 --- a/src/services/account.js +++ b/src/services/account.js @@ -753,8 +753,19 @@ module.exports = class AccountHelper { const tenant_id = tenantDetails && tenantDetails.parent_id !== null ? tenantDetails.parent_id : user.organization_id */ + + //Remove all 'admin' roles from user.user_organizations + if (user?.user_organizations?.length) { + user.user_organizations.forEach((org) => { + if (org.roles) { + org.roles = org.roles.filter((r) => r.role?.title?.toLowerCase() !== 'admin') + } + }) + } + // Transform user data user = UserTransformDTO.transform(user) + const tokenDetail = { data: { id: user.id, From c238455ae603aab113e478a6be266dcd89bc817b Mon Sep 17 00:00:00 2001 From: Nevil Mathew Date: Tue, 12 Aug 2025 23:10:31 +0530 Subject: [PATCH 54/55] removed: hardcoded admin value --- src/services/account.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/account.js b/src/services/account.js index 8da41dcb0..c33b9af7c 100644 --- a/src/services/account.js +++ b/src/services/account.js @@ -758,7 +758,7 @@ module.exports = class AccountHelper { if (user?.user_organizations?.length) { user.user_organizations.forEach((org) => { if (org.roles) { - org.roles = org.roles.filter((r) => r.role?.title?.toLowerCase() !== 'admin') + org.roles = org.roles.filter((r) => r.role?.title?.toLowerCase() !== common.ADMIN_ROLE) } }) } From 9303a85f8ebfcc6fe5381c977bd043739d943378 Mon Sep 17 00:00:00 2001 From: Rocky Date: Wed, 13 Aug 2025 15:53:52 +0530 Subject: [PATCH 55/55] change status code --- src/services/user.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/user.js b/src/services/user.js index aabb87ee3..9c431ae42 100644 --- a/src/services/user.js +++ b/src/services/user.js @@ -323,7 +323,7 @@ module.exports = class UserHelper { if (!user) { return responses.failureResponse({ message: 'USER_NOT_FOUND', - statusCode: httpStatusCode.unauthorized, + statusCode: httpStatusCode.not_found, responseCode: 'UNAUTHORIZED', }) }