Skip to content
92 changes: 63 additions & 29 deletions api-doc/Elevate-Project.postman_collection.json

Large diffs are not rendered by default.

271 changes: 190 additions & 81 deletions api-doc/api-doc.yaml

Large diffs are not rendered by default.

51 changes: 51 additions & 0 deletions controllers/v1/userProjects.js
Original file line number Diff line number Diff line change
Expand Up @@ -1566,4 +1566,55 @@ module.exports = class UserProjects extends Abstract {
}
})
}

/**
* @api {post} /v1/userProjects/updateAcl/:_id={{projectId}}
* @apiVersion 1.0.0
* @apiName Update ACL
* @apiHeader {String} X-auth-token Authenticity token
* @apiSampleRequest /v1/userProjects/updateAcl/68d38c9f69f139b91c9e57b8
* @apiSampleRequest {json} Request
* {
"acl": {
"visibility" : "SPECIFIC",
"users" : ["1", "2", "3", "4"]
}
}
* @apiUse successBody
* @apiParamExample {json} Response:
{
"success" : true,
"message" : "Project updated successfully.",
"result" : {
"_id" : "68d38c9f69f139b91c9e57b8",
"acl" : {
"visibility" : "SPECIFIC",
"users" : ["1", "2", "3", "4"]
}
}
}
*/

/**
* Update ACL for a project if user owns the project and submission level is ENTITY.
Comment thread
Prajwal17Tunerlabs marked this conversation as resolved.
*
* @method POST
* @name updateAcl
* @param {Object} req - request Data.
* @returns {Promise<Object>} - Response object containing update status, message, and updated ACL data
*/
async updateAcl(req) {
Comment thread
Prajwal17Tunerlabs marked this conversation as resolved.
return new Promise(async (resolve, reject) => {
try {
const response = await userProjectsHelper.updateAcl(req.params._id, req.body, req.userDetails)
Comment thread
Prajwal17Tunerlabs marked this conversation as resolved.
return resolve(response)
} 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,
})
}
})
}
}
5 changes: 4 additions & 1 deletion generics/constants/api-responses.js
Original file line number Diff line number Diff line change
Expand Up @@ -319,9 +319,12 @@ module.exports = {
USER_EXTENSION_DELETED: 'User extension deleted successfully',
NO_SOLUTION_FOUND_FOR_THE_LINK: 'This link appears to be invalid. Please use a valid link to continue.',
ACCESS_TOKEN_EXPIRED_CODE: 'ACC_TOKEN_EXPIRED',
ACCESS_TOKEN_EXPIRED: 'Access Token Expired!! Please Login Again.',
ACCESS_TOKEN_EXPIRED: 'Access Token Expired! Please Login Again.',
USER_SERVICE_DOWN_CODE: 'USER_SERVICE_DOWN',
USER_SERVICE_DOWN: 'User service is down',
INVALID_USER_PERMISSION: 'User do not have required permission ',
INVALID_USER_PERMISSION_CODE: 'INVALID_USER_PERMISSION',
PROJECT_DOES_NOT_BELONG_TO_USER: 'Project does not belong to the user!!',
SUBMISSION_LEVEL_NOT_COMPLIED: 'Not allowed to update project. SUBMISSION_LEVEL not complied!',
PROJECT_UPDATED_SUCCESSFULLY: 'Project updated successfully.',
}
4 changes: 2 additions & 2 deletions module/programs/validator/v1.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ module.exports = (req) => {
req.checkBody('requestForPIIConsent').exists().withMessage('required requestForPIIConsent value of program')
req.checkBody('scope')
.exists()
.withMessage('required solution scope')
.withMessage('required program scope')
.notEmpty()
.withMessage('solution scope cannot be empty')
.withMessage('program scope cannot be empty')
},
update: function () {
req.checkParams('_id')
Expand Down
47 changes: 16 additions & 31 deletions module/solutions/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,18 @@ module.exports = class SolutionsHelper {
CONSTANTS.common.MANDATORY_SCOPE_FIELD,
CONSTANTS.common.OPTIONAL_SCOPE_FIELD
)

// If a prefix is provided, modify all query conditions to apply on nested fields.
// Example: if prefix = "acl", a condition like { "scope": {...} }
// becomes { "acl.scope": {...} }.
// This ensures the targeting rules apply inside a specific nested object.
if (prefix != '') {
Comment thread
Prajwal17Tunerlabs marked this conversation as resolved.
builtQuery['$and'] = builtQuery['$and'].map((obj) => {
const key = Object.keys(obj)[0] // Extract the field name in the condition
const value = obj[key] // Extract the condition value
return { [`${prefix}.${key}`]: value }
})
}
filterQuery = {
...filterQuery,
...builtQuery,
Expand Down Expand Up @@ -2124,7 +2136,6 @@ module.exports = class SolutionsHelper {
}

matchQuery['$match']['tenantId'] = userDetails.userInformation.tenantId
matchQuery['$match']['orgId'] = userDetails.userInformation.organizationId

if (currentOrgOnly) {
let organizationId = userDetails.userInformation.organizationId
Expand Down Expand Up @@ -2537,27 +2548,6 @@ module.exports = class SolutionsHelper {
)
})

if (process.env.SUBMISSION_LEVEL == 'ENTITY' && requestedData.hasOwnProperty('entityId')) {
mergedData = userCreatedProjects.data.data
totalCount = mergedData.length
if (mergedData.length > 0) {
let startIndex = pageSize * (pageNo - 1)
let endIndex = startIndex + pageSize
mergedData = mergedData.slice(startIndex, endIndex)
}
return resolve({
success: true,
message: CONSTANTS.apiResponses.TARGETED_SOLUTIONS_FETCHED,
data: {
data: mergedData,
count: totalCount,
},
result: {
data: mergedData,
count: totalCount,
},
})
}
// Add program data to the fetched projects
if (userCreatedProjects.success && userCreatedProjects.data) {
totalCount = userCreatedProjects.data.count
Expand Down Expand Up @@ -3167,13 +3157,9 @@ module.exports = class SolutionsHelper {
try {
let query = { isDeleted: false }

if (process.env.SUBMISSION_LEVEL === 'ENTITY' && requestedData.hasOwnProperty('entityId')) {
if (process.env.SUBMISSION_LEVEL === 'ENTITY') {
// Use queryBasedOnRoleAndLocation function to form query for acl.visibility = SCOPE projects
let queryData = await this.queryBasedOnRoleAndLocation(
_.omit(requestedData, ['entityId']),
'',
'acl'
)
let queryData = await this.queryBasedOnRoleAndLocation(requestedData, '', 'acl')
// status of the project could be anything, hence deleting status property from the querydata
delete queryData.data.status
// isReusable field doesn't exist for projects model hence removing the key
Expand All @@ -3183,16 +3169,15 @@ module.exports = class SolutionsHelper {

// Construct query for projects accessible by the user
query = {
entityId: requestedData.entityId,
'solutionInformation.submissionLevel': process.env.SUBMISSION_LEVEL,
$or: [
{ 'acl.visibility': CONSTANTS.common.PROJECT_VISIBILITY_ALL },
{
'acl.visibility': CONSTANTS.common.PROJECT_VISIBILITY_SPECIFIC,
'acl.users': { $in: [userId] },
'acl.users': { $in: [userId.toString()] },
},
{ 'acl.visibility': CONSTANTS.common.PROJECT_VISIBILITY_SCOPE, ...matchQuery },
{ 'acl.visibility': CONSTANTS.common.PROJECT_VISIBILITY_SELF, userId: userId },
{ createdBy: userId },
],
}
} else {
Expand Down
171 changes: 103 additions & 68 deletions module/userProjects/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,8 @@ module.exports = class UserProjectsHelper {
message: CONSTANTS.apiResponses.USER_PROJECT_NOT_FOUND,
}
}

// if entityId & entityInformation are passed through payload, ignore them
const blackListedPayloadItems = ['entityId', 'entityInformation']
const blackListedPayloadItems = ['entityId', 'entityInformation', 'acl']
blackListedPayloadItems.map((payloadItem) => {
if (data.hasOwnProperty(payloadItem)) delete data[payloadItem]
})
Expand Down Expand Up @@ -182,6 +181,7 @@ module.exports = class UserProjectsHelper {
// validate user authenticity if the acl.visibility of project is SCOPE
else if (userProject[0].acl.visibility == CONSTANTS.common.PROJECT_VISIBILITY_SCOPE) {
let scopeData = data.userProfileInformation.scope
scopeData['tenantId'] = tenantId
let queryData = await solutionsHelper.queryBasedOnRoleAndLocation(scopeData, '', 'acl')
if (!queryData.success) {
return resolve(queryData)
Expand Down Expand Up @@ -274,7 +274,7 @@ module.exports = class UserProjectsHelper {

const projectsModel = Object.keys(schemas['projects'].schema)

let keysToRemoveFromUpdation = ['userRoleInformation', 'userProfile', 'certificate']
let keysToRemoveFromUpdation = ['userRoleInformation', 'userProfile', 'certificate', 'acl']
keysToRemoveFromUpdation.forEach((key) => {
if (data[key]) delete data[key]
})
Expand Down Expand Up @@ -302,70 +302,6 @@ module.exports = class UserProjectsHelper {
if (projectData && projectData.success == true) {
updateProject = _.merge(updateProject, projectData.data)
}
// let createNewProgramAndSolution = false;
// let solutionExists = false;

// if (data.programId && data.programId !== "") {

// // Check if program already existed in project and if its not an existing program.
// if (!userProject[0].programInformation) {
// createNewProgramAndSolution = true;
// } else if (
// userProject[0].programInformation &&
// userProject[0].programInformation._id &&
// userProject[0].programInformation._id.toString() !== data.programId
// ) {
// // Not an existing program.

// solutionExists = true;
// }

// } else if (data.programName) {

// if (!userProject[0].solutionInformation) {
// createNewProgramAndSolution = true;
// } else {
// solutionExists = true;
// // create new program using current name and add existing solution and remove program from it.
// }
// }

// if (createNewProgramAndSolution || solutionExists) {

// let programAndSolutionInformation =
// await this.createProgramAndSolution(
// data.programId,
// data.programName,
// updateProject.entityId ? [updateProject.entityId] : "",
// userToken,
// userProject[0].solutionInformation && userProject[0].solutionInformation._id ?
// userProject[0].solutionInformation._id : ""
// );

// if (!programAndSolutionInformation.success) {
// return resolve(programAndSolutionInformation);
// }

// if (solutionExists) {

// let updateProgram =
// await programsHelper.removeSolutions(
// userToken,
// userProject[0].programInformation._id,
// [userProject[0].solutionInformation._id]
// );

// if (!updateProgram.success) {
// throw {
// status: HTTP_STATUS_CODE.bad_request.status,
// message: CONSTANTS.apiResponses.PROGRAM_NOT_UPDATED
// }
// }
// }

// updateProject =
// _.merge(updateProject, programAndSolutionInformation.data);
// }

let booleanData = this.booleanData(schemas['projects'].schema)
let mongooseIdData = this.mongooseIdData(schemas['projects'].schema)
Expand Down Expand Up @@ -1698,7 +1634,10 @@ module.exports = class UserProjectsHelper {
if (bodyData.hasOwnProperty('acl')) {
bodyData.acl.visibility = bodyData.acl.visibility.toUpperCase()
bodyData.acl.users.push(userId)
if (!bodyData.acl.hasOwnProperty('scope') || !(bodyData.acl.scope.length > 0)) {
if (
!bodyData.acl.hasOwnProperty('scope') ||
!(Object.keys(bodyData.acl.scope.length) > 0)
) {
bodyData.acl['scope'] = solutionDetails.scope
}
projectCreation.data['acl'] = bodyData.acl
Expand Down Expand Up @@ -4282,6 +4221,7 @@ module.exports = class UserProjectsHelper {
validateAllTasks(allTasksFalttened)
}

delete updateData.acl
let updateResult = await this.sync(
projectId,
'',
Expand Down Expand Up @@ -4550,6 +4490,101 @@ module.exports = class UserProjectsHelper {
}
})
}

/**
* Update ACL for a project if user owns the project and submission level is ENTITY.
*
* @param {String} projectId - The ID of the project whose ACL should be updated
* @param {Object} bodyData - The request body containing ACL updates
* @param {Object} userDetails - Logged-in user's details object
* @returns {Promise<Object>} - Response object containing update status, message, and updated ACL data
*
* @throws {Object} - Throws an error if submission level is invalid or project update fails
*/
static updateAcl(projectId, bodyData, userDetails) {
return new Promise(async (resolve, reject) => {
try {
// Only allow updates if submission level is ENTITY
if (process.env.SUBMISSION_LEVEL !== 'ENTITY') {
throw {
success: false,
message: CONSTANTS.apiResponses.SUBMISSION_LEVEL_NOT_COMPLIED,
}
}

// Extract user and tenant IDs
const userId = userDetails.userInformation.userId
const tenantId = userDetails.userInformation.tenantId

// Check if project exists and belongs to the user
const projectData = await projectQueries.projectDocument({
_id: projectId,
createdBy: userId,
tenantId,
})

if (!projectData || projectData.length === 0) {
throw {
success: false,
message: CONSTANTS.apiResponses.PROJECT_NOT_FOUND,
status: HTTP_STATUS_CODE['bad_request'].status,
}
}
if (
bodyData.acl.visibility === CONSTANTS.common.PROJECT_VISIBILITY_SPECIFIC &&
bodyData.acl.users.length > 0
) {
if (!bodyData.acl.users.includes(userId.toString())) {
bodyData.acl.users.push(userId.toString())
}
}
// Update ACL field in the project
const updatedProject = await projectQueries.findOneAndUpdate(
{
_id: projectId,
createdBy: userId,
tenantId,
},
{
$set: { acl: bodyData.acl },
Comment thread
Prajwal17Tunerlabs marked this conversation as resolved.
},
{
new: true,
}
)

// Check if update was successful
if (!updatedProject) {
throw {
success: false,
message: CONSTANTS.apiResponses.PROJECT_UPDATE_FAILED,
}
}

// Success response
return resolve({
success: true,
status: 200,
message: CONSTANTS.apiResponses.PROJECT_UPDATED_SUCCESSFULLY,
result: {
Comment thread
Prajwal17Tunerlabs marked this conversation as resolved.
_id: updatedProject._id,
acl: updatedProject.acl,
},
data: {
_id: updatedProject._id,
acl: updatedProject.acl,
},
})
} catch (error) {
// Unified error response
return reject({
message: error.message,
success: false,
status: error.status ? error.status : HTTP_STATUS_CODE.internal_server_error.status,
})
}
})
}
}

/**
Expand Down
Loading