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/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 4395021e..03bcaf21 100644 --- a/src/permissions/constants.js +++ b/src/permissions/constants.js @@ -272,7 +272,22 @@ 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, + }, + + 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, }, diff --git a/src/permissions/copilotApplications.view.js b/src/permissions/copilotApplications.view.js new file mode 100644 index 00000000..9b0c917b --- /dev/null +++ b/src/permissions/copilotApplications.view.js @@ -0,0 +1,51 @@ + +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) => { + const opportunityId = _.parseInt(freq.params.id); + const currentUserId = freq.authUser.userId; + return models.CopilotOpportunity.findOne({ + 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); + + return models.ProjectMember.getActiveProjectMembers(projectId) + .then((members) => { + + return models.CopilotApplication.findOne({ + where: { + opportunityId: opportunityId, + userId: currentUserId, + }, + }).then((copilotApplication) => { + const isPartOfProject = isProjectManager && members.find(member => member.userId === currentUserId); + // check if auth user has access to this project + const hasAccess = util.hasAdminRole(req) || isPartOfProject || !!copilotApplication; + 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..80786aef --- /dev/null +++ b/src/routes/copilotOpportunityApply/list.js @@ -0,0 +1,50 @@ +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) => { + + const canAccessAllApplications = util.hasRoles(req, ADMIN_ROLES) || util.hasProjectManagerRole(req); + 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) { + 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({ + opportunityId, + }, + canAccessAllApplications ? {} : { 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..b07041ab 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') + .get(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