Skip to content

Issue #000 fix: creating usres in bulk - TypeError: Cannot read prope…#868

Open
vaivk369 wants to merge 29 commits intoELEVATE-Project:developfrom
vaivk369:new-dev
Open

Issue #000 fix: creating usres in bulk - TypeError: Cannot read prope…#868
vaivk369 wants to merge 29 commits intoELEVATE-Project:developfrom
vaivk369:new-dev

Conversation

@vaivk369
Copy link

@vaivk369 vaivk369 commented Dec 28, 2025

…rties of undefined (reading 'outputFilePath')

Summary by CodeRabbit

  • New Features

    • Self-service account creation with optional immediate sign-in (tokens & session) and tenant/domain resolution.
    • Org-admin endpoint now supports role and profile updates (admin-enforced); profile updates skip required-field validation.
    • Dynamic CSV invite processing mapping extra fields into metadata.
    • Meta-based filtering support for user list/search.
  • Bug Fixes

    • Safety checks to avoid runtime errors when metadata or linked entities are missing.
    • Preserve metadata entries previously removed by cleanup logic.
  • Chores

    • CI/CD workflows added for dev and QA deployments.
    • Improved logging and per-user enrichment in list/search.

…rties of undefined (reading 'outputFilePath')
@coderabbitai
Copy link

coderabbitai bot commented Dec 28, 2025

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Added tenant-aware account creation with optional post-registration login/session/token generation and Redis persistence; enhanced user listing/search with meta/status filters and per-user metadata processing; made generics/utils and invite processing more defensive and dynamic; added two GitHub Actions workflows for build & deploy.

Changes

Cohort / File(s) Summary
Generics utils
src/generics/utils.js
Added guard when accessing findEntity.data_type; removed two deletions of entityTypeValue from responseBody.meta (comments left in place).
Tenant controller
src/controllers/v1/tenant.js
Added require('@services/account') and new accountCreate(req) controller method that delegates to account service with try/catch.
Account service
src/services/account.js
Extended create(..., registerWithLogin = true, req = {}); resolve tenant via domain or tenant-code header; accept roles from req.body.roles; optional post-create login flow that generates tokens, persists session/refresh token to Redis, and returns tokens; added metaFilters support and per-user metadata enrichment in list/search.
Database queries (users)
src/database/queries/users.js
searchUsersWithOrganization now accepts status and metaFilters; applies uppercase status filter and appends JSONB User.meta WHERE clauses for validated meta keys.
Helpers (user invite)
src/helpers/userInvite.js
Replaced hardcoded field deletions with dynamic loop over entityFields/alreadyProcessed; map external entity names to IDs (single or array) and consolidate processed values under row.meta, then delete originals.
Org admin / User update
src/services/org-admin.js, src/services/user.js
Org-admin endpoint enforces admin-only role updates, allows profile updates via internal user update with skipRequiredValidation; User.update signature adds skipRequiredValidation param and propagates it to validation.
Validators
src/validators/v1/account.js, src/validators/v1/tenant.js
Replaced phone_code checks with a custom validator that requires phone_code when phone present; added tenant validator wrapper accountCreate delegating to account validator.
CI / Deployment workflows
.github/workflows/brac-dev-deployment.yaml, .github/workflows/brac-qa-deplyment.yaml
Added two GitHub Actions workflows to build/push Docker images to ECR and deploy to remote hosts (dev on develop, QA on tags) using secrets and remote deploy scripts.

Sequence Diagram(s)

sequenceDiagram
  participant Client as Client
  participant TenantDB as TenantDB
  participant AccountSvc as AccountService
  participant Auth as TokenService
  participant Redis as RedisStore

  Client->>AccountSvc: POST /tenant/accountCreate(body, deviceInfo, domain?, registerWithLogin?, req)
  alt domain provided
    AccountSvc->>TenantDB: resolve tenant by domain
    TenantDB-->>AccountSvc: tenantDetail / not found
  else domain absent
    AccountSvc->>TenantDB: resolve tenant by tenant-code header
    TenantDB-->>AccountSvc: tenantDetail / not found
  end
  AccountSvc->>AccountSvc: validate input, determine roles
  AccountSvc->>AccountSvc: create user record
  alt registerWithLogin == true
    AccountSvc->>Auth: generate access & refresh tokens
    Auth-->>AccountSvc: tokens
    AccountSvc->>Redis: persist session + refresh token
    Redis-->>AccountSvc: ack
    AccountSvc-->>Client: return user + tokens
  else
    AccountSvc-->>Client: return created user (no tokens)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

I’m a rabbit in the deploy glade,
I check nulls, map metas, and braid,
Tenants, tokens, sessions snug and small,
I hop through invites and answer the call,
Tiny paws, big changes — hip-hop-hurray! 🐇✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is vague and truncated, using a partial error message that doesn't clearly convey the main change or intent of the pull request. Replace the partial error message with a clear, concise description of the actual fix implemented (e.g., 'Fix bulk user creation with entity metadata handling' or similar based on the primary change).
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/generics/utils.js (2)

1022-1059: Note: Discrepancy between PR title and code change.

The PR title mentions "reading 'outputFilePath'" but the actual code change addresses accessing the data_type property on a potentially undefined findEntity. While the fix correctly prevents undefined property access errors, please verify that this change addresses the specific error mentioned in the PR title, or update the PR description to accurately reflect the fix.


1037-1055: Address fallback behavior when entity type is missing.

When findEntity is undefined (no matching entity in prunedEntities), the code falls through to the else block (lines 1049–1055). This processes metadata without entity type information and returns incomplete results like {name: undefined, id: undefined, externalId: undefined} if the corresponding data isn't found in feederData. The returned data is merged without validation into event bodies and change tracking structures across multiple call sites (account.js, user.js, userInvite.js), which could cause downstream issues. Either add validation to reject incomplete metadata or document why partial results are acceptable.

🧹 Nitpick comments (1)
src/generics/utils.js (1)

1041-1041: Defensive null-check correctly prevents the runtime error.

The added check for findEntity existence before accessing its data_type property prevents the "Cannot read properties of undefined" error when no matching entity is found in the prunedEntities array.

Minor note: The optional chaining findEntity?.data_type is redundant after the explicit findEntity && check, though it's harmless. Consider simplifying to:

🔎 Suggested simplification
-if (findEntity && (findEntity?.data_type == 'ARRAY' || findEntity?.data_type == 'ARRAY[STRING]')) {
+if (findEntity && (findEntity.data_type == 'ARRAY' || findEntity.data_type == 'ARRAY[STRING]')) {
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a2025dd and 2191628.

⛔ Files ignored due to path filters (1)
  • src/api-doc/bulkUser.md is excluded by !src/api-doc/**, !**/*.md
📒 Files selected for processing (1)
  • src/generics/utils.js
🧰 Additional context used
🧬 Code graph analysis (1)
src/generics/utils.js (1)
src/helpers/userInvite.js (1)
  • findEntity (579-579)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/services/account.js (1)

322-334: Type safety issue with req.body.roles.

If req.body.roles is truthy but not a string (e.g., already an array), calling .split(',') will throw a TypeError. Consider adding type validation.

Proposed fix
 			role = await roleQueries.findAll(
 				{
 					title: {
-						[Op.in]: req.body.roles ? req.body.roles.split(',') : process.env.DEFAULT_ROLE.split(','),
+						[Op.in]: req.body.roles
+							? (Array.isArray(req.body.roles) ? req.body.roles : req.body.roles.split(','))
+							: process.env.DEFAULT_ROLE.split(','),
 					},
 					tenant_code: tenantDetail.code,
 				},
🤖 Fix all issues with AI agents
In `@src/controllers/v1/tenant.js`:
- Around line 162-168: The JSDoc for the "Create account user" endpoint has an
incorrect `@name` value; change the `@name` annotation from "bulkUserCreate" to
"accountCreate" in the JSDoc block above the accountCreate handler so the docs
match the actual method/intent (look for the comment block containing "Create
account user" and update its `@name`).

In `@src/services/account.js`:
- Around line 1946-1969: The code reads user.user_organizations[0] without
checking existence which can throw if a user has no organizations; update the
loop in the section using user_organizations to first verify
user.user_organizations && user.user_organizations.length > 0, and if absent
either skip processing that user (continue) or fall back to
defaultOrganizationCode for userOrg and handle the missing id when calling
removeDefaultOrgEntityTypes (e.g., pass null/undefined or guard that call).
Ensure the branches adjust the values passed to findUserEntityTypesAndEntities
and removeDefaultOrgEntityTypes and still call utils.processDbResponse only when
safe.
- Around line 384-385: Remove the two debug console.log statements that print
validationData and userModel; either delete them entirely or replace them with
structured logging using the project's logger (e.g., logger.debug or
processLogger.debug) and ensure sensitive fields from validationData/userModel
are omitted or redacted before logging; update occurrences of
console.log('validationData', validationData) and console.log('userModel',
userModel) in src/services/account.js accordingly.
- Line 42: Remove the unused import "use" from i18next in this module: delete
the line that destructures use from the require call (the symbol "use" in the
const { use } = require('i18next') statement) so the file no longer imports an
unused binding; if the require call is otherwise unnecessary, replace or
simplify it accordingly (e.g., remove the entire require if nothing else from
i18next is used).
- Around line 70-84: tenantDomain can be undefined if neither domain nor
req.headers[common.TENANT_CODE_HEADER] are present, causing a TypeError when
accessing tenantDomain.tenant_code in the tenantQueries.findOne call; update the
logic in the block using tenantDomain and tenantId (the code around
tenantDomain, tenantId, TENANT_CODE_HEADER and the notFoundResponse call) to
explicitly handle the missing-case by returning a notFoundResponse (or similar
error) when both domain and the tenant header are absent, or ensure tenantDomain
is initialized (e.g., set tenantDomain = { tenant_code: tenantId }) before
calling tenantQueries.findOne so tenantQueries.findOne always gets a valid
tenant_code.
🧹 Nitpick comments (3)
src/services/account.js (3)

115-116: Remove empty else block.

The empty else block adds no value and should be removed for code cleanliness.

Proposed fix
 			if (!domainDetails) {
 				return responses.failureResponse({
 					message: 'INVALID_ORG_registration_code',
 					statusCode: httpStatusCode.bad_request,
 					responseCode: 'CLIENT_ERROR',
 				})
 			}
-		} else {
 		}

506-512: Remove commented-out code.

Commented-out code should be removed. If this logic is needed in the future, it can be retrieved from version control.

Proposed fix
-		/* 			let tenantDetails = await organizationQueries.findOne(
-			{ id: user.organization_id },
-			{ attributes: ['related_orgs'] }
-		)
-
-		const tenant_id =
-			tenantDetails && tenantDetails.parent_id !== null ? tenantDetails.parent_id : user.organization_id */

1730-1730: Good defensive error handling.

Swallowing Redis deletion errors with .catch((err) => {}) is appropriate here since this is a fire-and-forget cache invalidation that shouldn't fail the main operation. However, consider logging the error for observability.

-		utilsHelper.redisDel(redisUserKey).catch((err) => {})
+		utilsHelper.redisDel(redisUserKey).catch((err) => {
+			console.error('Failed to delete Redis cache:', err)
+		})

Comment on lines +162 to +168
/**
* Create account user
* @method POST
* @name bulkUserCreate
* @param {Object} req -request data.
* @returns {JSON} - success or error message
*/
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix JSDoc @name annotation.

The @name should be accountCreate, not bulkUserCreate.

Proposed fix
 	/**
 	 * Create account user
 	 * `@method` POST
-	 * `@name` bulkUserCreate
+	 * `@name` accountCreate
 	 * `@param` {Object} req -request data.
 	 * `@returns` {JSON} - success or error message
 	 */
🤖 Prompt for AI Agents
In `@src/controllers/v1/tenant.js` around lines 162 - 168, The JSDoc for the
"Create account user" endpoint has an incorrect `@name` value; change the `@name`
annotation from "bulkUserCreate" to "accountCreate" in the JSDoc block above the
accountCreate handler so the docs match the actual method/intent (look for the
comment block containing "Create account user" and update its `@name`).

const notificationUtils = require('@utils/notification')
const userHelper = require('@helpers/userHelper')
const { broadcastEvent } = require('@helpers/eventBroadcasterMain')
const { use } = require('i18next')
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove unused import.

The use function is imported from i18next but never used in this file.

Proposed fix
-const { use } = require('i18next')
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { use } = require('i18next')
🤖 Prompt for AI Agents
In `@src/services/account.js` at line 42, Remove the unused import "use" from
i18next in this module: delete the line that destructures use from the require
call (the symbol "use" in the const { use } = require('i18next') statement) so
the file no longer imports an unused binding; if the require call is otherwise
unnecessary, replace or simplify it accordingly (e.g., remove the entire require
if nothing else from i18next is used).

Comment on lines +70 to 84
let tenantDomain

const tenantDomain = await tenantDomainQueries.findOne({ domain })

if (!tenantDomain) {
return notFoundResponse('TENANT_DOMAIN_NOT_FOUND_PING_ADMIN')
if (domain) {
tenantDomain = await tenantDomainQueries.findOne({ domain })

if (!tenantDomain) {
return notFoundResponse('TENANT_DOMAIN_NOT_FOUND_PING_ADMIN')
}
} else if (req.headers?.[common.TENANT_CODE_HEADER]) {
tenantId = req.headers?.[common.TENANT_CODE_HEADER]
tenantDomain = { tenant_code: tenantId }
}

const tenantDetail = await tenantQueries.findOne({
code: tenantDomain.tenant_code,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Potential TypeError when both domain and tenant header are missing.

If domain is falsy and req.headers?.[common.TENANT_CODE_HEADER] is also undefined, tenantDomain remains undefined. Accessing tenantDomain.tenant_code on line 84 will throw a TypeError.

Proposed fix
 		if (domain) {
 			tenantDomain = await tenantDomainQueries.findOne({ domain })
 
 			if (!tenantDomain) {
 				return notFoundResponse('TENANT_DOMAIN_NOT_FOUND_PING_ADMIN')
 			}
 		} else if (req.headers?.[common.TENANT_CODE_HEADER]) {
 			tenantId = req.headers?.[common.TENANT_CODE_HEADER]
 			tenantDomain = { tenant_code: tenantId }
+		} else {
+			return notFoundResponse('TENANT_DOMAIN_OR_CODE_REQUIRED')
 		}
🤖 Prompt for AI Agents
In `@src/services/account.js` around lines 70 - 84, tenantDomain can be undefined
if neither domain nor req.headers[common.TENANT_CODE_HEADER] are present,
causing a TypeError when accessing tenantDomain.tenant_code in the
tenantQueries.findOne call; update the logic in the block using tenantDomain and
tenantId (the code around tenantDomain, tenantId, TENANT_CODE_HEADER and the
notFoundResponse call) to explicitly handle the missing-case by returning a
notFoundResponse (or similar error) when both domain and the tenant header are
absent, or ensure tenantDomain is initialized (e.g., set tenantDomain = {
tenant_code: tenantId }) before calling tenantQueries.findOne so
tenantQueries.findOne always gets a valid tenant_code.

Comment on lines +384 to +385
console.log('validationData', validationData)
console.log('userModel', userModel)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove debug console.log statements.

These debug statements should be removed before merging to production or converted to structured logging with appropriate log levels.

Proposed fix
-		console.log('validationData', validationData)
-		console.log('userModel', userModel)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
console.log('validationData', validationData)
console.log('userModel', userModel)
🤖 Prompt for AI Agents
In `@src/services/account.js` around lines 384 - 385, Remove the two debug
console.log statements that print validationData and userModel; either delete
them entirely or replace them with structured logging using the project's logger
(e.g., logger.debug or processLogger.debug) and ensure sensitive fields from
validationData/userModel are omitted or redacted before logging; update
occurrences of console.log('validationData', validationData) and
console.log('userModel', userModel) in src/services/account.js accordingly.

Comment on lines +1946 to +1969
const defaultOrganizationCode = process.env.DEFAULT_ORGANISATION_CODE

for (let i = 0; i < users.data.length; i++) {
let user = users.data[i]

// Convert Sequelize instance to plain object
if (user.toJSON) {
user = user.toJSON()
users.data[i] = user
}

let userOrg = user.user_organizations[0].organization_code
let validationData = await entityTypeQueries.findUserEntityTypesAndEntities({
status: 'ACTIVE',
organization_code: {
[Op.in]: [userOrg, defaultOrganizationCode],
},
tenant_code: params.query.tenant_code,
model_names: { [Op.contains]: [await userQueries.getModelName()] },
})
const prunedEntities = removeDefaultOrgEntityTypes(validationData, user.user_organizations[0].id)
const processedUser = await utils.processDbResponse(user, prunedEntities)
Object.assign(user, processedUser)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing bounds check on user_organizations array.

Accessing user.user_organizations[0] without checking if the array exists and has elements could cause a runtime error if a user has no organizations.

Proposed fix
 			for (let i = 0; i < users.data.length; i++) {
 				let user = users.data[i]
 
 				// Convert Sequelize instance to plain object
 				if (user.toJSON) {
 					user = user.toJSON()
 					users.data[i] = user
 				}
 
+				if (!user.user_organizations?.length) {
+					continue
+				}
+
 				let userOrg = user.user_organizations[0].organization_code
🤖 Prompt for AI Agents
In `@src/services/account.js` around lines 1946 - 1969, The code reads
user.user_organizations[0] without checking existence which can throw if a user
has no organizations; update the loop in the section using user_organizations to
first verify user.user_organizations && user.user_organizations.length > 0, and
if absent either skip processing that user (continue) or fall back to
defaultOrganizationCode for userOrg and handle the missing id when calling
removeDefaultOrgEntityTypes (e.g., pass null/undefined or guard that call).
Ensure the branches adjust the values passed to findUserEntityTypesAndEntities
and removeDefaultOrgEntityTypes and still call utils.processDbResponse only when
safe.

AbhilashKD and others added 5 commits January 21, 2026 10:47
Added AWS secret access key exports for deployment.
Updated environment variable export syntax for AWS credentials.
Issue #000 fix: serach Users with one or more meta params
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/services/account.js`:
- Around line 324-327: Replace the direct use of req.body.roles in the query by
validating/sanitizing it: parse req.body.roles, intersect it with an explicit
whitelist of allowed self-assignable roles (define a constant like
SELF_ASSIGNABLE_ROLES), and only use the resulting safe list for the title
[Op.in] clause; if no valid roles remain, fall back to
process.env.DEFAULT_ROLE.split(','). Additionally, enforce that role assignment
from the request is only allowed for internal/admin contexts (check the existing
auth/admin flag or throw) before honoring req.body.roles, and ensure tenant_code
still uses tenantDetail.code.
🧹 Nitpick comments (3)
src/database/queries/users.js (1)

431-443: JSONB meta filtering may benefit from an index for performance.

The implementation correctly validates the key with a regex to prevent injection, and Sequelize.where parameterizes the value. However, queries using meta->>'key' won't leverage indexes unless a functional GIN index exists on the meta column.

Consider adding an index if meta filtering will be used frequently:

CREATE INDEX idx_users_meta ON users USING GIN (meta);

Also, a minor improvement: the empty Op.and array initialization could be moved inside the if block to avoid creating it when unused.

Suggested improvement
 		// Filter by meta fields (generic - supports any meta field)
 		if (metaFilters && typeof metaFilters === 'object' && Object.keys(metaFilters).length > 0) {
-			userWhere[Op.and] = userWhere[Op.and] || []
+			const metaConditions = []
 			for (const [key, value] of Object.entries(metaFilters)) {
 				if (value !== null && value !== undefined && value !== '') {
 					// Use Sequelize.where with JSONB operator for safe parameterized queries
 					// The key is validated to be alphanumeric/underscore only to prevent SQL injection
 					if (/^[a-zA-Z0-9_]+$/.test(key)) {
-						userWhere[Op.and].push(Sequelize.where(Sequelize.literal(`"User".meta->>'${key}'`), value))
+						metaConditions.push(Sequelize.where(Sequelize.literal(`"User".meta->>'${key}'`), value))
 					}
 				}
 			}
+			if (metaConditions.length > 0) {
+				userWhere[Op.and] = (userWhere[Op.and] || []).concat(metaConditions)
+			}
 		}
src/services/account.js (2)

506-512: Remove or address commented-out code.

This commented block for tenant details lookup should either be removed if no longer needed, or uncommented and integrated if the functionality is required. Leaving commented code in production adds noise.


1730-1730: Empty catch block silently swallows Redis errors.

While fire-and-forget is acceptable here, the empty catch makes debugging difficult. Consider logging at debug level.

Suggested improvement
-		utilsHelper.redisDel(redisUserKey).catch((err) => {})
+		utilsHelper.redisDel(redisUserKey).catch((err) => {
+			console.debug('Redis cache clear failed for user:', redisUserKey, err.message)
+		})

Comment on lines 324 to 327
title: {
[Op.in]: process.env.DEFAULT_ROLE.split(','),
[Op.in]: req.body.roles ? req.body.roles.split(',') : process.env.DEFAULT_ROLE.split(','),
},
tenant_code: tenantDetail.code,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Potential privilege escalation: roles accepted directly from request body.

Allowing req.body.roles to directly control role assignment without validation could enable users to assign themselves privileged roles (e.g., org_admin, admin). This bypasses the invitation-based role assignment flow.

Consider:

  1. Validating that the requested roles are within an allowed set for self-registration
  2. Restricting this feature to internal/admin API calls only
  3. Adding explicit role whitelist validation
Suggested safeguard
+				// Only allow specific roles for self-registration
+				const allowedSelfRegistrationRoles = (process.env.ALLOWED_SELF_REGISTRATION_ROLES || process.env.DEFAULT_ROLE).split(',')
+				const requestedRoles = req.body.roles ? req.body.roles.split(',') : process.env.DEFAULT_ROLE.split(',')
+				const validRoles = requestedRoles.filter(role => allowedSelfRegistrationRoles.includes(role.trim()))
+				
 				role = await roleQueries.findAll(
 					{
 						title: {
-							[Op.in]: req.body.roles ? req.body.roles.split(',') : process.env.DEFAULT_ROLE.split(','),
+							[Op.in]: validRoles.length > 0 ? validRoles : process.env.DEFAULT_ROLE.split(','),
 						},
🤖 Prompt for AI Agents
In `@src/services/account.js` around lines 324 - 327, Replace the direct use of
req.body.roles in the query by validating/sanitizing it: parse req.body.roles,
intersect it with an explicit whitelist of allowed self-assignable roles (define
a constant like SELF_ASSIGNABLE_ROLES), and only use the resulting safe list for
the title [Op.in] clause; if no valid roles remain, fall back to
process.env.DEFAULT_ROLE.split(','). Additionally, enforce that role assignment
from the request is only allowed for internal/admin contexts (check the existing
auth/admin flag or throw) before honoring req.body.roles, and ensure tenant_code
still uses tenantDetail.code.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In @.github/workflows/brac-dev-deployment.yaml:
- Around line 76-79: The heredoc block used to write .env is indented so leading
whitespace will be injected into every line of ${{ secrets.DEV_ENV_BRAC }}; fix
by unindenting the heredoc (place the cat << 'EOF' > .env and the terminating
EOF at column 0) or switch to a strip-leading-tabs form (use cat <<-'EOF' > .env
and ensure any indentation is tabs) so that the generated .env contains exact
KEY=VALUE lines; update the lines around the symbols "cat << 'EOF' > .env", "${{
secrets.DEV_ENV_BRAC }}" and the terminating "EOF" accordingly.

In @.github/workflows/brac-qa-deplyment.yaml:
- Line 1: The workflow filename contains a typo: rename the file from
brac-qa-deplyment.yaml to brac-qa-deployment.yaml and update any references to
the old filename (e.g., in workflow dispatch calls, docs, README badges, or
other CI config) so tooling and triggers point to the new name; ensure the
workflow's internal "name: Tag Build & Deploy User Service (BRAC)" remains
unchanged and commit the renamed file so GitHub Actions recognizes the corrected
workflow.
- Around line 65-66: The workflow currently exports AWS_ACCESS_KEY_ID and
AWS_SECRET_ACCESS_KEY as plain-text environment variables in the remote shell;
replace this by avoiding direct export — either use an IAM instance role on the
target host or configure credentials via the GitHub Actions-provided steps
(e.g., aws-actions/configure-aws-credentials) instead of exporting secrets into
the shell, or at minimum wrap the export with history suppression (run "set +o
history" before exporting and "set -o history" after) and immediately unset the
variables after use; look for the exported symbols AWS_ACCESS_KEY_ID and
AWS_SECRET_ACCESS_KEY in the workflow and remove direct export usage
accordingly.

In `@src/database/queries/users.js`:
- Around line 413-415: Guard against non-string status values before calling
toUpperCase on status in the users query: check the type of the status variable
(and reject arrays/null/objects) and only call status.toUpperCase() when typeof
status === 'string' (otherwise either coerce safely with
String(status).toUpperCase() or skip/normalize the value). Update the logic
around userWhere.status in the users query (the block that currently does
userWhere.status = status.toUpperCase()) to perform this type
check/normalization so it cannot throw when status is an array, number, or other
non-string.

In `@src/services/account.js`:
- Around line 485-550: The code overwrites result = { access_token...,
refresh_token... } when registerWithLogin is true, discarding the previously set
result.user and causing later calls (utils.processDbResponse(result.user,
prunedEntities)) to receive undefined; fix by preserving the user object when
adding tokens—e.g., instead of replacing result, merge tokens into the existing
result (result = { ...result, access_token: accessToken, refresh_token:
refreshToken } or assign result.access_token = accessToken; result.refresh_token
= refreshToken) so result.user remains defined for subsequent calls to
utils.processDbResponse and updates via
userSessionsService.updateUserSessionAndsetRedisData remain correct.
- Around line 1970-1991: The loop over users calls
entityTypeQueries.findUserEntityTypesAndEntities for each user (see users.data
loop and user.user_organizations[0].organization_code), causing an N+1 query;
instead collect unique organization_code values from users.data (and include
defaultOrganizationCode), call findUserEntityTypesAndEntities once (or once per
batch) to fetch validationData for all orgs (using Op.in on organization_code
and tenant_code and model_names), build a map keyed by organization_code (or
user_organization id) to the returned entity types, then inside the loop reuse
that map and call removeDefaultOrgEntityTypes and utils.processDbResponse with
the pre-fetched prunedEntities (and optionally memoize per org id) to eliminate
per-user DB calls.
🧹 Nitpick comments (7)
.github/workflows/brac-dev-deployment.yaml (2)

54-56: Pin appleboy/ssh-action to a specific version or commit SHA instead of @master.

Using @master exposes the workflow to supply-chain attacks if the upstream repo is compromised — any new commit is automatically trusted. Pin to a release tag (e.g., @v1.0.3) or a full commit SHA.


65-68: Commented-out AWS credential exports leak intent; active exports may be unnecessary.

Lines 67-68 are commented out but still expose secret names. If ECR login on the remote host requires these credentials, uncomment them; otherwise remove the comments entirely to avoid confusion. Also note that export AWS_REGION and export AWS_ACCOUNT_ID on lines 65-66 expose secrets in the remote shell environment — ensure the remote server's shell history and process table are appropriately secured.

.github/workflows/brac-qa-deplyment.yaml (1)

55-56: Pin appleboy/ssh-action to a specific version or commit SHA instead of @master.

Same concern as the dev workflow — using @master is a supply-chain risk.

src/helpers/userInvite.js (2)

341-388: Inconsistent fallback values between hardcoded and dynamic meta field processing.

The hardcoded block (lines 282-339) uses mixed fallback values: null for some fields, '' (empty string) for others (e.g., professional_role uses '', block uses null). The new dynamic block uses null for non-array types (line 379) and .filter(Boolean) for arrays (line 374). This inconsistency may cause subtle differences in validation behavior downstream (e.g., null vs '' truthiness checks).

Consider aligning the fallback values, or better yet, consolidating the hardcoded block into the dynamic loop to eliminate the duplication entirely.


352-382: validationData.find() is called per field per row — consider pre-building a lookup map.

For large CSV files with many entity fields, calling .find() on validationData for every field of every row is O(rows × fields × validationData.length). A simple Map built once before the row loop would make lookups O(1).

Suggested improvement (outside the changed lines)
// Build once before the Promise.all loop (e.g., after line 262)
const entityDefMap = new Map(validationData.map((e) => [e.value, e]))

Then inside the loop:

-const entityDef = validationData.find((e) => e.value === field)
+const entityDef = entityDefMap.get(field)
src/database/queries/users.js (1)

436-448: JSONB meta field queries may not leverage indexes — consider adding expression indexes.

The "User".meta->>'key' queries generated here will perform a full table scan on the meta column unless there are GIN indexes or expression-based indexes defined. For frequently filtered meta fields, this could be a performance bottleneck on large user tables.

Consider adding a GIN index on the meta column if one doesn't already exist:

CREATE INDEX idx_users_meta ON users USING GIN (meta);

Or for specific high-cardinality fields, expression indexes:

CREATE INDEX idx_users_meta_province ON users ((meta->>'province'));

As per coding guidelines, "Review database queries for performance… and ensure indexes can be used."

src/services/account.js (1)

1907-1922: Silently ignoring JSON parse errors on meta filter could hide malformed input.

Line 1915-1917 catches the JSON parse error and silently ignores it. Consider logging a warning or returning a validation error to the caller so they know their meta filter was malformed rather than silently returning unfiltered results.

Comment on lines +76 to +79
# Write env safely (MULTILINE SAFE)
cat << 'EOF' > .env
${{ secrets.DEV_ENV_BRAC }}
EOF
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Heredoc indentation will inject leading whitespace into .env values.

Because the cat << 'EOF' block is indented (line 78), every line of ${{ secrets.DEV_ENV_BRAC }} will be prefixed with leading spaces/tabs. This can break environment variable parsing (e.g., KEY=value instead of KEY=value).

Proposed fix

Remove indentation or use <<- with tabs:

-            cat << 'EOF' > .env
-            ${{ secrets.DEV_ENV_BRAC }}
-            EOF
+            cat << 'EOF' > .env
+${{ secrets.DEV_ENV_BRAC }}
+EOF
🤖 Prompt for AI Agents
In @.github/workflows/brac-dev-deployment.yaml around lines 76 - 79, The heredoc
block used to write .env is indented so leading whitespace will be injected into
every line of ${{ secrets.DEV_ENV_BRAC }}; fix by unindenting the heredoc (place
the cat << 'EOF' > .env and the terminating EOF at column 0) or switch to a
strip-leading-tabs form (use cat <<-'EOF' > .env and ensure any indentation is
tabs) so that the generated .env contains exact KEY=VALUE lines; update the
lines around the symbols "cat << 'EOF' > .env", "${{ secrets.DEV_ENV_BRAC }}"
and the terminating "EOF" accordingly.

@@ -0,0 +1,87 @@
name: Tag Build & Deploy User Service (BRAC)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Filename typo: brac-qa-deplyment.yaml should be brac-qa-deployment.yaml.

🤖 Prompt for AI Agents
In @.github/workflows/brac-qa-deplyment.yaml at line 1, The workflow filename
contains a typo: rename the file from brac-qa-deplyment.yaml to
brac-qa-deployment.yaml and update any references to the old filename (e.g., in
workflow dispatch calls, docs, README badges, or other CI config) so tooling and
triggers point to the new name; ensure the workflow's internal "name: Tag Build
& Deploy User Service (BRAC)" remains unchanged and commit the renamed file so
GitHub Actions recognizes the corrected workflow.

Comment on lines +65 to +66
export AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_ID }}
export AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

AWS credentials exported in plain text on remote host.

AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are exported directly into the remote shell session. If the remote host has shell history enabled or process listing is accessible, these credentials could be leaked. Consider using an IAM instance role on the EC2 host instead, or at minimum ensure set +o history is used.

🤖 Prompt for AI Agents
In @.github/workflows/brac-qa-deplyment.yaml around lines 65 - 66, The workflow
currently exports AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY as plain-text
environment variables in the remote shell; replace this by avoiding direct
export — either use an IAM instance role on the target host or configure
credentials via the GitHub Actions-provided steps (e.g.,
aws-actions/configure-aws-credentials) instead of exporting secrets into the
shell, or at minimum wrap the export with history suppression (run "set +o
history" before exporting and "set -o history" after) and immediately unset the
variables after use; look for the exported symbols AWS_ACCESS_KEY_ID and
AWS_SECRET_ACCESS_KEY in the workflow and remove direct export usage
accordingly.

Comment on lines +413 to +415
if (status) {
userWhere.status = status.toUpperCase()
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

status.toUpperCase() will throw if status is not a string (e.g., passed as array or number).

While the caller currently passes params.query.status || false, if this query function is reused with unexpected input, it could throw. A quick guard would help:

Proposed fix
 		if (status) {
-			userWhere.status = status.toUpperCase()
+			userWhere.status = String(status).toUpperCase()
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (status) {
userWhere.status = status.toUpperCase()
}
if (status) {
userWhere.status = String(status).toUpperCase()
}
🤖 Prompt for AI Agents
In `@src/database/queries/users.js` around lines 413 - 415, Guard against
non-string status values before calling toUpperCase on status in the users
query: check the type of the status variable (and reject arrays/null/objects)
and only call status.toUpperCase() when typeof status === 'string' (otherwise
either coerce safely with String(status).toUpperCase() or skip/normalize the
value). Update the logic around userWhere.status in the users query (the block
that currently does userWhere.status = status.toUpperCase()) to perform this
type check/normalization so it cannot throw when status is an array, number, or
other non-string.

Comment on lines +485 to 550
let tokenDetail = {}
let result = { user }
if (registerWithLogin) {
/**
* create user session entry and add session_id to token data
* Entry should be created first, the session_id has to be added to token creation data
*/
const userSessionDetails = await userSessionsService.createUserSession(
user.id, // userid
'', // refresh token
'', // Access token
deviceInfo,
user.tenant_code
)

/**
* create user session entry and add session_id to token data
* Entry should be created first, the session_id has to be added to token creation data
*/
const userSessionDetails = await userSessionsService.createUserSession(
user.id, // userid
'', // refresh token
'', // Access token
deviceInfo,
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
*/

/**
* 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: ['related_orgs'] }
)

/* let tenantDetails = await organizationQueries.findOne(
{ id: user.organization_id },
{ attributes: ['related_orgs'] }
)
const tenant_id =
tenantDetails && tenantDetails.parent_id !== null ? tenantDetails.parent_id : user.organization_id */

const tenant_id =
tenantDetails && tenantDetails.parent_id !== null ? tenantDetails.parent_id : user.organization_id */
tokenDetail = {
data: {
id: user.id,
name: user.name,
session_id: userSessionDetails.result.id,
organization_ids: user.organizations.map((org) => String(org.id)), // Convert to string
organization_codes: user.organizations.map((org) => String(org.code)), // Convert to string
tenant_code: tenantDetail.code,
organizations: user.organizations,
},
}

const tokenDetail = {
data: {
id: user.id,
name: user.name,
session_id: userSessionDetails.result.id,
organization_ids: user.organizations.map((org) => String(org.id)), // Convert to string
organization_codes: user.organizations.map((org) => String(org.code)), // Convert to string
tenant_code: tenantDetail.code,
organizations: user.organizations,
},
const accessToken = utilsHelper.generateToken(
tokenDetail,
process.env.ACCESS_TOKEN_SECRET,
common.accessTokenExpiry
)

const refreshToken = utilsHelper.generateToken(
tokenDetail,
process.env.REFRESH_TOKEN_SECRET,
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
*/
await userSessionsService.updateUserSessionAndsetRedisData(
userSessionDetails.result.id,
accessToken,
refreshToken
)

result = { access_token: accessToken, refresh_token: refreshToken }
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical bug: result.user is undefined when registerWithLogin is true.

On line 486, result is initialized as { user }. On line 549, when registerWithLogin is true, result is overwritten to { access_token, refresh_token } — discarding the user property. Then line 619 attempts result.user = await utils.processDbResponse(result.user, prunedEntities), passing undefined to processDbResponse, and lines 620-622 attempt to set properties on the (likely undefined/null) return value.

Proposed fix
-			result = { access_token: accessToken, refresh_token: refreshToken }
+			result = { access_token: accessToken, refresh_token: refreshToken, user }

Also applies to: 619-622

🤖 Prompt for AI Agents
In `@src/services/account.js` around lines 485 - 550, The code overwrites result =
{ access_token..., refresh_token... } when registerWithLogin is true, discarding
the previously set result.user and causing later calls
(utils.processDbResponse(result.user, prunedEntities)) to receive undefined; fix
by preserving the user object when adding tokens—e.g., instead of replacing
result, merge tokens into the existing result (result = { ...result,
access_token: accessToken, refresh_token: refreshToken } or assign
result.access_token = accessToken; result.refresh_token = refreshToken) so
result.user remains defined for subsequent calls to utils.processDbResponse and
updates via userSessionsService.updateUserSessionAndsetRedisData remain correct.

Comment on lines +1970 to +1991
for (let i = 0; i < users.data.length; i++) {
let user = users.data[i]

// Convert Sequelize instance to plain object
if (user.toJSON) {
user = user.toJSON()
users.data[i] = user
}

let userOrg = user.user_organizations[0].organization_code
let validationData = await entityTypeQueries.findUserEntityTypesAndEntities({
status: 'ACTIVE',
organization_code: {
[Op.in]: [userOrg, defaultOrganizationCode],
},
tenant_code: params.query.tenant_code,
model_names: { [Op.contains]: [await userQueries.getModelName()] },
})
const prunedEntities = removeDefaultOrgEntityTypes(validationData, user.user_organizations[0].id)
const processedUser = await utils.processDbResponse(user, prunedEntities)
Object.assign(user, processedUser)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

N+1 query: findUserEntityTypesAndEntities is called inside a loop for each user.

This fires a separate DB query per user to fetch entity types and validation data. For large result sets, this degrades performance significantly. Consider grouping users by organization and batching the validation data lookups.

As per coding guidelines, "This is core business logic. Please check for correctness, efficiency, and potential edge cases."

🤖 Prompt for AI Agents
In `@src/services/account.js` around lines 1970 - 1991, The loop over users calls
entityTypeQueries.findUserEntityTypesAndEntities for each user (see users.data
loop and user.user_organizations[0].organization_code), causing an N+1 query;
instead collect unique organization_code values from users.data (and include
defaultOrganizationCode), call findUserEntityTypesAndEntities once (or once per
batch) to fetch validationData for all orgs (using Op.in on organization_code
and tenant_code and model_names), build a map keyed by organization_code (or
user_organization id) to the returned entity types, then inside the loop reuse
that map and call removeDefaultOrgEntityTypes and utils.processDbResponse with
the pre-fetched prunedEntities (and optionally memoize per org id) to eliminate
per-user DB calls.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
src/services/org-admin.js (1)

595-602: Remove the commented-out legacy check block.

Keeping large commented logic in the active method makes this path harder to audit and maintain.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/org-admin.js` around lines 595 - 602, Remove the commented-out
legacy user existence check in the org admin flow: delete the commented block
that declares checkUser and calls userQueries.findOne({ id: userId }) and the
subsequent CLIENT_ERROR response; this unused commented logic should be removed
from the method in src/services/org-admin.js to keep the codebase clean and
maintainable (look for the commented checkUser / userQueries.findOne and the
USER_NOT_FOUND response).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/services/org-admin.js`:
- Around line 621-633: The current role assignment block (condition using
isAdmin, bodyData.roles) uses roleQueries.findAll({ title: roles }) which can
drop invalid titles and is not tenant/active scoped; change the logic around
roleQueries.findAll (the call in src/services/org-admin.js) to query roles by
title AND tenant (and active status) so results are tenant-scoped, then verify
that the number of returned records matches the number of requested roles
exactly; if there is any mismatch, return the same INVALID_ROLE_ASSIGNMENTS
failure instead of silently accepting a partial set; keep assigning
updateData.roles = roleIds and setting hasRoleUpdate only after the exact-match
check passes.
- Around line 671-674: The role-update path currently calls
userQueries.updateUser({ id: userId }, updateData) without scoping to the tenant
or checking the update result; change the filter passed to
userQueries.updateUser to include the tenant identifier (e.g. { id: userId,
tenantId: currentTenantId }) so you only affect users in the same tenant, then
inspect the update result (rowsAffected / returned count / updated record) from
userQueries.updateUser and throw or return an error when no rows were updated
(to surface missing/invalid user in this tenant). Apply this change where
hasRoleUpdate is handled so role updates are tenant-scoped and validated.

In `@src/validators/v1/account.js`:
- Around line 73-87: The custom validator for req.checkBody('phone_code') only
checks a trimmed version but doesn't persist it; update the custom function in
src/validators/v1/account.js (the phone_code validator) to assign the normalized
value back to req.body.phone_code (e.g., set req.body.phone_code = trimmed)
after successful validation so whitespace is removed; apply the same change to
the other phone_code validator instance around the 194-208 block to ensure both
validators persist the normalized trimmed value.
- Around line 73-87: The phone_code validator in req.checkBody('phone_code')
only checks length and allows non-numeric values; update it to match the
login/account rules by trimming the value then validating it against a strict
country-code pattern (e.g., digits-only with optional leading '+' or just
digits) and enforcing the correct length (1–4 digits as used elsewhere),
throwing the same error messages when invalid or missing (keep the existing
conditional that requires phone_code when req.body.phone is present) so create
uses the same numeric/country-code validation as the login validation.

---

Nitpick comments:
In `@src/services/org-admin.js`:
- Around line 595-602: Remove the commented-out legacy user existence check in
the org admin flow: delete the commented block that declares checkUser and calls
userQueries.findOne({ id: userId }) and the subsequent CLIENT_ERROR response;
this unused commented logic should be removed from the method in
src/services/org-admin.js to keep the codebase clean and maintainable (look for
the commented checkUser / userQueries.findOne and the USER_NOT_FOUND response).

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4fc8628 and a966367.

📒 Files selected for processing (4)
  • src/services/org-admin.js
  • src/services/user.js
  • src/validators/v1/account.js
  • src/validators/v1/tenant.js

Comment on lines +621 to +633
if (isAdmin && bodyData.roles && Array.isArray(bodyData.roles) && bodyData.roles.length > 0) {
let roles = _.without(bodyData.roles, common.ADMIN_ROLE)
let getRoleIds = await roleQueries.findAll({ title: roles }, { attributes: ['id'] })
if (!getRoleIds) {
if (!getRoleIds || getRoleIds.length === 0) {
return responses.failureResponse({
message: 'INVALID_ROLE_ASSIGNMENTS',
statusCode: httpStatusCode.bad_request,
responseCode: 'CLIENT_ERROR',
})
}
let roleIds = getRoleIds.map((roleId) => roleId.id)
let checkUser = await userQueries.findOne({ id: userId })
if (!checkUser) {
return responses.failureResponse({
responseCode: 'CLIENT_ERROR',
statusCode: httpStatusCode.bad_request,
message: 'USER_NOT_FOUND',
})
updateData.roles = roleIds
hasRoleUpdate = true
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reject partial/unsafely-scoped role resolution.

Line 623 resolves roles by title only and Line 624 only checks > 0. This can silently accept partial matches (invalid roles dropped) and isn’t scoped to tenant/active roles.

Suggested fix
- let roles = _.without(bodyData.roles, common.ADMIN_ROLE)
- let getRoleIds = await roleQueries.findAll({ title: roles }, { attributes: ['id'] })
- if (!getRoleIds || getRoleIds.length === 0) {
+ const roleTitles = [...new Set(_.without(bodyData.roles, common.ADMIN_ROLE))]
+ const getRoleIds = await roleQueries.findAll(
+   {
+     title: roleTitles,
+     tenant_code: tokenInformation.tenant_code,
+     status: common.ACTIVE_STATUS,
+   },
+   { attributes: ['id', 'title'] }
+ )
+ if (!getRoleIds || getRoleIds.length !== roleTitles.length) {
    return responses.failureResponse({
      message: 'INVALID_ROLE_ASSIGNMENTS',
      statusCode: httpStatusCode.bad_request,
      responseCode: 'CLIENT_ERROR',
    })
  }
- let roleIds = getRoleIds.map((roleId) => roleId.id)
+ const roleIds = getRoleIds.map(({ id }) => id)

As per coding guidelines, src/services/** is core business logic and should be checked for correctness, efficiency, and potential edge cases.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/org-admin.js` around lines 621 - 633, The current role
assignment block (condition using isAdmin, bodyData.roles) uses
roleQueries.findAll({ title: roles }) which can drop invalid titles and is not
tenant/active scoped; change the logic around roleQueries.findAll (the call in
src/services/org-admin.js) to query roles by title AND tenant (and active
status) so results are tenant-scoped, then verify that the number of returned
records matches the number of requested roles exactly; if there is any mismatch,
return the same INVALID_ROLE_ASSIGNMENTS failure instead of silently accepting a
partial set; keep assigning updateData.roles = roleIds and setting hasRoleUpdate
only after the exact-match check passes.

Comment on lines +671 to +674
// Update roles if provided (only for admin)
if (hasRoleUpdate) {
await userQueries.updateUser({ id: userId }, updateData)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Scope role update by tenant and validate affected rows.

Line 673 updates by { id: userId } only and does not verify update count. This can mutate the wrong tenant’s user and can return success even when no user was updated.

Suggested fix
 if (hasRoleUpdate) {
-  await userQueries.updateUser({ id: userId }, updateData)
+  const [rowsAffected] = await userQueries.updateUser(
+    { id: userId, tenant_code: tokenInformation.tenant_code },
+    updateData
+  )
+  if (rowsAffected === 0) {
+    return responses.failureResponse({
+      responseCode: 'CLIENT_ERROR',
+      statusCode: httpStatusCode.bad_request,
+      message: 'USER_NOT_FOUND',
+    })
+  }
 }

As per coding guidelines, src/services/** is core business logic and should be checked for correctness, efficiency, and potential edge cases.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/org-admin.js` around lines 671 - 674, The role-update path
currently calls userQueries.updateUser({ id: userId }, updateData) without
scoping to the tenant or checking the update result; change the filter passed to
userQueries.updateUser to include the tenant identifier (e.g. { id: userId,
tenantId: currentTenantId }) so you only affect users in the same tenant, then
inspect the update result (rowsAffected / returned count / updated record) from
userQueries.updateUser and throw or return an error when no rows were updated
(to surface missing/invalid user in this tenant). Apply this change where
hasRoleUpdate is handled so role updates are tenant-scoped and validated.

Comment on lines +73 to +87
req.checkBody('phone_code').custom((value) => {
// If phone is provided, phone_code is mandatory
if (req.body.phone && !value) {
throw new Error('phone_code is required when phone is provided')
}
// If phone_code is provided, validate its format
if (value) {
// Convert to string and trim if it's not already a string
const trimmed = typeof value === 'string' ? value.trim() : String(value || '').trim()
if (trimmed.length < 2 || trimmed.length > 4) {
throw new Error('Phone code must be between 2 and 4 characters')
}
}
return true
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Normalize phone_code after validation to avoid whitespace persistence.

Lines 81 and 202 validate against a trimmed value but never persist that normalized value, so req.body.phone_code can remain untrimmed.

Proposed fix
 		req.checkBody('phone_code').custom((value) => {
 			...
 			if (value) {
 				const trimmed = typeof value === 'string' ? value.trim() : String(value || '').trim()
 				...
+				req.body.phone_code = trimmed
 			}
 			return true
 		})

As per coding guidelines: src/validators/**: Validate all incoming data thoroughly. Check for missing or incomplete validation rules.

Also applies to: 194-208

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/validators/v1/account.js` around lines 73 - 87, The custom validator for
req.checkBody('phone_code') only checks a trimmed version but doesn't persist
it; update the custom function in src/validators/v1/account.js (the phone_code
validator) to assign the normalized value back to req.body.phone_code (e.g., set
req.body.phone_code = trimmed) after successful validation so whitespace is
removed; apply the same change to the other phone_code validator instance around
the 194-208 block to ensure both validators persist the normalized trimmed
value.

⚠️ Potential issue | 🟠 Major

create accepts invalid phone_code formats.

On Line 82, only length is enforced, so non-country-code values can pass. This is inconsistent with Line 203 and login validation, and can create accounts with unusable phone credentials.

Proposed fix
-		req.checkBody('phone_code').custom((value) => {
+		req.checkBody('phone_code').custom((value) => {
 			// If phone is provided, phone_code is mandatory
 			if (req.body.phone && !value) {
 				throw new Error('phone_code is required when phone is provided')
 			}
 			// If phone_code is provided, validate its format
 			if (value) {
-				// Convert to string and trim if it's not already a string
 				const trimmed = typeof value === 'string' ? value.trim() : String(value || '').trim()
-				if (trimmed.length < 2 || trimmed.length > 4) {
-					throw new Error('Phone code must be between 2 and 4 characters')
+				if (!/^\+[1-9][0-9]{0,3}$/.test(trimmed)) {
+					throw new Error('phone_code must be a valid country code (e.g., +1, +91)')
 				}
+				req.body.phone_code = trimmed
 			}
 			return true
 		})

As per coding guidelines: src/validators/**: Validate all incoming data thoroughly. Check for missing or incomplete validation rules.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
req.checkBody('phone_code').custom((value) => {
// If phone is provided, phone_code is mandatory
if (req.body.phone && !value) {
throw new Error('phone_code is required when phone is provided')
}
// If phone_code is provided, validate its format
if (value) {
// Convert to string and trim if it's not already a string
const trimmed = typeof value === 'string' ? value.trim() : String(value || '').trim()
if (trimmed.length < 2 || trimmed.length > 4) {
throw new Error('Phone code must be between 2 and 4 characters')
}
}
return true
})
req.checkBody('phone_code').custom((value) => {
// If phone is provided, phone_code is mandatory
if (req.body.phone && !value) {
throw new Error('phone_code is required when phone is provided')
}
// If phone_code is provided, validate its format
if (value) {
const trimmed = typeof value === 'string' ? value.trim() : String(value || '').trim()
if (!/^\+[1-9][0-9]{0,3}$/.test(trimmed)) {
throw new Error('phone_code must be a valid country code (e.g., +1, +91)')
}
req.body.phone_code = trimmed
}
return true
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/validators/v1/account.js` around lines 73 - 87, The phone_code validator
in req.checkBody('phone_code') only checks length and allows non-numeric values;
update it to match the login/account rules by trimming the value then validating
it against a strict country-code pattern (e.g., digits-only with optional
leading '+' or just digits) and enforcing the correct length (1–4 digits as used
elsewhere), throwing the same error messages when invalid or missing (keep the
existing conditional that requires phone_code when req.body.phone is present) so
create uses the same numeric/country-code validation as the login validation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants