diff --git a/.github/workflows/brac-dev-deployment.yaml b/.github/workflows/brac-dev-deployment.yaml new file mode 100644 index 0000000..9483f49 --- /dev/null +++ b/.github/workflows/brac-dev-deployment.yaml @@ -0,0 +1,79 @@ +name: Dev Build & Deploy Entity 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_DEV }} + key: ${{ secrets.SSH_KEY_DEV }} + port: ${{ secrets.PORT_DEV }} + script: | + set -e + # Export AWS variables + export AWS_REGION="${{ secrets.AWS_REGION }}" + export AWS_ACCOUNT_ID="${{ secrets.AWS_ACCOUNT_ID }}" + 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 }} + 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/src/constants/interface-routes/configs.json b/src/constants/interface-routes/configs.json index f990ca1..f087f58 100644 --- a/src/constants/interface-routes/configs.json +++ b/src/constants/interface-routes/configs.json @@ -156,6 +156,19 @@ } ] }, + { + "sourceRoute": "/entity-management/v1/entities/createUserAsAnEntity", + "type": "POST", + "priority": "MUST_HAVE", + "inSequence": false, + "orchestrated": false, + "targetPackages": [ + { + "basePackageName": "entity", + "packageName": "elevate-entity-management" + } + ] + }, { "sourceRoute": "/entity-management/v1/entities/update", "type": "POST", diff --git a/src/controllers/v1/entities.js b/src/controllers/v1/entities.js index 08ae63d..38248f2 100644 --- a/src/controllers/v1/entities.js +++ b/src/controllers/v1/entities.js @@ -641,6 +641,144 @@ module.exports = class Entities extends Abstract { }) } + /** + * Add entities after bulk import from user service. + * @api {POST} /entity/api/v1/entities/createUserAsAnEntity + * @apiVersion 1.0.0 + * @apiName createUserAsAnEntity + * @apiGroup Entities + * @apiHeader {String} X-authenticated-user-token Authenticity token + * @apiHeader {String} internal-access-token Internal access token + * @param {Object} req - Event data from user service bulk create. + * @returns {JSON} - Added entity information. + */ + createUserAsAnEntity(req) { + return new Promise(async (resolve, reject) => { + try { + const eventData = req.body + + // Get organizations from event data - check direct, oldValues, and newValues + const organizations = + eventData.organizations || + eventData.oldValues?.organizations || + eventData.newValues?.organizations || + [] + + // Determine entity type based on roles in organizations array + // Priority: first check for org_admin role, then check for user role + let entityType = null + if (organizations.length > 0 && organizations[0].roles && Array.isArray(organizations[0].roles)) { + const roles = organizations[0].roles + const roleTitles = roles.map((role) => role.title || role.label) + + // Check for org_admin role first (higher priority) + if (roleTitles.includes(CONSTANTS.common.ORG_ADMIN)) { + entityType = 'linkageChampion' + } else if (roleTitles.includes(CONSTANTS.common.USER_ROLE)) { + // Check for user role + entityType = 'participant' + } + } + + // If no supported role found, skip the operation + if (!entityType) { + return resolve({ + message: CONSTANTS.apiResponses.ENTITY_TYPE_NOT_SUPPORTED, + result: null, + }) + } + + // Extract required data from event - check direct, oldValues, and newValues + const externalId = eventData.entityId ? eventData.entityId.toString() : null + const name = eventData.name || eventData.oldValues?.name || eventData.newValues?.name || null + + if (!externalId || !name) { + return reject({ + status: HTTP_STATUS_CODE.bad_request.status, + message: CONSTANTS.apiResponses.MISSING_REQUIRED_FIELDS, + errorObject: { externalId, name }, + }) + } + + // Construct userDetails from event data - check direct, oldValues, and newValues + const tenantId = + eventData.tenant_code || + eventData.oldValues?.tenant_code || + eventData.newValues?.tenant_code || + null + const orgId = organizations.length > 0 ? organizations[0].id : null + const userId = + eventData.created_by || + eventData.id || + eventData.userId || + eventData.oldValues?.id || + eventData.newValues?.id || + null + + if (!tenantId || !orgId) { + return reject({ + status: HTTP_STATUS_CODE.bad_request.status, + message: CONSTANTS.apiResponses.MISSING_TENANT_OR_ORG_INFO, + errorObject: { tenantId, orgId }, + }) + } + + const userDetails = { + userInformation: { + userId: userId ? userId.toString() : 'SYSTEM', + }, + tenantAndOrgInfo: { + tenantId: tenantId, + orgId: [orgId.toString()], + }, + } + + // Check if entity with same externalId already exists + const existingEntity = await entitiesQueries.findOne( + { + 'metaInformation.externalId': externalId, + entityType: entityType, + tenantId: tenantId, + }, + { _id: 1, metaInformation: 1, entityType: 1 } + ) + + if (existingEntity) { + // Entity already exists, return existing entity + return resolve({ + message: CONSTANTS.apiResponses.ENTITY_ALREADY_EXISTS, + result: existingEntity, + }) + } + + // Prepare query parameters + const queryParams = { + type: entityType, + } + + // Prepare request body for entity creation + const entityBody = { + externalId: externalId, + name: name, + } + + // Call 'entitiesHelper.add' to perform the entity addition operation + let result = await entitiesHelper.add(queryParams, entityBody, userDetails) + + return resolve({ + message: CONSTANTS.apiResponses.ENTITY_ADDED, + result: result, + }) + } catch (error) { + return reject({ + status: error.status || HTTP_STATUS_CODE.internal_server_error.status, + message: error.message || HTTP_STATUS_CODE.internal_server_error.message, + errorObject: error, + }) + } + }) + } + /** * List of entities by location ids. * @api {get} v1/entities/list List all entities based locationIds diff --git a/src/generics/constants/api-responses.js b/src/generics/constants/api-responses.js index 2f76711..01ff6f4 100644 --- a/src/generics/constants/api-responses.js +++ b/src/generics/constants/api-responses.js @@ -80,4 +80,8 @@ module.exports = { ENTITIES_DELETED_SUCCESSFULLY: 'ENTITIES_DELETED_SUCCESSFULLY', ADMIN_ROLE_REQUIRED: 'Access denied: Admin role required', NOT_A_VALID_MONGOID: 'externalId cannot be a Mongo ObjectId', + ENTITY_ALREADY_EXISTS: 'ENTITY_ALREADY_EXISTS', + MISSING_TENANT_OR_ORG_INFO: 'MISSING_TENANT_OR_ORG_INFO', + MISSING_REQUIRED_FIELDS: 'MISSING_REQUIRED_FIELDS', + ENTITY_TYPE_NOT_SUPPORTED: 'ENTITY_TYPE_NOT_SUPPORTED', } diff --git a/src/generics/constants/common.js b/src/generics/constants/common.js index ded910e..a34c402 100644 --- a/src/generics/constants/common.js +++ b/src/generics/constants/common.js @@ -14,6 +14,7 @@ module.exports = { '/entities/bulkCreate', '/entities/bulkUpdate', '/entities/add', + '/entities/createUserAsAnEntity', '/entities/update', '/entityTypes/create', '/entityTypes/update', @@ -44,6 +45,7 @@ module.exports = { ADMIN_ROLE: 'admin', ORG_ADMIN: 'org_admin', TENANT_ADMIN: 'tenant_admin', + USER_ROLE: 'user', SERVER_TIME_OUT: 5000, GUEST_URLS: ['/entities/details', '/entities/entityListBasedOnEntityType', 'entities/subEntityList'], ALL: 'ALL', diff --git a/src/generics/middleware/authenticator.js b/src/generics/middleware/authenticator.js index a86006a..2fa8ed8 100644 --- a/src/generics/middleware/authenticator.js +++ b/src/generics/middleware/authenticator.js @@ -99,7 +99,9 @@ module.exports = async function (req, res, next, token = '') { ) if (performInternalAccessTokenCheck) { - if (req.headers['internal-access-token'] !== process.env.INTERNAL_ACCESS_TOKEN) { + const internalAccessToken = req.headers['internal_access_token'] || req.headers['internal-access-token'] + + if (internalAccessToken !== process.env.INTERNAL_ACCESS_TOKEN) { rspObj.errCode = CONSTANTS.apiResponses.TOKEN_MISSING_CODE rspObj.errMsg = CONSTANTS.apiResponses.TOKEN_MISSING_MESSAGE rspObj.responseCode = HTTP_STATUS_CODE['unauthorized'].status diff --git a/src/models/entities.js b/src/models/entities.js index 8efbfc6..b2d9122 100644 --- a/src/models/entities.js +++ b/src/models/entities.js @@ -18,6 +18,9 @@ module.exports = { externalId: { type: String, index: true }, name: { type: String, index: true }, targetedEntityTypes: { type: Array }, + status: { type: String, default: 'ACTIVE', index: true }, + onBoardingProjectId: { type: String }, + IDPProjectId: { type: String }, }, childHierarchyPath: Array, userId: { diff --git a/src/module/entities/helper.js b/src/module/entities/helper.js index 6e94f3d..1e8a52f 100644 --- a/src/module/entities/helper.js +++ b/src/module/entities/helper.js @@ -1278,7 +1278,6 @@ module.exports = class UserProjectsHelper { let result = await entitiesQueries.getAggregate(aggregateData) count = result?.[0]?.totalCount?.[0]?.count || 0 - if (aggregateStaging == true) { if (!Array.isArray(result) || !(result.length > 0)) { throw { diff --git a/src/module/entities/validator/v1.js b/src/module/entities/validator/v1.js index 5946d04..5d60dbc 100644 --- a/src/module/entities/validator/v1.js +++ b/src/module/entities/validator/v1.js @@ -145,6 +145,11 @@ module.exports = (req) => { registryMappingUpload: function () { req.checkQuery('entityType').exists().withMessage('required entity type') }, + createUserAsAnEntity: function () { + // Minimal validation for internal endpoint receiving event data + req.checkBody('entity').exists().withMessage('entity field is required') + req.checkBody('entityId').exists().withMessage('entityId field is required') + }, } if (entitiesValidator[req.params.method]) {