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
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
1 change: 1 addition & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
17 changes: 16 additions & 1 deletion src/permissions/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
51 changes: 51 additions & 0 deletions src/permissions/copilotApplications.view.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
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);
};
50 changes: 50 additions & 0 deletions src/routes/copilotOpportunityApply/list.js
Original file line number Diff line number Diff line change
@@ -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);
});
},
];
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