Skip to content

Production release 2.4.16 #427

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 38 commits into from
Dec 17, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
60f86ea
Allow only admins to update milestone dates
r0hit-gupta Sep 19, 2019
893aff0
update tests
r0hit-gupta Sep 29, 2019
b8f8a41
update tests
r0hit-gupta Sep 30, 2019
1cee66e
Merge pull request #382 from r0hit-gupta/issue-381-fix
Oct 3, 2019
23a21b6
Fixing userId to a hard coded value for m2m calls
Nov 1, 2019
dddd930
fix: issues during updating milestone completionDate
maxceem Nov 5, 2019
14b34b8
fix: try to fix "projectUpdatedKafkaHandler" unit test
maxceem Nov 7, 2019
5210d46
Revert "fix: try to fix "projectUpdatedKafkaHandler" unit test"
maxceem Nov 7, 2019
a563a50
fix: "projectUpdatedKafkaHandler" unit test
maxceem Nov 7, 2019
af2bf9e
Merge pull request #1 from topcoder-platform/dev
PrakashDurlabhji Nov 8, 2019
f04ea8b
f2f avoid draft status
PrakashDurlabhji Nov 8, 2019
9839c0e
Merge pull request #408 from PrakashDurlabhji/dev
maxceem Nov 11, 2019
9bff750
feat: project members endpoint with additional fields
maxceem Nov 24, 2019
591b5b3
refactor: project member list endpoint naming things
maxceem Nov 26, 2019
e443142
fix: renamed workingHourStart/End as per Member Service
maxceem Nov 26, 2019
92c43a2
feat: project member & invites endpoint with additional fields
Nov 28, 2019
9747253
Fix additional member field values
Dec 1, 2019
c44ce33
Project Member & Invite endpoint fixes
Dec 2, 2019
78b969f
Merge pull request #414 from dpkshrma/feature/project-invites-traits
maxceem Dec 3, 2019
84ab28f
fix: admin endpoint to reindex project with "invites"
maxceem Dec 4, 2019
8313e2e
Mask invitation emails
yoution Dec 3, 2019
8423d42
Merge pull request #418 from yoution/feature/addemailmask
maxceem Dec 4, 2019
a904eaa
fix: units test after masking invitation emails
maxceem Dec 5, 2019
f9c323e
fix: units test after masking invitation emails, part 2
maxceem Dec 5, 2019
8fdc588
fix: jsonpathes for masking emails to avoid maximum stack call issues
maxceem Dec 5, 2019
2f0f598
fix: jsonpathes for masking emails to avoid maximum stack call issues…
maxceem Dec 5, 2019
26d6d89
fix: still return members and invites if getting user details from Me…
maxceem Dec 6, 2019
350896d
fix: return invites with status "requested" by LIST project invites e…
maxceem Dec 10, 2019
5611565
fix: error during requesting user details for invites without userId
maxceem Dec 10, 2019
b1a7346
fix: improve stability of populating member/invite details
maxceem Dec 10, 2019
9e528d4
feat: temporary disable masking emails inside invites
maxceem Dec 13, 2019
f61687c
feat: temporary disable masking emails inside invites, part 2
maxceem Dec 13, 2019
f5a59c2
feat: create invite endpoint support returning additional fields
maxceem Dec 14, 2019
9f4a2a5
fix: create invite endpoint support returning additional fields
maxceem Dec 14, 2019
1751a57
Merge pull request #424 from topcoder-platform/hotfix/milestone-compl…
RishiRajSahu Dec 16, 2019
b0521a8
Merge pull request #425 from topcoder-platform/hotfix/es-skills
RishiRajSahu Dec 16, 2019
f68101f
feat: populate user details in update member endpoint
maxceem Dec 17, 2019
30d1769
Merge branch 'master' into dev
Dec 17, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion config/custom-environment-variables.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,6 @@
"REG_STATS": "LOOKER_API_REG_STATS_QUERY_ID",
"BUDGET": "LOOKER_API_BUDGET_QUERY_ID"
}
}
},
"DEFAULT_M2M_USERID": "DEFAULT_M2M_USERID"
}
3 changes: 2 additions & 1 deletion config/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,5 +71,6 @@
"REG_STATS": 1234,
"BUDGET": 123
}
}
},
"DEFAULT_M2M_USERID": -101
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"pg": "^4.5.5",
"pg-native": "^1.10.1",
"sequelize": "^3.23.0",
"jsonpath": "^1.0.2",
"tc-core-library-js": "appirio-tech/tc-core-library-js.git#v2.6",
"traverse": "^0.6.6",
"urlencode": "^1.1.0"
Expand Down
8 changes: 6 additions & 2 deletions src/models/project.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable valid-jsdoc */

import _ from 'lodash';
import { PROJECT_STATUS } from '../constants';
import { PROJECT_STATUS, INVITE_STATUS } from '../constants';

module.exports = function defineProject(sequelize, DataTypes) {
const Project = sequelize.define('Project', {
Expand Down Expand Up @@ -76,7 +76,7 @@ module.exports = function defineProject(sequelize, DataTypes) {
Project.hasMany(models.ProjectMember, { as: 'members', foreignKey: 'projectId' });
Project.hasMany(models.ProjectAttachment, { as: 'attachments', foreignKey: 'projectId' });
Project.hasMany(models.ProjectPhase, { as: 'phases', foreignKey: 'projectId' });
Project.hasMany(models.ProjectMemberInvite, { as: 'memberInvites', foreignKey: 'projectId' });
Project.hasMany(models.ProjectMemberInvite, { as: 'invites', foreignKey: 'projectId' });
Project.hasMany(models.ScopeChangeRequest, { as: 'scopeChangeRequests', foreignKey: 'projectId' });
Project.hasMany(models.WorkStream, { as: 'workStreams', foreignKey: 'projectId' });
},
Expand Down Expand Up @@ -188,6 +188,10 @@ module.exports = function defineProject(sequelize, DataTypes) {
model: models.PhaseProduct,
as: 'products',
}],
}, {
model: models.ProjectMemberInvite,
as: 'invites',
where: { status: INVITE_STATUS.PENDING },
}],
});
},
Expand Down
3 changes: 3 additions & 0 deletions src/permissions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ module.exports = () => {
Authorizer.setPolicy('project.view', projectView);
Authorizer.setPolicy('project.edit', projectEdit);
Authorizer.setPolicy('project.delete', projectDelete);
Authorizer.setPolicy('project.getMember', projectView);
Authorizer.setPolicy('project.addMember', projectView);
Authorizer.setPolicy('project.listMembers', projectView);
Authorizer.setPolicy('project.removeMember', projectMemberDelete);
Authorizer.setPolicy('project.addAttachment', projectEdit);
Authorizer.setPolicy('project.updateAttachment', projectAttachmentUpdate);
Expand Down Expand Up @@ -85,6 +87,7 @@ module.exports = () => {
Authorizer.setPolicy('projectMemberInvite.create', projectView);
Authorizer.setPolicy('projectMemberInvite.put', true);
Authorizer.setPolicy('projectMemberInvite.get', true);
Authorizer.setPolicy('projectMemberInvite.list', projectView);

Authorizer.setPolicy('form.create', projectAdmin);
Authorizer.setPolicy('form.edit', projectAdmin);
Expand Down
17 changes: 17 additions & 0 deletions src/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,18 @@ router.all(
),
);

router.all(
RegExp(`\\/${apiVersion}\\/.*`), (req, res, next) => {
// if it is an M2M call, hard code user id to a deafult value to avoid errors
// Ideally, the m2m token should have unique userId, which may not be an actual user, as well
const isMachineToken = _.get(req, 'authUser.isMachine', false);
if (req.authUser && !req.authUser.userId && isMachineToken) {
req.authUser.userId = config.DEFAULT_M2M_USERID;
}
return next();
},
);

router.route('/v4/projects/metadata/projectTemplates')
.get(require('./projectTemplates/list'));
router.route('/v4/projects/metadata/projectTemplates/:templateId(\\d+)')
Expand Down Expand Up @@ -107,9 +119,11 @@ router.route('/v4/projects/:projectId(\\d+)/scopeChangeRequests/:requestId(\\d+)
// .delete(require('./scopeChangeRequests/delete'));

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

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

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

router.route('/v4/projects/:projectId(\\d+)/members/invites')
.get(require('./projectMemberInvites/list'));

router.route('/v4/projects/:projectId(\\d+)/members/invite')
.post(require('./projectMemberInvites/create'))
.put(require('./projectMemberInvites/update'))
Expand Down
19 changes: 15 additions & 4 deletions src/routes/projectMemberInvites/create.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@


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

if (!invite.userIds && !invite.emails) {
const err = new Error('Either userIds or emails are required');
Expand Down Expand Up @@ -340,16 +341,26 @@ module.exports = [
sendInviteEmail(req, projectId, v);
}
});
return values;
return values.map(value => value.get({ plain: true }));
}); // models.sequelize.Promise.all
}); // models.ProjectMemberInvite.getPendingInvitesForProject
})
.then(values => (
// populate successful invites with user details if required
util.getObjectsWithMemberDetails(values, fields, req)
.catch((err) => {
req.log.error('Cannot get user details for invites.');
req.log.debug('Error during getting user details for invites', err);
})
))
.then((values) => {
const success = _.assign({}, { success: values });
if (failed.length) {
res.status(403).json(util.wrapResponse(req.id, _.assign({}, success, { failed }), null, 403));
res.status(403).json(util.wrapResponse(req.id,
util.maskInviteEmails('$.email', _.assign({}, success, { failed }), req), null, 403));
} else {
res.status(201).json(util.wrapResponse(req.id, success, null, 201));
res.status(201).json(util.wrapResponse(req.id,
util.maskInviteEmails('$.success[?(@.email)]', success, req), null, 201));
}
})
.catch(err => next(err));
Expand Down
6 changes: 5 additions & 1 deletion src/routes/projectMemberInvites/create.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,8 @@ describe('Project Member Invite create', () => {
should.exist(resJson);
resJson.role.should.equal('customer');
resJson.projectId.should.equal(project2.id);
// temporary disable this feature, because it has some side effects
// resJson.email.should.equal('he**o@wo**d.com'); // email is masked
resJson.email.should.equal('hello@world.com');
server.services.pubsub.publish.calledWith('project.member.invite.created').should.be.true;
done();
Expand Down Expand Up @@ -407,6 +409,8 @@ describe('Project Member Invite create', () => {
resJson.role.should.equal('customer');
resJson.projectId.should.equal(project2.id);
resJson.userId.should.equal(12345);
// temporary disable this feature, because it has some side effects
// resJson.email.should.equal('he**o@wo**d.com'); // email is masked
resJson.email.should.equal('hello@world.com');
server.services.pubsub.publish.calledWith('project.member.invite.created').should.be.true;
done();
Expand Down Expand Up @@ -848,7 +852,7 @@ describe('Project Member Invite create', () => {
} else {
const resJson = res.body.result.content.failed;
should.exist(resJson);
resJson[0].email.should.equal('DUPLICATE_UPPERCASE@test.com');
resJson[0].email.should.equal('DUPLICATE_UPPERCASE@test.com'); // email is masked
resJson[0].message.should.equal('User with such email is already invited to this project.');
resJson.length.should.equal(1);
done();
Expand Down
63 changes: 43 additions & 20 deletions src/routes/projectMemberInvites/get.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@


import _ from 'lodash';
import Joi from 'joi';
import validate from 'express-validation';
import { middleware as tcMiddleware } from 'tc-core-library-js';
import models from '../../models';
import util from '../../util';
Expand All @@ -9,27 +9,50 @@ import util from '../../util';
* API to update invite member to project.
*
*/
const schema = {
query: {
fields: Joi.string().optional(),
},
};
const permissions = tcMiddleware.permissions;

module.exports = [
// handles request validations
validate(schema),
permissions('projectMemberInvite.get'),
(req, res, next) => {
const projectId = _.parseInt(req.params.projectId);
const currentUserId = req.authUser.userId;
let invite;
return models.ProjectMemberInvite.getPendingInviteByEmailOrUserId(projectId, req.authUser.email, currentUserId)
.then((_invite) => {
invite = _invite;
if (!invite) {
// check there is an existing invite for the user with status PENDING
// handle 404
const err = new Error('invite not found for project id ' +
`${projectId}, userId ${currentUserId}, email ${req.authUser.email}`);
err.status = 404;
return next(err);
}
return res.json(util.wrapResponse(req.id, invite));
});
async (req, res, next) => {
try {
const projectId = _.parseInt(req.params.projectId);
const currentUserId = req.authUser.userId;
const invite = await models.ProjectMemberInvite.getPendingInviteByEmailOrUserId(
projectId, req.authUser.email, currentUserId,
);
if (!invite) {
// check there is an existing invite for the user with status PENDING
// handle 404
const err = new Error(
'invite not found for project id ' +
`${projectId}, userId ${currentUserId}, email ${req.authUser.email}`,
);
err.status = 404;
throw err;
}

let fields = null;
if (req.query.fields) {
fields = req.query.fields.split(',');
}
let inviteWithDetails;
try {
[inviteWithDetails] = await util.getObjectsWithMemberDetails([invite], fields, req);
} catch (err) {
inviteWithDetails = invite;
req.log.error('Cannot get user details for invite.');
req.log.debug('Error during getting user details for invite.', err);
}

return res.json(util.wrapResponse(req.id, inviteWithDetails));
} catch (err) {
return next(err);
}
},
];
44 changes: 44 additions & 0 deletions src/routes/projectMemberInvites/list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import _ from 'lodash';
import Joi from 'joi';
import validate from 'express-validation';
import { middleware as tcMiddleware } from 'tc-core-library-js';
import models from '../../models';
import util from '../../util';

/**
* API to list all project member invites.
*
*/
const permissions = tcMiddleware.permissions;

const schema = {
query: {
fields: Joi.string().optional(),
},
};

module.exports = [
validate(schema),
permissions('projectMemberInvite.list'),
async (req, res, next) => {
try {
let fields = null;
if (req.query.fields) {
fields = req.query.fields.split(',');
}
const projectId = _.parseInt(req.params.projectId);
const invites = await models.ProjectMemberInvite.getPendingAndReguestedInvitesForProject(projectId);
let invitesWithDetails;
try {
invitesWithDetails = await util.getObjectsWithMemberDetails(invites, fields, req);
} catch (err) {
invitesWithDetails = invites;
req.log.error('Cannot get user details for invites.');
req.log.debug('Error during getting user details for invites', err);
}
return res.json(util.wrapResponse(req.id, util.maskInviteEmails('$[*].email', invitesWithDetails, req)));
} catch (err) {
return next(err);
}
},
];
6 changes: 4 additions & 2 deletions src/routes/projectMemberInvites/update.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,11 +135,13 @@ module.exports = [
};
return util
.addUserToProject(req, member)
.then(() => res.json(util.wrapResponse(req.id, updatedInvite)))
.then(() => res.json(util.wrapResponse(req.id,
util.maskInviteEmails('$.email', updatedInvite, req))))
.catch(err => next(err));
});
}
return res.json(util.wrapResponse(req.id, updatedInvite));

return res.json(util.wrapResponse(req.id, util.maskInviteEmails('$.email', updatedInvite, req)));
});
});
},
Expand Down
54 changes: 54 additions & 0 deletions src/routes/projectMembers/get.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@


import _ from 'lodash';
import Joi from 'joi';
import validate from 'express-validation';
import { middleware as tcMiddleware } from 'tc-core-library-js';
import util from '../../util';

/**
* API to get a project member in a project.
*/
const permissions = tcMiddleware.permissions;

const schema = {
query: {
fields: Joi.string().optional(),
},
};

module.exports = [
validate(schema),
permissions('project.getMember'),
async (req, res, next) => {
try {
const projectId = _.parseInt(req.params.projectId);
let fields = null;
if (req.query.fields) {
fields = req.query.fields.split(',');
}
const memberId = _.parseInt(req.params.id);
const member = _.find(req.context.currentProjectMembers, user => user.id === memberId);
if (!member) {
const err = new Error(
`member not found for project id ${projectId}, userId ${memberId}`,
);
err.status = 404;
throw err;
}

let memberWithDetails;
try {
[memberWithDetails] = await util.getObjectsWithMemberDetails([member], fields, req);
} catch (err) {
memberWithDetails = member;
req.log.error('Cannot get user details for the member.');
req.log.debug('Error during getting user details for member.', err);
}

return res.json(util.wrapResponse(req.id, memberWithDetails));
} catch (err) {
return next(err);
}
},
];
Loading