Skip to content

Commit 51c50d8

Browse files
authored
Merge pull request #803 from topcoder-platform/pm-855
feat(PM-855): List copilot applications
2 parents 0a5d052 + 7adb0cc commit 51c50d8

File tree

8 files changed

+199
-1
lines changed

8 files changed

+199
-1
lines changed

docs/swagger.yaml

+58
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,64 @@ paths:
502502
description: "Internal Server Error"
503503
schema:
504504
$ref: "#/definitions/ErrorModel"
505+
"/projects/copilots/opportunity/{copilotOpportunityId}/applications":
506+
get:
507+
tags:
508+
- projects copilot opportunity applications
509+
operationId: listCopilotOpportunity
510+
security:
511+
- Bearer: []
512+
description: "Retrieve the list copilot opportunity applications."
513+
parameters:
514+
- $ref: "#/parameters/copilotOpportunityIdParam"
515+
- name: sort
516+
required: false
517+
description: >
518+
sort projects by createdAt, updatedAt. Default
519+
is createdAt asc
520+
in: query
521+
type: string
522+
responses:
523+
"200":
524+
description: A list of projects
525+
schema:
526+
type: array
527+
items:
528+
$ref: "#/definitions/CopilotOpportunityApplication"
529+
headers:
530+
X-Next-Page:
531+
type: integer
532+
description: The index of the next page
533+
X-Page:
534+
type: integer
535+
description: The index of the current page (starting at 1)
536+
X-Per-Page:
537+
type: integer
538+
description: The number of items to list per page
539+
X-Prev-Page:
540+
type: integer
541+
description: The index of the previous page
542+
X-Total:
543+
type: integer
544+
description: The total number of items
545+
X-Total-Pages:
546+
type: integer
547+
description: The total number of pages
548+
Link:
549+
type: string
550+
description: Pagination link header.
551+
"401":
552+
description: "Unauthorized"
553+
schema:
554+
$ref: "#/definitions/ErrorModel"
555+
"403":
556+
description: "Forbidden - User does not have permission"
557+
schema:
558+
$ref: "#/definitions/ErrorModel"
559+
"500":
560+
description: "Internal Server Error"
561+
schema:
562+
$ref: "#/definitions/ErrorModel"
505563
"/projects/{projectId}/attachments":
506564
get:
507565
tags:

src/constants.js

+1
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export const USER_ROLE = {
9090
PROJECT_MANAGER: 'Project Manager',
9191
TOPCODER_USER: 'Topcoder User',
9292
TG_ADMIN: 'tgadmin',
93+
TC_COPILOT: 'copilot',
9394
};
9495

9596
export const ADMIN_ROLES = [USER_ROLE.CONNECT_ADMIN, USER_ROLE.TOPCODER_ADMIN, USER_ROLE.TG_ADMIN];

src/permissions/constants.js

+16-1
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,22 @@ export const PERMISSION = { // eslint-disable-line import/prefer-default-export
272272
description: 'Who can apply for copilot opportunity.',
273273
},
274274
topcoderRoles: [
275-
USER_ROLE.COPILOT,
275+
USER_ROLE.TC_COPILOT,
276+
],
277+
scopes: SCOPES_PROJECTS_WRITE,
278+
},
279+
280+
LIST_COPILOT_OPPORTUNITY: {
281+
meta: {
282+
title: 'Apply copilot opportunity',
283+
group: 'Apply Copilot',
284+
description: 'Who can apply for copilot opportunity.',
285+
},
286+
topcoderRoles: [
287+
USER_ROLE.TOPCODER_ADMIN,
288+
],
289+
projectRoles: [
290+
USER_ROLE.PROJECT_MANAGER,
276291
],
277292
scopes: SCOPES_PROJECTS_WRITE,
278293
},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
2+
import _ from 'lodash';
3+
import util from '../util';
4+
import models from '../models';
5+
6+
/**
7+
* Topcoder admin and Project managers who are part of the project can view the copilot applications in it
8+
* Also, users who had an application will have access to view it.
9+
* @param {Object} freq the express request instance
10+
* @return {Promise} Returns a promise
11+
*/
12+
module.exports = freq => new Promise((resolve, reject) => {
13+
const opportunityId = _.parseInt(freq.params.id);
14+
const currentUserId = freq.authUser.userId;
15+
return models.CopilotOpportunity.findOne({
16+
where: {
17+
id: opportunityId,
18+
},
19+
})
20+
.then((opportunity) => {
21+
const req = freq;
22+
req.context = req.context || {};
23+
req.context.currentOpportunity = opportunity;
24+
const projectId = opportunity.projectId;
25+
const isProjectManager = util.hasProjectManagerRole(req);
26+
27+
return models.ProjectMember.getActiveProjectMembers(projectId)
28+
.then((members) => {
29+
30+
return models.CopilotApplication.findOne({
31+
where: {
32+
opportunityId: opportunityId,
33+
userId: currentUserId,
34+
},
35+
}).then((copilotApplication) => {
36+
const isPartOfProject = isProjectManager && members.find(member => member.userId === currentUserId);
37+
// check if auth user has access to this project
38+
const hasAccess = util.hasAdminRole(req) || isPartOfProject || !!copilotApplication;
39+
return Promise.resolve(hasAccess);
40+
})
41+
})
42+
})
43+
.then((hasAccess) => {
44+
if (!hasAccess) {
45+
const errorMessage = 'You do not have permissions to perform this action';
46+
// user is not an admin nor is a registered project member
47+
return reject(new Error(errorMessage));
48+
}
49+
return resolve(true);
50+
});
51+
});

src/permissions/index.js

+4
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const copilotAndAbove = require('./copilotAndAbove');
99
const workManagementPermissions = require('./workManagementForTemplate');
1010
const projectSettingEdit = require('./projectSetting.edit');
1111
const customerPaymentConfirm = require('./customerPayment.confirm');
12+
const viewCopilotApplications = require('./copilotApplications.view');
1213

1314
const generalPermission = require('./generalPermission');
1415
const { PERMISSION } = require('./constants');
@@ -199,4 +200,7 @@ module.exports = () => {
199200
Authorizer.setPolicy('customerPayment.view', generalPermission(PERMISSION.VIEW_CUSTOMER_PAYMENT));
200201
Authorizer.setPolicy('customerPayment.edit', generalPermission(PERMISSION.UPDATE_CUSTOMER_PAYMENT));
201202
Authorizer.setPolicy('customerPayment.confirm', customerPaymentConfirm);
203+
204+
// Copilot opportunity
205+
Authorizer.setPolicy('copilotApplications.view', viewCopilotApplications);
202206
};
+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import _ from 'lodash';
2+
import { middleware as tcMiddleware } from 'tc-core-library-js';
3+
4+
import models from '../../models';
5+
import { ADMIN_ROLES } from '../../constants';
6+
import util from '../../util';
7+
8+
const permissions = tcMiddleware.permissions;
9+
10+
module.exports = [
11+
permissions('copilotApplications.view'),
12+
(req, res, next) => {
13+
14+
const canAccessAllApplications = util.hasRoles(req, ADMIN_ROLES) || util.hasProjectManagerRole(req);
15+
const userId = req.authUser.userId;
16+
const opportunityId = _.parseInt(req.params.id);
17+
18+
let sort = req.query.sort ? decodeURIComponent(req.query.sort) : 'createdAt desc';
19+
if (sort.indexOf(' ') === -1) {
20+
sort += ' asc';
21+
}
22+
const sortableProps = ['createdAt asc', 'createdAt desc'];
23+
if (_.indexOf(sortableProps, sort) < 0) {
24+
return util.handleError('Invalid sort criteria', null, req, next);
25+
}
26+
const sortParams = sort.split(' ');
27+
28+
// Admin can see all requests and the PM can only see requests created by them
29+
const whereCondition = _.assign({
30+
opportunityId,
31+
},
32+
canAccessAllApplications ? {} : { createdBy: userId },
33+
);
34+
35+
return models.CopilotApplication.findAll({
36+
where: whereCondition,
37+
include: [
38+
{
39+
model: models.CopilotOpportunity,
40+
as: 'copilotOpportunity',
41+
},
42+
],
43+
order: [[sortParams[0], sortParams[1]]],
44+
})
45+
.then(copilotApplications => res.json(copilotApplications))
46+
.catch((err) => {
47+
util.handleError('Error fetching copilot applications', err, req, next);
48+
});
49+
},
50+
];

src/routes/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,8 @@ router.route('/v5/projects/copilot/opportunity/:id(\\d+)')
407407
// Project copilot opportunity apply
408408
router.route('/v5/projects/copilots/opportunity/:id(\\d+)/apply')
409409
.post(require('./copilotOpportunityApply/create'));
410+
router.route('/v5/projects/copilots/opportunity/:id(\\d+)/applications')
411+
.get(require('./copilotOpportunityApply/list'));
410412

411413
// Project Estimation Items
412414
router.route('/v5/projects/:projectId(\\d+)/estimations/:estimationId(\\d+)/items')

src/util.js

+17
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,23 @@ const projectServiceUtils = {
225225
return _.intersection(roles, ADMIN_ROLES.map(r => r.toLowerCase())).length > 0;
226226
},
227227

228+
/**
229+
* Helper funtion to verify if user has project manager role
230+
* @param {object} req Request object that should contain authUser
231+
* @return {boolean} true/false
232+
*/
233+
hasProjectManagerRole: (req) => {
234+
const isMachineToken = _.get(req, 'authUser.isMachine', false);
235+
const tokenScopes = _.get(req, 'authUser.scopes', []);
236+
if (isMachineToken) {
237+
if (_.indexOf(tokenScopes, M2M_SCOPES.CONNECT_PROJECT_ADMIN) >= 0) return true;
238+
return false;
239+
}
240+
let roles = _.get(req, 'authUser.roles', []);
241+
roles = roles.map(s => s.toLowerCase());
242+
return roles.includes(USER_ROLE.PROJECT_MANAGER.toLowerCase());
243+
},
244+
228245
/**
229246
* Parses query fields and groups them per table
230247
* @param {array} queryFields list of query fields

0 commit comments

Comments
 (0)