Skip to content

feat(PM-855): List copilot applications #803

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
May 5, 2025
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
58 changes: 58 additions & 0 deletions docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
15 changes: 15 additions & 0 deletions src/permissions/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"',
Expand Down
55 changes: 55 additions & 0 deletions src/permissions/copilotApplications.view.js
Original file line number Diff line number Diff line change
@@ -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.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);

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,
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);
});
});
4 changes: 4 additions & 0 deletions src/permissions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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);
};
51 changes: 51 additions & 0 deletions src/routes/copilotOpportunityApply/list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
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);
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,
},
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);
});
},
];
2 changes: 2 additions & 0 deletions src/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
17 changes: 17 additions & 0 deletions src/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down