From 3d4cea53db87f6483092dd6e5a287c6ffe0f17d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CKomal?= <“komal_m@tekditechnologies.com”> Date: Mon, 9 Feb 2026 16:19:12 +0530 Subject: [PATCH] Internal access user api --- src/.env.sample | 5 +- src/constants/common.js | 3 ++ src/envVariables.js | 5 ++ src/middlewares/authenticator.js | 83 +++++++++++++++++++++++++++----- 4 files changed, 83 insertions(+), 13 deletions(-) diff --git a/src/.env.sample b/src/.env.sample index b7c705af..784aea3b 100644 --- a/src/.env.sample +++ b/src/.env.sample @@ -27,8 +27,9 @@ KAFKA_TOPIC = 'topic' # Kafka topic to push notification data NOTIFICATION_KAFKA_TOPIC = notificationtopic -# Internal access token for communicationcation between services via network call -INTERNAL_ACCESS_TOKEN = 'internal-access-token' +# Internal access token for service-to-service / data-pipeline calls (e.g. internal user update). Set a strong secret. +INTERNAL_ACCESS_TOKEN= + # JWT Access Token expiry In Days ACCESS_TOKEN_EXPIRY = '1' diff --git a/src/constants/common.js b/src/constants/common.js index 03f4eccb..0690fe76 100644 --- a/src/constants/common.js +++ b/src/constants/common.js @@ -14,9 +14,12 @@ module.exports = { DEFAULT_PAGE_SIZE: 100, }, getPaginationOffset, + // Header used by data-pipeline / internal services to specify target user for internal user update (no user auth token) + INTERNAL_USER_ID_HEADER: 'x-internal-user-id', internalAccessUrls: [ '/user/v1/profile/details', '/user/v1/user/profileById', + '/user/v1/user/update', '/user/v1/account/list', '/user/v1/user/read', '/user/v1/admin/create', diff --git a/src/envVariables.js b/src/envVariables.js index 2fca2daf..6b3d461d 100644 --- a/src/envVariables.js +++ b/src/envVariables.js @@ -116,6 +116,11 @@ let enviromentVariables = { optional: true, default: 86400, }, + INTERNAL_ACCESS_TOKEN: { + message: + 'Secret token for internal/service-to-service and data-pipeline API calls (e.g. user update without user auth token). Keep secure.', + optional: true, + }, REDIS_HOST: { message: 'Redis Host Url', optional: false, diff --git a/src/middlewares/authenticator.js b/src/middlewares/authenticator.js index 912d1a81..db387a5b 100644 --- a/src/middlewares/authenticator.js +++ b/src/middlewares/authenticator.js @@ -21,21 +21,36 @@ const tenantDomainQueries = require('@database/queries/tenantDomain') const organizationQueries = require('@database/queries/organization') async function checkPermissions(roleTitle, requestPath, requestMethod) { - const parts = requestPath.match(/[^/]+/g) - const api_path = [`/${parts[0]}/${parts[1]}/${parts[2]}/*`] - - if (parts[4]) api_path.push(`/${parts[0]}/${parts[1]}/${parts[2]}/${parts[3]}*`) - else - api_path.push( - `/${parts[0]}/${parts[1]}/${parts[2]}/${parts[3]}`, - `/${parts[0]}/${parts[1]}/${parts[2]}/${parts[3]}*` - ) + const parts = requestPath.match(/[^/]+/g) || [] + let api_path + + if (parts.length >= 4) { + api_path = [`/${parts[0]}/${parts[1]}/${parts[2]}/*`] + if (parts[4]) api_path.push(`/${parts[0]}/${parts[1]}/${parts[2]}/${parts[3]}*`) + else + api_path.push( + `/${parts[0]}/${parts[1]}/${parts[2]}/${parts[3]}`, + `/${parts[0]}/${parts[1]}/${parts[2]}/${parts[3]}*` + ) + } else if (parts.length === 3) { + api_path = [`/${parts[0]}/${parts[1]}/${parts[2]}`, `/${parts[0]}/${parts[1]}/${parts[2]}/*`] + } else if (parts.length === 2) { + api_path = [`/${parts[0]}/${parts[1]}`, `/${parts[0]}/${parts[1]}/*`] + } else if (parts.length === 1) { + api_path = [`/${parts[0]}`, `/${parts[0]}/*`] + } else { + return false + } if (Array.isArray(roleTitle) && !roleTitle.includes(common.PUBLIC_ROLE)) { roleTitle.push(common.PUBLIC_ROLE) } - const filter = { role_title: roleTitle, module: parts[2], api_path: { [Op.in]: api_path } } + const filter = { + role_title: roleTitle, + ...(parts[2] != null && { module: parts[2] }), + api_path: { [Op.in]: api_path }, + } const attributes = ['request_type', 'api_path', 'module'] const allowedPermissions = await rolePermissionMappingQueries.findAll(filter, attributes) @@ -98,7 +113,53 @@ module.exports = async function (req, res, next) { } }) - if (internalAccess && !authHeader) return next() + // Internal access without user token (existing internal URLs unchanged; pipeline uses internal_access_token only) + if (internalAccess && !authHeader) { + // New path only: data-pipeline user update — resolve target user from headers, set decodedToken; no change to other internal URLs + const internalUserUpdatePath = '/user/v1/user/update' + if (req.path.includes(internalUserUpdatePath)) { + // Express normalizes headers to lowercase + const userId = (req.headers[common.INTERNAL_USER_ID_HEADER] || '').trim() + const tenantCode = (req.headers[common.TENANT_CODE_HEADER] || req.headers['x-tenant-code'] || '').trim() + if (!userId || !tenantCode) { + throw responses.failureResponse({ + message: 'INTERNAL_UPDATE_REQUIRES_USER_ID_AND_TENANT', + statusCode: httpStatusCode.bad_request, + responseCode: 'CLIENT_ERROR', + }) + } + const user = await userQueries.findUserWithOrganization( + { id: userId, tenant_code: tenantCode }, + {}, + false + ) + if (!user) { + throw responses.failureResponse({ + message: 'USER_NOT_FOUND', + statusCode: httpStatusCode.unauthorized, + responseCode: 'UNAUTHORIZED', + }) + } + const org = user.organizations?.[0] + if (!org) { + throw responses.failureResponse({ + message: 'USER_HAS_NO_ORGANIZATION', + statusCode: httpStatusCode.bad_request, + responseCode: 'CLIENT_ERROR', + }) + } + req.decodedToken = { + id: user.id, + name: user.name, + session_id: null, + tenant_code: user.tenant_code, + organization_id: org.id, + organization_code: org.code, + roles: org.roles || [], + } + } + return next() + } if (!authHeader) { try {