From 93c1ca9cb8d625b8da8b90a192c0f1e595b92d51 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Fri, 2 May 2025 23:54:04 +0200 Subject: [PATCH 01/10] feat: list copilot applications --- src/permissions/constants.js | 15 ++++++ src/permissions/copilotApplications.view.js | 55 +++++++++++++++++++++ src/permissions/index.js | 4 ++ src/routes/copilotOpportunityApply/list.js | 47 ++++++++++++++++++ src/routes/index.js | 2 + src/util.js | 17 +++++++ 6 files changed, 140 insertions(+) create mode 100644 src/permissions/copilotApplications.view.js create mode 100644 src/routes/copilotOpportunityApply/list.js diff --git a/src/permissions/constants.js b/src/permissions/constants.js index 4395021e..85c38465 100644 --- a/src/permissions/constants.js +++ b/src/permissions/constants.js @@ -277,6 +277,21 @@ export const PERMISSION = { // eslint-disable-line import/prefer-default-export scopes: SCOPES_PROJECTS_WRITE, }, + LIST_COPILOT_OPPORTUNITY: { + meta: { + title: 'Apply copilot opportunity', + group: 'Apply Copilot', + description: 'Who can apply for copilot opportunity.', + }, + topcoderRoles: [ + USER_ROLE.TOPCODER_ADMIN, + ], + projectRoles: [ + USER_ROLE.PROJECT_MANAGER, + ], + scopes: SCOPES_PROJECTS_WRITE, + }, + MANAGE_PROJECT_BILLING_ACCOUNT_ID: { meta: { title: 'Manage Project property "billingAccountId"', diff --git a/src/permissions/copilotApplications.view.js b/src/permissions/copilotApplications.view.js new file mode 100644 index 00000000..0fb14bf4 --- /dev/null +++ b/src/permissions/copilotApplications.view.js @@ -0,0 +1,55 @@ + +import _ from 'lodash'; +import util from '../util'; +import models from '../models'; + +/** + * Topcoder admin and Project managers who are part of the project can view the copilot applications in it + * Also, users who had an application will have access to view it. + * @param {Object} freq the express request instance + * @return {Promise} Returns a promise + */ +module.exports = freq => new Promise((resolve, reject) => { + console.log("start permission check"); + const opportunityId = _.parseInt(freq.params.id); + const currentUserId = freq.authUser.userId; + return models.CopilotOpportunity.find({ + where: { + id: opportunityId, + }, + }) + .then((opportunity) => { + const req = freq; + req.context = req.context || {}; + req.context.currentOpportunity = opportunity; + const projectId = opportunity.projectId; + const isProjectManager = util.hasProjectManagerRole(req); + + console.log("got opportunity", opportunityId); + return models.ProjectMember.getActiveProjectMembers(projectId) + .then((members) => { + + console.log("got active members", projectId); + return models.CopilotApplications.findOne({ + where: { + opportunityId: opportunityId, + userId: currentUserId, + }, + }).then((copilotApplication) => { + const isPartOfProject = isProjectManager && members.find(member => member.userId === currentUserId); + // check if auth user has acecss to this project + const hasAccess = util.hasAdminRole(req) || isPartOfProject || !!copilotApplication; + console.log("got assigned application", hasAccess); + return Promise.resolve(hasAccess); + }) + }) + }) + .then((hasAccess) => { + if (!hasAccess) { + const errorMessage = 'You do not have permissions to perform this action'; + // user is not an admin nor is a registered project member + return reject(new Error(errorMessage)); + } + return resolve(true); + }); +}); diff --git a/src/permissions/index.js b/src/permissions/index.js index edb9cb6f..01cb7799 100644 --- a/src/permissions/index.js +++ b/src/permissions/index.js @@ -9,6 +9,7 @@ const copilotAndAbove = require('./copilotAndAbove'); const workManagementPermissions = require('./workManagementForTemplate'); const projectSettingEdit = require('./projectSetting.edit'); const customerPaymentConfirm = require('./customerPayment.confirm'); +const viewCopilotApplications = require('./copilotApplications.view'); const generalPermission = require('./generalPermission'); const { PERMISSION } = require('./constants'); @@ -199,4 +200,7 @@ module.exports = () => { Authorizer.setPolicy('customerPayment.view', generalPermission(PERMISSION.VIEW_CUSTOMER_PAYMENT)); Authorizer.setPolicy('customerPayment.edit', generalPermission(PERMISSION.UPDATE_CUSTOMER_PAYMENT)); Authorizer.setPolicy('customerPayment.confirm', customerPaymentConfirm); + + // Copilot opportunity + Authorizer.setPolicy('copilotApplications.view', viewCopilotApplications); }; diff --git a/src/routes/copilotOpportunityApply/list.js b/src/routes/copilotOpportunityApply/list.js new file mode 100644 index 00000000..b7bb0cc7 --- /dev/null +++ b/src/routes/copilotOpportunityApply/list.js @@ -0,0 +1,47 @@ +import _ from 'lodash'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; + +import models from '../../models'; +import { ADMIN_ROLES } from '../../constants'; +import util from '../../util'; + +const permissions = tcMiddleware.permissions; + +module.exports = [ + permissions('copilotApplications.view'), + (req, res, next) => { + + console.log("start list operation"); + const isAdmin = util.hasRoles(req, ADMIN_ROLES); + + let sort = req.query.sort ? decodeURIComponent(req.query.sort) : 'createdAt desc'; + if (sort.indexOf(' ') === -1) { + sort += ' asc'; + } + const sortableProps = ['createdAt asc', 'createdAt desc']; + if (_.indexOf(sortableProps, sort) < 0) { + return util.handleError('Invalid sort criteria', null, req, next); + } + const sortParams = sort.split(' '); + + // Admin can see all requests and the PM can only see requests created by them + const whereCondition = _.assign({}, + isAdmin ? {} : { createdBy: userId }, + ); + + return models.CopilotApplication.findAll({ + where: whereCondition, + include: [ + { + model: models.CopilotOpportunity, + as: 'copilotOpportunity', + }, + ], + order: [[sortParams[0], sortParams[1]]], + }) + .then(copilotApplications => res.json(copilotApplications)) + .catch((err) => { + util.handleError('Error fetching copilot applications', err, req, next); + }); + }, +]; diff --git a/src/routes/index.js b/src/routes/index.js index 137c3759..97ba56ea 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -407,6 +407,8 @@ router.route('/v5/projects/copilot/opportunity/:id(\\d+)') // Project copilot opportunity apply router.route('/v5/projects/copilots/opportunity/:id(\\d+)/apply') .post(require('./copilotOpportunityApply/create')); +router.route('/v5/projects/copilots/opportunity/:id(\\d+)/applications') + .post(require('./copilotOpportunityApply/list')); // Project Estimation Items router.route('/v5/projects/:projectId(\\d+)/estimations/:estimationId(\\d+)/items') diff --git a/src/util.js b/src/util.js index 711b76a8..43b3c092 100644 --- a/src/util.js +++ b/src/util.js @@ -225,6 +225,23 @@ const projectServiceUtils = { return _.intersection(roles, ADMIN_ROLES.map(r => r.toLowerCase())).length > 0; }, + /** + * Helper funtion to verify if user has project manager role + * @param {object} req Request object that should contain authUser + * @return {boolean} true/false + */ + hasProjectManagerRole: (req) => { + const isMachineToken = _.get(req, 'authUser.isMachine', false); + const tokenScopes = _.get(req, 'authUser.scopes', []); + if (isMachineToken) { + if (_.indexOf(tokenScopes, M2M_SCOPES.CONNECT_PROJECT_ADMIN) >= 0) return true; + return false; + } + let roles = _.get(req, 'authUser.roles', []); + roles = roles.map(s => s.toLowerCase()); + return roles.includes(USER_ROLE.PROJECT_MANAGER.toLowerCase()); + }, + /** * Parses query fields and groups them per table * @param {array} queryFields list of query fields From b6f6b8bc7824095d119bdd6aad50b8580c2b878b Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Fri, 2 May 2025 23:54:34 +0200 Subject: [PATCH 02/10] added circle config --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 1acd4a4c..8e10e3e6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -149,7 +149,7 @@ workflows: context : org-global filters: branches: - only: ['develop', 'migration-setup'] + only: ['develop', 'migration-setup', 'pm-855'] - deployProd: context : org-global filters: From 45f8ff29fd330b24cca6a91c4db9ebf314694cf6 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Sun, 4 May 2025 00:13:21 +0200 Subject: [PATCH 03/10] changed route method --- docs/swagger.yaml | 58 +++++++++++++++++++++++++++++++++++++++++++++ src/routes/index.js | 2 +- 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/docs/swagger.yaml b/docs/swagger.yaml index dbadf9e6..ae0cd049 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -502,6 +502,64 @@ paths: description: "Internal Server Error" schema: $ref: "#/definitions/ErrorModel" + "/projects/copilots/opportunity/{copilotOpportunityId}/applications": + get: + tags: + - projects copilot opportunity applications + operationId: listCopilotOpportunity + security: + - Bearer: [] + description: "Retrieve the list copilot opportunity applications." + parameters: + - $ref: "#/parameters/copilotOpportunityIdParam" + - name: sort + required: false + description: > + sort projects by createdAt, updatedAt. Default + is createdAt asc + in: query + type: string + responses: + "200": + description: A list of projects + schema: + type: array + items: + $ref: "#/definitions/CopilotOpportunityApplication" + headers: + X-Next-Page: + type: integer + description: The index of the next page + X-Page: + type: integer + description: The index of the current page (starting at 1) + X-Per-Page: + type: integer + description: The number of items to list per page + X-Prev-Page: + type: integer + description: The index of the previous page + X-Total: + type: integer + description: The total number of items + X-Total-Pages: + type: integer + description: The total number of pages + Link: + type: string + description: Pagination link header. + "401": + description: "Unauthorized" + schema: + $ref: "#/definitions/ErrorModel" + "403": + description: "Forbidden - User does not have permission" + schema: + $ref: "#/definitions/ErrorModel" + "500": + description: "Internal Server Error" + schema: + $ref: "#/definitions/ErrorModel" "/projects/{projectId}/attachments": get: tags: diff --git a/src/routes/index.js b/src/routes/index.js index 97ba56ea..b07041ab 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -408,7 +408,7 @@ router.route('/v5/projects/copilot/opportunity/:id(\\d+)') router.route('/v5/projects/copilots/opportunity/:id(\\d+)/apply') .post(require('./copilotOpportunityApply/create')); router.route('/v5/projects/copilots/opportunity/:id(\\d+)/applications') - .post(require('./copilotOpportunityApply/list')); + .get(require('./copilotOpportunityApply/list')); // Project Estimation Items router.route('/v5/projects/:projectId(\\d+)/estimations/:estimationId(\\d+)/items') From 21edae949ee67c31c4bbd91c736ec0667d60ed53 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Sun, 4 May 2025 10:16:53 +0200 Subject: [PATCH 04/10] updated permission method --- src/permissions/copilotApplications.view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/permissions/copilotApplications.view.js b/src/permissions/copilotApplications.view.js index 0fb14bf4..f40167dc 100644 --- a/src/permissions/copilotApplications.view.js +++ b/src/permissions/copilotApplications.view.js @@ -13,7 +13,7 @@ module.exports = freq => new Promise((resolve, reject) => { console.log("start permission check"); const opportunityId = _.parseInt(freq.params.id); const currentUserId = freq.authUser.userId; - return models.CopilotOpportunity.find({ + return models.CopilotOpportunity.findOne({ where: { id: opportunityId, }, From 840f1555ad44c6053425f414d1c4d8bfd9f707fd Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Sun, 4 May 2025 11:20:17 +0200 Subject: [PATCH 05/10] updated permission method --- src/permissions/copilotApplications.view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/permissions/copilotApplications.view.js b/src/permissions/copilotApplications.view.js index f40167dc..fd35e539 100644 --- a/src/permissions/copilotApplications.view.js +++ b/src/permissions/copilotApplications.view.js @@ -30,7 +30,7 @@ module.exports = freq => new Promise((resolve, reject) => { .then((members) => { console.log("got active members", projectId); - return models.CopilotApplications.findOne({ + return models.CopilotApplication.findOne({ where: { opportunityId: opportunityId, userId: currentUserId, From d093fe5e853e2d3af45c2d5c45edc51be16331d2 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Sun, 4 May 2025 12:01:09 +0200 Subject: [PATCH 06/10] updated permission method --- src/routes/copilotOpportunityApply/list.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/routes/copilotOpportunityApply/list.js b/src/routes/copilotOpportunityApply/list.js index b7bb0cc7..0348d3e0 100644 --- a/src/routes/copilotOpportunityApply/list.js +++ b/src/routes/copilotOpportunityApply/list.js @@ -13,6 +13,7 @@ module.exports = [ console.log("start list operation"); const isAdmin = util.hasRoles(req, ADMIN_ROLES); + const userId = req.authUser.userId; let sort = req.query.sort ? decodeURIComponent(req.query.sort) : 'createdAt desc'; if (sort.indexOf(' ') === -1) { From 84117313266e715a74fc5fcb0bbe563de06619e8 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Sun, 4 May 2025 22:11:25 +0200 Subject: [PATCH 07/10] added opp id to the where clause --- src/routes/copilotOpportunityApply/list.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/routes/copilotOpportunityApply/list.js b/src/routes/copilotOpportunityApply/list.js index 0348d3e0..0c0e7fe8 100644 --- a/src/routes/copilotOpportunityApply/list.js +++ b/src/routes/copilotOpportunityApply/list.js @@ -14,6 +14,7 @@ module.exports = [ console.log("start list operation"); const isAdmin = util.hasRoles(req, ADMIN_ROLES); const userId = req.authUser.userId; + const opportunityId = _.parseInt(req.params.id); let sort = req.query.sort ? decodeURIComponent(req.query.sort) : 'createdAt desc'; if (sort.indexOf(' ') === -1) { @@ -26,7 +27,9 @@ module.exports = [ const sortParams = sort.split(' '); // Admin can see all requests and the PM can only see requests created by them - const whereCondition = _.assign({}, + const whereCondition = _.assign({ + opportunityId, + }, isAdmin ? {} : { createdBy: userId }, ); From 43dc20c3d1af53700c537d8802779ba785831fa3 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Sun, 4 May 2025 23:57:44 +0200 Subject: [PATCH 08/10] show all applications for pm --- src/permissions/copilotApplications.view.js | 6 +----- src/routes/copilotOpportunityApply/list.js | 5 ++--- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/permissions/copilotApplications.view.js b/src/permissions/copilotApplications.view.js index fd35e539..9b0c917b 100644 --- a/src/permissions/copilotApplications.view.js +++ b/src/permissions/copilotApplications.view.js @@ -10,7 +10,6 @@ import models from '../models'; * @return {Promise} Returns a promise */ module.exports = freq => new Promise((resolve, reject) => { - console.log("start permission check"); const opportunityId = _.parseInt(freq.params.id); const currentUserId = freq.authUser.userId; return models.CopilotOpportunity.findOne({ @@ -25,11 +24,9 @@ module.exports = freq => new Promise((resolve, reject) => { const projectId = opportunity.projectId; const isProjectManager = util.hasProjectManagerRole(req); - console.log("got opportunity", opportunityId); return models.ProjectMember.getActiveProjectMembers(projectId) .then((members) => { - console.log("got active members", projectId); return models.CopilotApplication.findOne({ where: { opportunityId: opportunityId, @@ -37,9 +34,8 @@ module.exports = freq => new Promise((resolve, reject) => { }, }).then((copilotApplication) => { const isPartOfProject = isProjectManager && members.find(member => member.userId === currentUserId); - // check if auth user has acecss to this project + // check if auth user has access to this project const hasAccess = util.hasAdminRole(req) || isPartOfProject || !!copilotApplication; - console.log("got assigned application", hasAccess); return Promise.resolve(hasAccess); }) }) diff --git a/src/routes/copilotOpportunityApply/list.js b/src/routes/copilotOpportunityApply/list.js index 0c0e7fe8..80786aef 100644 --- a/src/routes/copilotOpportunityApply/list.js +++ b/src/routes/copilotOpportunityApply/list.js @@ -11,8 +11,7 @@ module.exports = [ permissions('copilotApplications.view'), (req, res, next) => { - console.log("start list operation"); - const isAdmin = util.hasRoles(req, ADMIN_ROLES); + const canAccessAllApplications = util.hasRoles(req, ADMIN_ROLES) || util.hasProjectManagerRole(req); const userId = req.authUser.userId; const opportunityId = _.parseInt(req.params.id); @@ -30,7 +29,7 @@ module.exports = [ const whereCondition = _.assign({ opportunityId, }, - isAdmin ? {} : { createdBy: userId }, + canAccessAllApplications ? {} : { createdBy: userId }, ); return models.CopilotApplication.findAll({ From c42c1d430dfb44e0a6655ef63aaa02236819c2e2 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Mon, 5 May 2025 15:42:03 +0200 Subject: [PATCH 09/10] fix: allow only copilots to apply for opportunity --- src/constants.js | 1 + src/permissions/constants.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/constants.js b/src/constants.js index 77bd3b20..8307b773 100644 --- a/src/constants.js +++ b/src/constants.js @@ -90,6 +90,7 @@ export const USER_ROLE = { PROJECT_MANAGER: 'Project Manager', TOPCODER_USER: 'Topcoder User', TG_ADMIN: 'tgadmin', + TC_COPILOT: 'copilot', }; export const ADMIN_ROLES = [USER_ROLE.CONNECT_ADMIN, USER_ROLE.TOPCODER_ADMIN, USER_ROLE.TG_ADMIN]; diff --git a/src/permissions/constants.js b/src/permissions/constants.js index 85c38465..03bcaf21 100644 --- a/src/permissions/constants.js +++ b/src/permissions/constants.js @@ -272,7 +272,7 @@ export const PERMISSION = { // eslint-disable-line import/prefer-default-export description: 'Who can apply for copilot opportunity.', }, topcoderRoles: [ - USER_ROLE.COPILOT, + USER_ROLE.TC_COPILOT, ], scopes: SCOPES_PROJECTS_WRITE, }, From 7adb0ccf7e86fbad01b3df8aae1138bbe64679a7 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Mon, 5 May 2025 16:16:13 +0200 Subject: [PATCH 10/10] removed dev deployment circle config --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 8e10e3e6..1acd4a4c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -149,7 +149,7 @@ workflows: context : org-global filters: branches: - only: ['develop', 'migration-setup', 'pm-855'] + only: ['develop', 'migration-setup'] - deployProd: context : org-global filters: