diff --git a/.github/workflows/brac-dev-deployment.yaml b/.github/workflows/brac-dev-deployment.yaml new file mode 100644 index 00000000..57cbee0a --- /dev/null +++ b/.github/workflows/brac-dev-deployment.yaml @@ -0,0 +1,86 @@ +name: Dev Build & Deploy User Service (BRAC) + +on: + push: + branches: + - develop + +env: + AWS_REGION: ${{ secrets.AWS_REGION }} + ECR_REPOSITORY: ${{ secrets.ECR_REPOSITORY_BRAC }} + AWS_ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }} + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + + permissions: + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + # ========================= + # AWS Authentication + # ========================= + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ env.AWS_REGION }} + + # ========================= + # Login to Amazon ECR + # ========================= + - name: Login to Amazon ECR + uses: aws-actions/amazon-ecr-login@v2 + + # ========================= + # Build & Push Image + # ========================= + - name: Build and Push Docker Image to ECR + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: | + ${{ env.AWS_ACCOUNT_ID }}.dkr.ecr.${{ env.AWS_REGION }}.amazonaws.com/${{ env.ECR_REPOSITORY }}:latest-brac + ${{ env.AWS_ACCOUNT_ID }}.dkr.ecr.${{ env.AWS_REGION }}.amazonaws.com/${{ env.ECR_REPOSITORY }}:${{ github.sha }} + # ========================= + # Deploy on Server + # ========================= + - name: Deploy Stack + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.HOST_NAME_DEV }} + username: ${{ secrets.USERNAME }} + key: ${{ secrets.SSH_KEY }} + port: ${{ secrets.PORT }} + script: | + set -e + + # Export AWS variables + export AWS_REGION="${{ secrets.AWS_REGION }}" + export AWS_ACCOUNT_ID="${{ secrets.AWS_ACCOUNT_ID }}" + #export aws-access-key-id="${{ secrets.AWS_ACCESS_KEY_ID }}" + #export aws-secret-access-key="${{ secrets.AWS_SECRET_ACCESS_KEY }}" + cd ${{ secrets.TARGET_DIR_DEV }} + + # Backup old env if exists + if [ -f .env ]; then + mv .env .env-bkp + fi + + # Write env safely (MULTILINE SAFE) + cat << 'EOF' > .env + ${{ secrets.DEV_ENV_BRAC }} + EOF + + # Login to ECR (non-interactive) + aws ecr get-login-password --region "$AWS_REGION" \ + | docker login --username AWS \ + --password-stdin "$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com" + + ./deploy.sh diff --git a/.github/workflows/brac-qa-deplyment.yaml b/.github/workflows/brac-qa-deplyment.yaml new file mode 100644 index 00000000..3cd4f440 --- /dev/null +++ b/.github/workflows/brac-qa-deplyment.yaml @@ -0,0 +1,87 @@ +name: Tag Build & Deploy User Service (BRAC) + +on: + push: + tags: + - "v*" + +env: + AWS_REGION: ${{ secrets.AWS_REGION }} + AWS_ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }} + ECR_REPOSITORY: ${{ secrets.ECR_REPOSITORY_BRAC }} + TAG: ${{ github.ref_name }} + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + + permissions: + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + # ========================= + # AWS Authentication + # ========================= + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ env.AWS_REGION }} + + # ========================= + # Login to Amazon ECR + # ========================= + - name: Login to Amazon ECR + uses: aws-actions/amazon-ecr-login@v2 + + # ========================= + # Build & Push Docker Image + # ========================= + - name: Build and Push Docker Image to ECR + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: | + ${{ env.AWS_ACCOUNT_ID }}.dkr.ecr.${{ env.AWS_REGION }}.amazonaws.com/${{ env.ECR_REPOSITORY }}:${{ env.TAG }} + + # ========================= + # Deploy on QA Server + # ========================= + - name: Deploy Stack to QA + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.HOST_NAME_QA }} + username: ${{ secrets.USERNAME }} + key: ${{ secrets.EC2_KEY }} + port: ${{ secrets.PORT }} + script: | + set -e + + export AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_ID }} + export AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }} + export AWS_REGION=${{ env.AWS_REGION }} + + cd ${{ secrets.TARGET_DIR_QA }} + + # Backup old env if exists + if [ -f .env ]; then + mv .env .env-bkp + fi + + # Write env safely (MULTILINE SAFE) + cat << 'EOF' > .env + ${{ secrets.QA_ENV_BRAC }} + EOF + + aws ecr get-login-password --region ${AWS_REGION} \ + | docker login \ + --username AWS \ + --password-stdin \ + ${{ env.AWS_ACCOUNT_ID }}.dkr.ecr.${AWS_REGION}.amazonaws.com + + ./deploy.sh ${{ env.TAG }} diff --git a/src/api-doc/bulkUser.md b/src/api-doc/bulkUser.md new file mode 100644 index 00000000..1a9c9762 --- /dev/null +++ b/src/api-doc/bulkUser.md @@ -0,0 +1,182 @@ +# Bulk User Creation Guide + +This guide provides step-by-step instructions for performing bulk user creation in the Elevate User Service. + +## Prerequisites + +- Valid JWT token with admin privileges +- CSV file containing user data in the required format +- Access to the API endpoints + +## CSV Format + +Your CSV file must include the following columns (case-sensitive): + +``` +name,email,phone_code,phone,username,password,roles,province,district,local_municipality,linkageChampion,supervisor +``` + +### Sample CSV Content + +``` +name,email,phone_code,phone,username,password,roles,province,district,local_municipality,linkageChampion,supervisor +Farabi Ahmedullah,farabi.ahmedullah@yopmail.com,91,7012345499,farabi,Password@123,session_manager,SA-EC,SA-EC-ALFR,SA-EC-ALFR-MATA,,amolp +Carol Miranda,carol.miranda@yopmail.com,91,7012345599,carol,Password@123,session_manager,SA-EC,SA-EC-ALFR,SA-EC-ALFR-MATA,,amolp +Amol Patil,amol,patil@yopmail.com,91,7012345699,amolp,Password@123,org_admin,SA-EC,SA-EC-ALFR,SA-EC-ALFR-MATA,, +Suvarna Kale,suvarnak@yopmail.com,91,7012345699,suvarna,Password@123,user,SA-EC,SA-EC-ALFR,SA-EC-ALFR-MATA,,farabi +``` + +### Field Descriptions + +- `name`: User's full name (required) +- `email`: User's email address (required if phone not provided) +- `phone_code`: Country code for phone (e.g., 91 for India) +- `phone`: User's phone number (required if email not provided) +- `username`: Desired username (optional, system will generate if not provided) +- `password`: User's password (required for direct creation, not for invitations) +- `roles`: Comma-separated list of roles (e.g., "session_manager,org_admin") +- Additional columns like `province`, `district`, etc., are for metadata + +## Step-by-Step Process + +### Step 1: Get Signed URL for File Upload + +First, obtain a signed URL to upload your CSV file to cloud storage. + +**Endpoint:** `GET /v1/cloud-services/file/getSignedUrl` + +**Query Parameters:** + +- `fileName`: Name of your CSV file (e.g., `bulk_users.csv`) + +**Headers:** + +- `X-auth-token`: Your JWT token + +**Example Request:** + +```bash +curl --location '{{baseURL}}user/v1/cloud-services/file/getSignedUrl?fileName=bulk_users.csv' \ +--header 'X-auth-token: YOUR_JWT_TOKEN' +``` + +**Response:** + +```json +{ + "success": true, + "message": "SIGNED_URL_GENERATED_SUCCESSFULLY", + "result": { + "signedUrl": "https://your-cloud-storage-url...", + "filePath": "users/YOUR_USER_ID-TIMESTAMP-bulk_users.csv", + "destFilePath": "users/YOUR_USER_ID-TIMESTAMP-bulk_users.csv" + } +} +``` + +### Step 2: Upload CSV File + +Upload your CSV file to the signed URL obtained in Step 1. + +**Example Request:** + +```bash +curl -X PUT -T /path/to/your/bulk_users.csv 'SIGNED_URL_FROM_STEP_1' +``` + +**Note:** Replace `/path/to/your/bulk_users.csv` with the actual path to your CSV file, and use single quotes around the signed URL to prevent shell interpretation of special characters. + +### Step 3: Perform Bulk User Creation + +Call the bulk user creation endpoint with the file path from Step 1. + +**Endpoint:** `POST /v1/tenant/bulkUserCreate` + +**Headers:** + +- `X-auth-token`: Your JWT token +- Organization code header (configurable via `ORG_CODE_HEADER_NAME` env var, defaults to `x-org-code`): Your organization code (e.g., `brac_gbl`) +- Tenant code header (configurable via `TENANT_CODE_HEADER_NAME` env var, defaults to `x-tenant-code`): Your tenant code (e.g., `brac`) +- `Content-Type`: `application/json` + +**Note on Headers:** The header names for organization and tenant codes are configurable through environment variables: + +- `ORG_CODE_HEADER_NAME=organization` (current setting) +- `TENANT_CODE_HEADER_NAME=tenant` (current setting) + +If these are not set, the defaults are `x-org-code` and `x-tenant-code`. Use the appropriate header names based on your environment configuration. + +**Request Body:** + +```json +{ + "file_path": "users/YOUR_USER_ID-TIMESTAMP-bulk_users.csv", + "editable_fields": ["name", "email"], + "upload_type": "CREATE" +} +``` + +**Example Request:** + +```bash +curl --location 'http://localhost:3567/user/v1/tenant/bulkUserCreate' \ +--header 'Content-Type: application/json' \ +--header 'X-auth-token: YOUR_JWT_TOKEN' \ +--header 'organization: brac_gbl' \ +--header 'tenant: brac' \ +--data '{ + "file_path" : "users/YOUR_USER_ID-TIMESTAMP-bulk_users.csv", + "editable_fields" : ["name"], + "upload_type": "CREATE" +}' +``` + +**Note:** The header names `organization` and `tenant` match the current environment variable settings. If your environment uses different header names (e.g., `x-org-code`, `x-tenant-code`), update the curl command accordingly. + +**Response:** + +```json +{ + "success": true, + "message": "USER_CSV_UPLOADED", + "result": { + "id": 123, + "name": "bulk_users.csv", + "input_path": "users/YOUR_USER_ID-TIMESTAMP-bulk_users.csv", + "type": "CSV", + "organization_id": 66, + "created_by": 3074, + "tenant_code": "brac", + "uploadType": "CREATE", + "status": "PENDING", + "created_at": "2025-12-26T06:31:24.000Z", + "updated_at": "2025-12-26T06:31:24.000Z" + } +} +``` + +## Processing and Results + +- The bulk upload is processed asynchronously via a background queue. +- You will receive an email notification with a download link to the results CSV once processing is complete. +- The results CSV will contain the status of each user creation/update attempt. + +## Upload Types + +- `"CREATE"`: Directly creates user accounts with provided passwords +- `"UPLOAD"`: Creates users and sends invitation emails +- `"INVITE"`: Sends invitation emails without creating accounts + +## Troubleshooting + +- **404 Error on Download**: Ensure the CSV file was successfully uploaded to the signed URL in Step 2. +- **Validation Errors**: Check that your CSV format matches the sample and all required fields are present. +- **Permission Denied**: Ensure your JWT token has admin privileges for the specified tenant and organization. +- **Expired Signed URL**: Signed URLs expire after 15 minutes. If expired, repeat Step 1. + +## Additional Notes + +- The process supports up to 1000 users per CSV file. +- Duplicate emails/phones will be handled based on existing user records. +- System-generated usernames will be assigned if not provided or if conflicts occur. +- All operations are logged and can be audited. diff --git a/src/controllers/v1/tenant.js b/src/controllers/v1/tenant.js index 0e628ed7..c0db673b 100644 --- a/src/controllers/v1/tenant.js +++ b/src/controllers/v1/tenant.js @@ -11,6 +11,8 @@ const utilsHelper = require('@generics/utils') const common = require('@constants/common') const httpStatusCode = require('@generics/http-status') const responses = require('@helpers/responses') +const accountService = require('@services/account') + module.exports = class Tenant { /** * Updates tenant data @@ -133,6 +135,7 @@ module.exports = class Tenant { return error } } + /** * List tenants * @method POST @@ -156,6 +159,29 @@ module.exports = class Tenant { return error } } + /** + * Create account user + * @method POST + * @name AccountCreate + * @param {Object} req -request data. + * @returns {JSON} - success or error message + */ + + async accountCreate(req) { + try { + let registerWithLogin = false + const usersRes = await accountService.create( + req.body, + {}, + '', + registerWithLogin, //set registerWithLogin to true, this will create user with login + req + ) + return usersRes + } catch (error) { + return error + } + } /** * Read tenant details for internal service calls diff --git a/src/database/queries/users.js b/src/database/queries/users.js index ed2e6d1c..8bfade29 100644 --- a/src/database/queries/users.js +++ b/src/database/queries/users.js @@ -401,6 +401,8 @@ exports.searchUsersWithOrganization = async ({ emailIds, excluded_user_ids, tenantCode, + status, + metaFilters, }) => { try { const offset = (page - 1) * limit @@ -408,6 +410,10 @@ exports.searchUsersWithOrganization = async ({ // Base filter for user const userWhere = {} + if (status) { + userWhere.status = status.toUpperCase() + } + // Filter by userIds / exclude if (userIds && Array.isArray(userIds)) { userWhere.id = { [Op.in]: userIds } @@ -427,6 +433,20 @@ exports.searchUsersWithOrganization = async ({ userWhere.name = { [Op.iLike]: `%${search}%` } } + // 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] || [] + 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)) + } + } + } + } + const users = await database.User.findAndCountAll({ where: userWhere, limit: limit, diff --git a/src/generics/utils.js b/src/generics/utils.js index e383ae26..aa7dbc4a 100644 --- a/src/generics/utils.js +++ b/src/generics/utils.js @@ -1038,7 +1038,7 @@ function parseMetaData(meta = {}, prunedEntities, feederData) { // find the entity type from the entities array with the value of the entity const findEntity = prunedEntities.find((entity) => entity.value == metaKey) // check the data type of the entity to loop in Array type entities - if (findEntity.data_type == 'ARRAY' || findEntity.data_type == 'ARRAY[STRING]') { + if (findEntity && (findEntity?.data_type == 'ARRAY' || findEntity?.data_type == 'ARRAY[STRING]')) { metaData[metaKey] = meta?.[metaKey].map((entity) => { const id = getId(entity) // get the id from the input const find = Object.values(feederData).find( diff --git a/src/helpers/userInvite.js b/src/helpers/userInvite.js index 3f11a31c..689ab5bb 100644 --- a/src/helpers/userInvite.js +++ b/src/helpers/userInvite.js @@ -338,13 +338,54 @@ module.exports = class UserInviteHelper { : [], } - delete row.block - delete row.state - delete row.school - delete row.cluster - delete row.district - delete row.professional_role - delete row.professional_subroles + // 2. Handle dynamic entityFields (skipping what we already did) + const alreadyProcessed = [ + 'block', + 'state', + 'school', + 'cluster', + 'district', + 'professional_role', + 'professional_subroles', + ] + + entityFields.forEach((field) => { + // 1. Skip if already handled in the hardcoded block + if (alreadyProcessed.includes(field)) return + + // 2. Only process if the row actually has data for this field + if (row[field]) { + // Find the definition to check if it's an ARRAY type + const entityDef = validationData.find((e) => e.value === field) + const cleanField = field.replaceAll(/\s+/g, '').toLowerCase() + + if ( + entityDef && + (entityDef.data_type === 'ARRAY' || entityDef.data_type === 'ARRAY[STRING]') + ) { + // Handle ARRAY types: split by comma, clean each value, and map to IDs + row.meta[field] = row[field] + .split(',') + .map((val) => { + const cleanVal = val.trim().replaceAll(/\s+/g, '').toLowerCase() + const lookupKey = `${cleanVal}${cleanField}` + return externalEntityNameIdMap?.[lookupKey]?._id + }) + .filter(Boolean) // Removes null/undefined if an ID isn't found + } else { + // Handle Single value types (Standard logic) + const cleanVal = String(row[field]).replaceAll(/\s+/g, '').toLowerCase() + const lookupKey = `${cleanVal}${cleanField}` + row.meta[field] = externalEntityNameIdMap?.[lookupKey]?._id || null + } + } + }) + + // 3. Delete all processed fields from the root row + const allFieldsToDelete = [...alreadyProcessed, ...entityFields] + allFieldsToDelete.forEach((field) => { + delete row[field] + }) // Handle password field if exists if (row.password) { diff --git a/src/services/account.js b/src/services/account.js index 33c8afe1..c0f586e4 100644 --- a/src/services/account.js +++ b/src/services/account.js @@ -39,6 +39,7 @@ const UserTransformDTO = require('@dtos/userDTO') const notificationUtils = require('@utils/notification') const userHelper = require('@helpers/userHelper') const { broadcastEvent } = require('@helpers/eventBroadcasterMain') +const { use } = require('i18next') module.exports = class AccountHelper { /** @@ -54,7 +55,8 @@ module.exports = class AccountHelper { * @returns {JSON} - returns account creation details. */ - static async create(bodyData, deviceInfo, domain) { + static async create(bodyData, deviceInfo, domain, registerWithLogin = true, req = {}) { + let tenantId const projection = ['password'] let isInvitedUserId = false @@ -65,18 +67,24 @@ module.exports = class AccountHelper { statusCode: httpStatusCode.not_acceptable, responseCode: 'CLIENT_ERROR', }) + 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, status: common.ACTIVE_STATUS, }) - + if (!tenantDetail) { return notFoundResponse('TENANT_NOT_FOUND_PING_ADMIN') } @@ -314,7 +322,7 @@ module.exports = class AccountHelper { role = await roleQueries.findAll( { 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, }, @@ -373,6 +381,8 @@ module.exports = class AccountHelper { }), userQueries.getColumns(), ]) + console.log('validationData', validationData) + console.log('userModel', userModel) const prunedEntities = removeDefaultOrgEntityTypes(validationData, userOrgId) @@ -460,6 +470,7 @@ module.exports = class AccountHelper { } else { userCredentials = await UserCredentialQueries.create(userCredentialsBody) } + /* FLOW STARTED: user login after registration */ user = await userQueries.findUserWithOrganization( { id: insertedUser.id, tenant_code: tenantDetail.code }, @@ -471,43 +482,71 @@ module.exports = class AccountHelper { ) const roleData = user.organizations[0].roles + 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 } } // user.user_roles = roleData @@ -529,29 +568,6 @@ module.exports = class AccountHelper { .join(' and ') : '' - 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 - ) - // Delete Redis OTP entries if (encryptedEmailId) await utilsHelper.redisDel(encryptedEmailId) if (encryptedPhoneNumber) await utilsHelper.redisDel(bodyData.phone_code + encryptedPhoneNumber) @@ -570,8 +586,6 @@ module.exports = class AccountHelper { ) } - const result = { access_token: accessToken, refresh_token: refreshToken, user } - if (plaintextEmailId) { notificationUtils.sendEmailNotification({ emailId: plaintextEmailId, @@ -635,7 +649,6 @@ module.exports = class AccountHelper { broadcastEvent('userEvents', { requestBody: eventBody, isInternal: true }) - return responses.successResponse({ statusCode: httpStatusCode.created, message: 'USER_CREATED_SUCCESSFULLY', @@ -1632,7 +1645,7 @@ module.exports = class AccountHelper { let foundKeys = {} let result = [] - /* Required to resolve all promises first before preparing response object else sometime + /* 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 */ await Promise.all( @@ -1714,8 +1727,7 @@ module.exports = class AccountHelper { // Clear Redis cache asynchronously (fire and forget) const redisUserKey = `${common.redisUserPrefix}${tenantCode}_${userId}` - utilsHelper.redisDel(redisUserKey).catch((err) => { - }) + utilsHelper.redisDel(redisUserKey).catch((err) => {}) return responses.successResponse({ statusCode: httpStatusCode.ok, @@ -1856,6 +1868,9 @@ module.exports = class AccountHelper { * @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. + * @param {Object} [params.query.meta] - Meta field filters (JSON string or object) to filter users by custom meta fields. + * @param {Object} [params.body.meta] - Meta field filters (object) to filter users by custom meta fields. + * Example: { "province": "6952163ae83c1c00147132a8", "district": "6952163ae83c1c00147132bb" } * * @returns {Promise} JSON response with user list and count. */ @@ -1889,6 +1904,23 @@ module.exports = class AccountHelper { } }) + // Extract meta filters from query or body (supports any meta field) + const metaFilters = {} + if (params.query?.meta) { + try { + // If meta is a JSON string, parse it + const meta = + typeof params.query.meta === 'string' ? JSON.parse(params.query.meta) : params.query.meta + Object.assign(metaFilters, meta) + } catch (e) { + // If parsing fails, ignore meta filter + } + } + // Also check body for meta filters + if (params.body?.meta && typeof params.body.meta === 'object') { + Object.assign(metaFilters, params.body.meta) + } + let users = await userQueries.searchUsersWithOrganization({ roleIds, organization_id: params.query.organization_id, @@ -1899,6 +1931,8 @@ module.exports = class AccountHelper { emailIds: emailIds.length > 0 ? emailIds : false, excluded_user_ids: params.body?.excluded_user_ids || false, tenantCode: params.query.tenant_code, + status: params.query.status || false, + metaFilters: Object.keys(metaFilters).length > 0 ? metaFilters : undefined, }) if (users.count == 0) { @@ -1912,7 +1946,7 @@ module.exports = class AccountHelper { }) } - /* Required to resolve all promises first before preparing response object else sometime + /* 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( @@ -1924,10 +1958,38 @@ module.exports = class AccountHelper { if (user.email) { user.email = emailEncryption.decrypt(user.email) } + if (user.phone) { + user.phone = emailEncryption.decrypt(user.phone) + } return user }) ) + 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) + } + return responses.successResponse({ statusCode: httpStatusCode.ok, message: 'USER_LIST',