Skip to content

Commit 2d32e94

Browse files
authored
Merge pull request #427 from topcoder-platform/dev
Production release 2.4.16
2 parents e215e9c + 30d1769 commit 2d32e94

22 files changed

+578
-47
lines changed

config/custom-environment-variables.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,5 +61,6 @@
6161
"REG_STATS": "LOOKER_API_REG_STATS_QUERY_ID",
6262
"BUDGET": "LOOKER_API_BUDGET_QUERY_ID"
6363
}
64-
}
64+
},
65+
"DEFAULT_M2M_USERID": "DEFAULT_M2M_USERID"
6566
}

config/default.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,5 +71,6 @@
7171
"REG_STATS": 1234,
7272
"BUDGET": 123
7373
}
74-
}
74+
},
75+
"DEFAULT_M2M_USERID": -101
7576
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
"pg": "^4.5.5",
6363
"pg-native": "^1.10.1",
6464
"sequelize": "^3.23.0",
65+
"jsonpath": "^1.0.2",
6566
"tc-core-library-js": "appirio-tech/tc-core-library-js.git#v2.6",
6667
"traverse": "^0.6.6",
6768
"urlencode": "^1.1.0"

src/models/project.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable valid-jsdoc */
22

33
import _ from 'lodash';
4-
import { PROJECT_STATUS } from '../constants';
4+
import { PROJECT_STATUS, INVITE_STATUS } from '../constants';
55

66
module.exports = function defineProject(sequelize, DataTypes) {
77
const Project = sequelize.define('Project', {
@@ -76,7 +76,7 @@ module.exports = function defineProject(sequelize, DataTypes) {
7676
Project.hasMany(models.ProjectMember, { as: 'members', foreignKey: 'projectId' });
7777
Project.hasMany(models.ProjectAttachment, { as: 'attachments', foreignKey: 'projectId' });
7878
Project.hasMany(models.ProjectPhase, { as: 'phases', foreignKey: 'projectId' });
79-
Project.hasMany(models.ProjectMemberInvite, { as: 'memberInvites', foreignKey: 'projectId' });
79+
Project.hasMany(models.ProjectMemberInvite, { as: 'invites', foreignKey: 'projectId' });
8080
Project.hasMany(models.ScopeChangeRequest, { as: 'scopeChangeRequests', foreignKey: 'projectId' });
8181
Project.hasMany(models.WorkStream, { as: 'workStreams', foreignKey: 'projectId' });
8282
},
@@ -188,6 +188,10 @@ module.exports = function defineProject(sequelize, DataTypes) {
188188
model: models.PhaseProduct,
189189
as: 'products',
190190
}],
191+
}, {
192+
model: models.ProjectMemberInvite,
193+
as: 'invites',
194+
where: { status: INVITE_STATUS.PENDING },
191195
}],
192196
});
193197
},

src/permissions/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ module.exports = () => {
2121
Authorizer.setPolicy('project.view', projectView);
2222
Authorizer.setPolicy('project.edit', projectEdit);
2323
Authorizer.setPolicy('project.delete', projectDelete);
24+
Authorizer.setPolicy('project.getMember', projectView);
2425
Authorizer.setPolicy('project.addMember', projectView);
26+
Authorizer.setPolicy('project.listMembers', projectView);
2527
Authorizer.setPolicy('project.removeMember', projectMemberDelete);
2628
Authorizer.setPolicy('project.addAttachment', projectEdit);
2729
Authorizer.setPolicy('project.updateAttachment', projectAttachmentUpdate);
@@ -85,6 +87,7 @@ module.exports = () => {
8587
Authorizer.setPolicy('projectMemberInvite.create', projectView);
8688
Authorizer.setPolicy('projectMemberInvite.put', true);
8789
Authorizer.setPolicy('projectMemberInvite.get', true);
90+
Authorizer.setPolicy('projectMemberInvite.list', projectView);
8891

8992
Authorizer.setPolicy('form.create', projectAdmin);
9093
Authorizer.setPolicy('form.edit', projectAdmin);

src/routes/index.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,18 @@ router.all(
3434
),
3535
);
3636

37+
router.all(
38+
RegExp(`\\/${apiVersion}\\/.*`), (req, res, next) => {
39+
// if it is an M2M call, hard code user id to a deafult value to avoid errors
40+
// Ideally, the m2m token should have unique userId, which may not be an actual user, as well
41+
const isMachineToken = _.get(req, 'authUser.isMachine', false);
42+
if (req.authUser && !req.authUser.userId && isMachineToken) {
43+
req.authUser.userId = config.DEFAULT_M2M_USERID;
44+
}
45+
return next();
46+
},
47+
);
48+
3749
router.route('/v4/projects/metadata/projectTemplates')
3850
.get(require('./projectTemplates/list'));
3951
router.route('/v4/projects/metadata/projectTemplates/:templateId(\\d+)')
@@ -107,9 +119,11 @@ router.route('/v4/projects/:projectId(\\d+)/scopeChangeRequests/:requestId(\\d+)
107119
// .delete(require('./scopeChangeRequests/delete'));
108120

109121
router.route('/v4/projects/:projectId(\\d+)/members')
122+
.get(require('./projectMembers/list'))
110123
.post(require('./projectMembers/create'));
111124

112125
router.route('/v4/projects/:projectId(\\d+)/members/:id(\\d+)')
126+
.get(require('./projectMembers/get'))
113127
.delete(require('./projectMembers/delete'))
114128
.patch(require('./projectMembers/update'));
115129

@@ -217,6 +231,9 @@ router.route('/v4/timelines/metadata/milestoneTemplates/:milestoneTemplateId(\\d
217231
.patch(require('./milestoneTemplates/update'))
218232
.delete(require('./milestoneTemplates/delete'));
219233

234+
router.route('/v4/projects/:projectId(\\d+)/members/invites')
235+
.get(require('./projectMemberInvites/list'));
236+
220237
router.route('/v4/projects/:projectId(\\d+)/members/invite')
221238
.post(require('./projectMemberInvites/create'))
222239
.put(require('./projectMemberInvites/update'))

src/routes/projectMemberInvites/create.js

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11

2-
32
import validate from 'express-validation';
43
import _ from 'lodash';
54
import Joi from 'joi';
@@ -235,6 +234,8 @@ module.exports = [
235234
(req, res, next) => {
236235
let failed = [];
237236
const invite = req.body.param;
237+
// let us request user fields during creating, probably this should be move to GET by ID endpoint instead
238+
const fields = req.query.fields ? req.query.fields.split(',') : null;
238239

239240
if (!invite.userIds && !invite.emails) {
240241
const err = new Error('Either userIds or emails are required');
@@ -340,16 +341,26 @@ module.exports = [
340341
sendInviteEmail(req, projectId, v);
341342
}
342343
});
343-
return values;
344+
return values.map(value => value.get({ plain: true }));
344345
}); // models.sequelize.Promise.all
345346
}); // models.ProjectMemberInvite.getPendingInvitesForProject
346347
})
348+
.then(values => (
349+
// populate successful invites with user details if required
350+
util.getObjectsWithMemberDetails(values, fields, req)
351+
.catch((err) => {
352+
req.log.error('Cannot get user details for invites.');
353+
req.log.debug('Error during getting user details for invites', err);
354+
})
355+
))
347356
.then((values) => {
348357
const success = _.assign({}, { success: values });
349358
if (failed.length) {
350-
res.status(403).json(util.wrapResponse(req.id, _.assign({}, success, { failed }), null, 403));
359+
res.status(403).json(util.wrapResponse(req.id,
360+
util.maskInviteEmails('$.email', _.assign({}, success, { failed }), req), null, 403));
351361
} else {
352-
res.status(201).json(util.wrapResponse(req.id, success, null, 201));
362+
res.status(201).json(util.wrapResponse(req.id,
363+
util.maskInviteEmails('$.success[?(@.email)]', success, req), null, 201));
353364
}
354365
})
355366
.catch(err => next(err));

src/routes/projectMemberInvites/create.spec.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,8 @@ describe('Project Member Invite create', () => {
353353
should.exist(resJson);
354354
resJson.role.should.equal('customer');
355355
resJson.projectId.should.equal(project2.id);
356+
// temporary disable this feature, because it has some side effects
357+
// resJson.email.should.equal('he**o@wo**d.com'); // email is masked
356358
resJson.email.should.equal('[email protected]');
357359
server.services.pubsub.publish.calledWith('project.member.invite.created').should.be.true;
358360
done();
@@ -407,6 +409,8 @@ describe('Project Member Invite create', () => {
407409
resJson.role.should.equal('customer');
408410
resJson.projectId.should.equal(project2.id);
409411
resJson.userId.should.equal(12345);
412+
// temporary disable this feature, because it has some side effects
413+
// resJson.email.should.equal('he**o@wo**d.com'); // email is masked
410414
resJson.email.should.equal('[email protected]');
411415
server.services.pubsub.publish.calledWith('project.member.invite.created').should.be.true;
412416
done();
@@ -848,7 +852,7 @@ describe('Project Member Invite create', () => {
848852
} else {
849853
const resJson = res.body.result.content.failed;
850854
should.exist(resJson);
851-
resJson[0].email.should.equal('[email protected]');
855+
resJson[0].email.should.equal('[email protected]'); // email is masked
852856
resJson[0].message.should.equal('User with such email is already invited to this project.');
853857
resJson.length.should.equal(1);
854858
done();
Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
2-
31
import _ from 'lodash';
2+
import Joi from 'joi';
3+
import validate from 'express-validation';
44
import { middleware as tcMiddleware } from 'tc-core-library-js';
55
import models from '../../models';
66
import util from '../../util';
@@ -9,27 +9,50 @@ import util from '../../util';
99
* API to update invite member to project.
1010
*
1111
*/
12+
const schema = {
13+
query: {
14+
fields: Joi.string().optional(),
15+
},
16+
};
1217
const permissions = tcMiddleware.permissions;
1318

1419
module.exports = [
15-
// handles request validations
20+
validate(schema),
1621
permissions('projectMemberInvite.get'),
17-
(req, res, next) => {
18-
const projectId = _.parseInt(req.params.projectId);
19-
const currentUserId = req.authUser.userId;
20-
let invite;
21-
return models.ProjectMemberInvite.getPendingInviteByEmailOrUserId(projectId, req.authUser.email, currentUserId)
22-
.then((_invite) => {
23-
invite = _invite;
24-
if (!invite) {
25-
// check there is an existing invite for the user with status PENDING
26-
// handle 404
27-
const err = new Error('invite not found for project id ' +
28-
`${projectId}, userId ${currentUserId}, email ${req.authUser.email}`);
29-
err.status = 404;
30-
return next(err);
31-
}
32-
return res.json(util.wrapResponse(req.id, invite));
33-
});
22+
async (req, res, next) => {
23+
try {
24+
const projectId = _.parseInt(req.params.projectId);
25+
const currentUserId = req.authUser.userId;
26+
const invite = await models.ProjectMemberInvite.getPendingInviteByEmailOrUserId(
27+
projectId, req.authUser.email, currentUserId,
28+
);
29+
if (!invite) {
30+
// check there is an existing invite for the user with status PENDING
31+
// handle 404
32+
const err = new Error(
33+
'invite not found for project id ' +
34+
`${projectId}, userId ${currentUserId}, email ${req.authUser.email}`,
35+
);
36+
err.status = 404;
37+
throw err;
38+
}
39+
40+
let fields = null;
41+
if (req.query.fields) {
42+
fields = req.query.fields.split(',');
43+
}
44+
let inviteWithDetails;
45+
try {
46+
[inviteWithDetails] = await util.getObjectsWithMemberDetails([invite], fields, req);
47+
} catch (err) {
48+
inviteWithDetails = invite;
49+
req.log.error('Cannot get user details for invite.');
50+
req.log.debug('Error during getting user details for invite.', err);
51+
}
52+
53+
return res.json(util.wrapResponse(req.id, inviteWithDetails));
54+
} catch (err) {
55+
return next(err);
56+
}
3457
},
3558
];
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import _ from 'lodash';
2+
import Joi from 'joi';
3+
import validate from 'express-validation';
4+
import { middleware as tcMiddleware } from 'tc-core-library-js';
5+
import models from '../../models';
6+
import util from '../../util';
7+
8+
/**
9+
* API to list all project member invites.
10+
*
11+
*/
12+
const permissions = tcMiddleware.permissions;
13+
14+
const schema = {
15+
query: {
16+
fields: Joi.string().optional(),
17+
},
18+
};
19+
20+
module.exports = [
21+
validate(schema),
22+
permissions('projectMemberInvite.list'),
23+
async (req, res, next) => {
24+
try {
25+
let fields = null;
26+
if (req.query.fields) {
27+
fields = req.query.fields.split(',');
28+
}
29+
const projectId = _.parseInt(req.params.projectId);
30+
const invites = await models.ProjectMemberInvite.getPendingAndReguestedInvitesForProject(projectId);
31+
let invitesWithDetails;
32+
try {
33+
invitesWithDetails = await util.getObjectsWithMemberDetails(invites, fields, req);
34+
} catch (err) {
35+
invitesWithDetails = invites;
36+
req.log.error('Cannot get user details for invites.');
37+
req.log.debug('Error during getting user details for invites', err);
38+
}
39+
return res.json(util.wrapResponse(req.id, util.maskInviteEmails('$[*].email', invitesWithDetails, req)));
40+
} catch (err) {
41+
return next(err);
42+
}
43+
},
44+
];

src/routes/projectMemberInvites/update.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,11 +135,13 @@ module.exports = [
135135
};
136136
return util
137137
.addUserToProject(req, member)
138-
.then(() => res.json(util.wrapResponse(req.id, updatedInvite)))
138+
.then(() => res.json(util.wrapResponse(req.id,
139+
util.maskInviteEmails('$.email', updatedInvite, req))))
139140
.catch(err => next(err));
140141
});
141142
}
142-
return res.json(util.wrapResponse(req.id, updatedInvite));
143+
144+
return res.json(util.wrapResponse(req.id, util.maskInviteEmails('$.email', updatedInvite, req)));
143145
});
144146
});
145147
},

src/routes/projectMembers/get.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
2+
3+
import _ from 'lodash';
4+
import Joi from 'joi';
5+
import validate from 'express-validation';
6+
import { middleware as tcMiddleware } from 'tc-core-library-js';
7+
import util from '../../util';
8+
9+
/**
10+
* API to get a project member in a project.
11+
*/
12+
const permissions = tcMiddleware.permissions;
13+
14+
const schema = {
15+
query: {
16+
fields: Joi.string().optional(),
17+
},
18+
};
19+
20+
module.exports = [
21+
validate(schema),
22+
permissions('project.getMember'),
23+
async (req, res, next) => {
24+
try {
25+
const projectId = _.parseInt(req.params.projectId);
26+
let fields = null;
27+
if (req.query.fields) {
28+
fields = req.query.fields.split(',');
29+
}
30+
const memberId = _.parseInt(req.params.id);
31+
const member = _.find(req.context.currentProjectMembers, user => user.id === memberId);
32+
if (!member) {
33+
const err = new Error(
34+
`member not found for project id ${projectId}, userId ${memberId}`,
35+
);
36+
err.status = 404;
37+
throw err;
38+
}
39+
40+
let memberWithDetails;
41+
try {
42+
[memberWithDetails] = await util.getObjectsWithMemberDetails([member], fields, req);
43+
} catch (err) {
44+
memberWithDetails = member;
45+
req.log.error('Cannot get user details for the member.');
46+
req.log.debug('Error during getting user details for member.', err);
47+
}
48+
49+
return res.json(util.wrapResponse(req.id, memberWithDetails));
50+
} catch (err) {
51+
return next(err);
52+
}
53+
},
54+
];

0 commit comments

Comments
 (0)