From 7cf5810af226a8b66ea862345a3dc7eda08da059 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Tue, 19 Mar 2019 11:35:52 +0800 Subject: [PATCH 1/9] winning submission from challenge 30086547 - Topcoder Connect - Projects for copilots --- local/seed/index.js | 2 +- local/seed/projects.json | 86 ++++++++++++++ local/seed/seedProjects.js | 58 ++++++++- src/models/project.js | 12 +- src/permissions/project.view.js | 5 +- .../projectMemberInvites/create.spec.js | 27 +++-- src/routes/projectMembers/create.spec.js | 110 ++++++++++++++---- src/routes/projects/list-db.js | 3 +- src/routes/projects/list-db.spec.js | 7 +- src/routes/projects/list.js | 2 +- src/routes/projects/list.spec.js | 7 +- 11 files changed, 265 insertions(+), 54 deletions(-) diff --git a/local/seed/index.js b/local/seed/index.js index c13edcce..a98fcc1c 100644 --- a/local/seed/index.js +++ b/local/seed/index.js @@ -10,4 +10,4 @@ async function seed() { await seedProjects(targetUrl, token); } -seed(); +seed().then(() => process.exit()); diff --git a/local/seed/projects.json b/local/seed/projects.json index 8d4fb65f..7e9cb032 100644 --- a/local/seed/projects.json +++ b/local/seed/projects.json @@ -174,5 +174,91 @@ "status": "cancelled", "cancelReason": "Test cancel" } + }, + { + "param": { + "name": "Reviewed project with copilot invited", + "details": { + "utm": { + "code": "" + }, + "appDefinition": { + "primaryTarget": "phone", + "goal": { + "value": "Nothing" + }, + "users": { + "value": "No one" + }, + "notes": "" + }, + "hideDiscussions": true + }, + "description": "Hello this is a sample description... This requires at least 160 characters. I'm trying to satisfy this condition. But I could n't if I don't type this unnecessary message", + "templateId": 3, + "type": "website", + "status": "reviewed", + "invites": [{ + "param": { + "userIds": [40051332], + "role": "copilot" + }}] + } + }, + { + "param": { + "name": "Reviewed project with copilot as a member with copilot role", + "details": { + "utm": { + "code": "" + }, + "appDefinition": { + "primaryTarget": "phone", + "goal": { + "value": "Nothing" + }, + "users": { + "value": "No one" + }, + "notes": "" + }, + "hideDiscussions": true + }, + "description": "Hello this is a sample description... This requires at least 160 characters. I'm trying to satisfy this condition. But I could n't if I don't type this unnecessary message", + "templateId": 3, + "type": "website", + "status": "reviewed", + "invites": [{ + "param": { + "userIds": [40051332], + "role": "copilot" + }}], + "acceptInvitation": true + } + }, + { + "param": { + "name": "Reviewed project when copilot is not a member and not invited", + "details": { + "utm": { + "code": "" + }, + "appDefinition": { + "primaryTarget": "phone", + "goal": { + "value": "Nothing" + }, + "users": { + "value": "No one" + }, + "notes": "" + }, + "hideDiscussions": true + }, + "description": "Hello this is a sample description... This requires at least 160 characters. I'm trying to satisfy this condition. But I could n't if I don't type this unnecessary message", + "templateId": 3, + "type": "website", + "status": "reviewed" + } } ] diff --git a/local/seed/seedProjects.js b/local/seed/seedProjects.js index 36cc3d79..9352ff66 100644 --- a/local/seed/seedProjects.js +++ b/local/seed/seedProjects.js @@ -1,7 +1,8 @@ +import util from '../../src/tests/util'; + const axios = require('axios'); const Promise = require('bluebird'); const _ = require('lodash'); - const projects = require('./projects.json'); /** @@ -16,21 +17,53 @@ module.exports = (targetUrl, token) => { Authorization: `Bearer ${token}`, }; + const adminHeaders = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${util.jwts.connectAdmin}`, + }; + console.log('Creating projects'); projectPromises = projects.map((project, i) => { const status = _.get(project, 'param.status'); const cancelReason = _.get(project, 'param.cancelReason'); + const invites = _.cloneDeep(_.get(project, 'param.invites')); + const acceptInvitation = _.get(project, 'param.acceptInvitation'); + delete project.param.status; delete project.param.cancelReason; + delete project.param.invites; + delete project.param.acceptInvitation; return axios .post(projectsUrl, project, { headers }) .catch((err) => { console.log(`Failed to create project ${i}: ${err.message}`); }) - .then((response) => { + .then(async (response) => { const projectId = _.get(response, 'data.result.content.id'); + if (Array.isArray(invites)) { + let promises = [] + invites.forEach(invite => { + promises.push(createProjectMemberInvite(projectId, invite, targetUrl, headers)) + }) + const responses = await Promise.all(promises) + if (acceptInvitation) { + let acceptInvitationPromises = [] + responses.forEach(response => { + const userId = _.get(response, 'data.result.content.success[0].userId') + acceptInvitationPromises.push(updateProjectMemberInvite(projectId, { + param: { + userId, + status: 'accepted' + } + }, targetUrl, adminHeaders)) + }) + + await Promise.all(acceptInvitationPromises) + } + } + return { projectId, status, @@ -72,3 +105,24 @@ function updateProjectStatus(project, updateParams, targetUrl, headers) { }, ); } + +function createProjectMemberInvite(projectId, params, targetUrl, headers) { + const projectMemberInviteUrl = `${targetUrl}projects/${projectId}/members/invite`; + + return axios + .post(projectMemberInviteUrl, params, { headers }) + .catch((err) => { + console.log(`Failed to create project member invites ${projectId}: ${err.message}`); + }) +} + +function updateProjectMemberInvite(projectId, params, targetUrl, headers) { + const updateProjectMemberInviteUrl = `${targetUrl}projects/${projectId}/members/invite`; + + return axios + .put(updateProjectMemberInviteUrl, params, { headers }) + .catch((err) => { + console.log(`Failed to update project member invites ${projectId}: ${err.message}`); + }) +} + diff --git a/src/models/project.js b/src/models/project.js index 5339b5b4..4e23efc7 100644 --- a/src/models/project.js +++ b/src/models/project.js @@ -1,7 +1,7 @@ /* eslint-disable valid-jsdoc */ import _ from 'lodash'; -import { PROJECT_STATUS, PROJECT_MEMBER_ROLE } from '../constants'; +import { PROJECT_STATUS } from '../constants'; module.exports = function defineProject(sequelize, DataTypes) { const Project = sequelize.define('Project', { @@ -64,18 +64,18 @@ module.exports = function defineProject(sequelize, DataTypes) { /* * @Co-pilots should be able to view projects any of the following conditions are met: * a. they are registered active project members on the project - * b. any project that is in 'reviewed' state AND does not yet have a co-pilot assigned + * b. any project that is in 'reviewed' state AND copilot is invited * @param userId the id of user */ getProjectIdsForCopilot(userId) { return this.findAll({ where: { $or: [ + ['"Project".status=? AND EXISTS(SELECT * FROM "project_member_invites" WHERE "deletedAt" ' + + 'IS NULL AND "projectId" = "Project".id ' + + 'AND "status" IN (\'requested\', \'pending\') AND "userId" = ? )', PROJECT_STATUS.REVIEWED, userId], ['EXISTS(SELECT * FROM "project_members" WHERE "deletedAt" ' + - 'IS NULL AND "projectId" = "Project".id AND "userId" = ? )', userId], - ['"Project".status=? AND NOT EXISTS(SELECT * FROM "project_members" WHERE ' + - ' "deletedAt" IS NULL AND "projectId" = "Project".id AND "role" = ? )', - PROJECT_STATUS.REVIEWED, PROJECT_MEMBER_ROLE.COPILOT], + 'IS NULL AND "projectId" = "Project".id AND "userId" = ? )', userId], ], }, attributes: ['id'], diff --git a/src/permissions/project.view.js b/src/permissions/project.view.js index 61e4ebed..3701e059 100644 --- a/src/permissions/project.view.js +++ b/src/permissions/project.view.js @@ -24,8 +24,7 @@ module.exports = freq => new Promise((resolve, reject) => { || util.hasRoles(req, MANAGER_ROLES) || !_.isUndefined(_.find(members, m => m.userId === currentUserId)); - // if user is co-pilot and the project doesn't have any copilots then - // user can access the project + // if user is co-pilot and he is a member or if project is in "reviewed" status and he is invited if (!hasAccess && util.hasRole(req, USER_ROLE.COPILOT)) { return models.Project.getProjectIdsForCopilot(currentUserId) .then((ids) => { @@ -53,7 +52,7 @@ module.exports = freq => new Promise((resolve, reject) => { .then((project) => { if (!project || [PROJECT_STATUS.DRAFT, PROJECT_STATUS.IN_REVIEW].indexOf(project.status) >= 0) { errorMessage = 'Copilot: Project is not yet available to copilots'; - } else { + } else if (project.status !== PROJECT_STATUS.REVIEWED) { // project status is 'active' or higher so it's not available to copilots errorMessage = 'Copilot: Project has already started'; } diff --git a/src/routes/projectMemberInvites/create.spec.js b/src/routes/projectMemberInvites/create.spec.js index 74f6be9c..e9701f99 100644 --- a/src/routes/projectMemberInvites/create.spec.js +++ b/src/routes/projectMemberInvites/create.spec.js @@ -65,18 +65,27 @@ describe('Project Member Invite create', () => { lastActivityUserId: '1', }).then((p2) => { project2 = p2; - models.ProjectMemberInvite.create({ - projectId: project1.id, - userId: 40051335, - email: null, - role: PROJECT_MEMBER_ROLE.MANAGER, - status: INVITE_STATUS.PENDING, + models.ProjectMember.create({ + userId: 40051332, + projectId: project2.id, + role: 'copilot', + isPrimary: true, createdBy: 1, updatedBy: 1, - createdAt: '2016-06-30 00:33:07+00', - updatedAt: '2016-06-30 00:33:07+00', }).then(() => { - done(); + models.ProjectMemberInvite.create({ + projectId: project1.id, + userId: 40051335, + email: null, + role: PROJECT_MEMBER_ROLE.MANAGER, + status: INVITE_STATUS.PENDING, + createdBy: 1, + updatedBy: 1, + createdAt: '2016-06-30 00:33:07+00', + updatedAt: '2016-06-30 00:33:07+00', + }).then(() => { + done(); + }); }); })); }); diff --git a/src/routes/projectMembers/create.spec.js b/src/routes/projectMembers/create.spec.js index f55ad90a..f6c85633 100644 --- a/src/routes/projectMembers/create.spec.js +++ b/src/routes/projectMembers/create.spec.js @@ -60,7 +60,7 @@ describe('Project Members create', () => { .expect(403, done); }); - it('should return 201 and then 400 if user is already registered', (done) => { + it('should return 201 when invited then accepted and then 404 if user is already as a member', (done) => { const mockHttpClient = _.merge(testUtil.mockHttpClient, { get: () => Promise.resolve({ status: 200, @@ -79,9 +79,15 @@ describe('Project Members create', () => { }); sandbox.stub(util, 'getHttpClient', () => mockHttpClient); request(server) - .post(`/v4/projects/${project1.id}/members/`) + .post(`/v4/projects/${project1.id}/members/invite`) .set({ - Authorization: `Bearer ${testUtil.jwts.copilot}`, + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send({ + param: { + userIds: [40051332], + role: 'copilot', + }, }) .expect('Content-Type', /json/) .expect(201) @@ -89,26 +95,59 @@ describe('Project Members create', () => { if (err) { done(err); } else { - const resJson = res.body.result.content; + const resJson = res.body.result.content.success[0]; should.exist(resJson); resJson.role.should.equal('copilot'); resJson.projectId.should.equal(project1.id); resJson.userId.should.equal(40051332); - server.services.pubsub.publish.calledWith('project.member.added').should.be.true; + server.services.pubsub.publish.calledWith('project.member.invite.created').should.be.true; request(server) - .post(`/v4/projects/${project1.id}/members/`) + .put(`/v4/projects/${project1.id}/members/invite`) .set({ - Authorization: `Bearer ${testUtil.jwts.copilot}`, + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .send({ + param: { + userId: 40051332, + status: 'accepted', + }, }) .expect('Content-Type', /json/) - .expect(400) + .expect(200) .end((err2, res2) => { if (err2) { - done(err); + done(err2); } else { - res2.body.result.status.should.equal(400); - done(); + const resJson2 = res2.body.result.content; + should.exist(resJson2); + resJson2.role.should.equal('copilot'); + resJson2.projectId.should.equal(project1.id); + resJson2.userId.should.equal(40051332); + server.services.pubsub.publish.calledWith('project.member.invite.updated').should.be.true; + server.services.pubsub.publish.calledWith('project.member.added').should.be.true; + + request(server) + .put(`/v4/projects/${project1.id}/members/invite`) + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .send({ + param: { + userId: 40051332, + status: 'accepted', + }, + }) + .expect('Content-Type', /json/) + .expect(404) + .end((err3, res3) => { + if (err3) { + done(err3); + } else { + res3.body.result.status.should.equal(404); + done(); + } + }); } }); } @@ -238,28 +277,53 @@ describe('Project Members create', () => { it('sends single BUS_API_EVENT.PROJECT_TEAM_UPDATED message when copilot added', (done) => { request(server) - .post(`/v4/projects/${project1.id}/members/`) + .post(`/v4/projects/${project1.id}/members/invite`) .set({ - Authorization: `Bearer ${testUtil.jwts.copilot}`, + Authorization: `Bearer ${testUtil.jwts.admin}`, }) .send({ + param: { + userIds: [40051332], + role: 'copilot', + }, }) .expect(201) .end((err) => { if (err) { done(err); } else { - testUtil.wait(() => { - createEventSpy.calledTwice.should.be.true; - createEventSpy.firstCall.calledWith(BUS_API_EVENT.MEMBER_JOINED_COPILOT); - createEventSpy.secondCall.calledWith(BUS_API_EVENT.PROJECT_TEAM_UPDATED, sinon.match({ - projectId: project1.id, - projectName: project1.name, - projectUrl: `https://local.topcoder-dev.com/projects/${project1.id}`, + request(server) + .put(`/v4/projects/${project1.id}/members/invite`) + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .send({ + param: { userId: 40051332, - initiatorUserId: 40051332, - })).should.be.true; - done(); + status: 'accepted', + }, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err2) => { + if (err2) { + done(err2); + } else { + testUtil.wait(() => { + createEventSpy.callCount.should.equal(4); + createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_MEMBER_INVITE_REQUESTED); + createEventSpy.secondCall.calledWith(BUS_API_EVENT.PROJECT_MEMBER_INVITE_UPDATED); + createEventSpy.thirdCall.calledWith(BUS_API_EVENT.MEMBER_JOINED_COPILOT); + createEventSpy.lastCall.calledWith(BUS_API_EVENT.PROJECT_TEAM_UPDATED, sinon.match({ + projectId: project1.id, + projectName: project1.name, + projectUrl: `https://local.topcoder-dev.com/projects/${project1.id}`, + userId: 40051336, + initiatorUserId: 40051336, + })).should.be.true; + done(); + }); + } }); } }); diff --git a/src/routes/projects/list-db.js b/src/routes/projects/list-db.js index 3e10736f..6d19428f 100644 --- a/src/routes/projects/list-db.js +++ b/src/routes/projects/list-db.js @@ -133,11 +133,12 @@ module.exports = [ } // If user requested projects where he/she is a member or // if they are not a copilot then return projects that they are members in. - // Copilots can view projects that they are members in or they have + // Copilots can view projects that they are members in or they are invited // const getProjectIds = !memberOnly && util.hasRole(req, USER_ROLE.COPILOT) ? models.Project.getProjectIdsForCopilot(req.authUser.userId) : models.ProjectMember.getProjectIdsForUser(req.authUser.userId); + return getProjectIds .then((accessibleProjectIds) => { let allowedProjectIds = accessibleProjectIds; diff --git a/src/routes/projects/list-db.spec.js b/src/routes/projects/list-db.spec.js index 08f4d14c..f7022e51 100644 --- a/src/routes/projects/list-db.spec.js +++ b/src/routes/projects/list-db.spec.js @@ -184,8 +184,7 @@ describe('LIST Project db', () => { }); }); - it('should return the project when project that is in reviewed state AND does not yet' + - 'have a co-pilot assigned', (done) => { + it('should return the project when project that is in reviewed state in which the copilot is its member or has been invited', (done) => { request(server) .get('/v4/projects/db/') .set({ @@ -198,9 +197,9 @@ describe('LIST Project db', () => { done(err); } else { const resJson = res.body.result.content; - res.body.result.metadata.totalCount.should.equal(3); + res.body.result.metadata.totalCount.should.equal(2); should.exist(resJson); - resJson.should.have.lengthOf(3); + resJson.should.have.lengthOf(2); done(); } }); diff --git a/src/routes/projects/list.js b/src/routes/projects/list.js index d9be9f66..88325822 100755 --- a/src/routes/projects/list.js +++ b/src/routes/projects/list.js @@ -438,7 +438,7 @@ module.exports = [ } // If user requested projects where he/she is a member or // if they are not a copilot then return projects that they are members in. - // Copilots can view projects that they are members in or they have + // Copilots can view projects that they are members in or they are invited // const getProjectIds = !memberOnly && util.hasRole(req, USER_ROLE.COPILOT) ? models.Project.getProjectIdsForCopilot(req.authUser.userId) : diff --git a/src/routes/projects/list.spec.js b/src/routes/projects/list.spec.js index dfecec90..3edc9f4c 100644 --- a/src/routes/projects/list.spec.js +++ b/src/routes/projects/list.spec.js @@ -303,8 +303,7 @@ describe('LIST Project', () => { }); }); - it('should return the project when project that is in reviewed state AND does not yet ' + - 'have a co-pilot assigned', (done) => { + it('should return the project when project that is in reviewed state in which the copilot is its member or has been invited', (done) => { request(server) .get('/v4/projects') .set({ @@ -317,9 +316,9 @@ describe('LIST Project', () => { done(err); } else { const resJson = res.body.result.content; - res.body.result.metadata.totalCount.should.equal(3); + res.body.result.metadata.totalCount.should.equal(2); should.exist(resJson); - resJson.should.have.lengthOf(3); + resJson.should.have.lengthOf(2); done(); } }); From 5b99768506e3d786aefe5bbb7c57a3993560f3cc Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Tue, 19 Mar 2019 11:51:25 +0800 Subject: [PATCH 2/9] use pshah_copilot as demo user for project with copilot --- local/seed/projects.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/local/seed/projects.json b/local/seed/projects.json index 7e9cb032..9fb184af 100644 --- a/local/seed/projects.json +++ b/local/seed/projects.json @@ -200,7 +200,7 @@ "status": "reviewed", "invites": [{ "param": { - "userIds": [40051332], + "userIds": [40152855], "role": "copilot" }}] } @@ -230,7 +230,7 @@ "status": "reviewed", "invites": [{ "param": { - "userIds": [40051332], + "userIds": [40152855], "role": "copilot" }}], "acceptInvitation": true From 63c7d65534586daa678e574b26df91725334a072 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Tue, 19 Mar 2019 12:30:24 +0800 Subject: [PATCH 3/9] fix seed script to wait until created projects are indexed in ES before starting updating statuses --- local/seed/seedProjects.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/local/seed/seedProjects.js b/local/seed/seedProjects.js index 9352ff66..d503a630 100644 --- a/local/seed/seedProjects.js +++ b/local/seed/seedProjects.js @@ -74,14 +74,18 @@ module.exports = (targetUrl, token) => { return Promise.all(projectPromises) .then((createdProjects) => { - console.log('Updating statuses'); - return Promise.all( - createdProjects.map(({ projectId, status, cancelReason }) => - updateProjectStatus(projectId, { status, cancelReason }, targetUrl, headers).catch((ex) => { - console.log(`Failed to update project status of project with id ${projectId}: ${ex.message}`); - }), - ), - ); + console.log('Wait 5 seconds to give time ES to index created projects...'); + return Promise.delay(5000).then(() => { + console.log('Updating statuses...'); + + return Promise.all( + createdProjects.map(({ projectId, status, cancelReason }) => + updateProjectStatus(projectId, { status, cancelReason }, targetUrl, headers).catch((ex) => { + console.log(`Failed to update project status of project with id ${projectId}: ${ex.message}`); + }), + ), + ) + }); }) .then(() => console.log('Done project seed.')) .catch(ex => console.error(ex)); From b3d1cb84a93977a7d6d2f638720e033c515e6c35 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Tue, 19 Mar 2019 12:30:33 +0800 Subject: [PATCH 4/9] fix m2m config --- config/m2m.local.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/m2m.local.js b/config/m2m.local.js index 9aef91f2..8fb26130 100644 --- a/config/m2m.local.js +++ b/config/m2m.local.js @@ -5,7 +5,7 @@ if (process.env.NODE_ENV === 'test') { config = require('./test.json'); } else { config = { - identityServiceEndpoint: "https://api.topcoder-dev.com/", + identityServiceEndpoint: "https://api.topcoder-dev.com/v3/", authSecret: 'secret', authDomain: 'topcoder-dev.com', logLevel: 'debug', From 8a3c1feb8e27302e57b6a0c6d296d6f9a446f295 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Tue, 19 Mar 2019 15:23:18 +0800 Subject: [PATCH 5/9] updated so copilots cannot access projects until they accept invitation - so basically copilots have the same rights to see list of project and project details as other regular non-privilage users --- src/models/project.js | 3 --- src/routes/projects/list-db.js | 12 ++++-------- src/routes/projects/list.js | 12 ++++-------- 3 files changed, 8 insertions(+), 19 deletions(-) diff --git a/src/models/project.js b/src/models/project.js index 4e23efc7..759c8f98 100644 --- a/src/models/project.js +++ b/src/models/project.js @@ -71,9 +71,6 @@ module.exports = function defineProject(sequelize, DataTypes) { return this.findAll({ where: { $or: [ - ['"Project".status=? AND EXISTS(SELECT * FROM "project_member_invites" WHERE "deletedAt" ' + - 'IS NULL AND "projectId" = "Project".id ' + - 'AND "status" IN (\'requested\', \'pending\') AND "userId" = ? )', PROJECT_STATUS.REVIEWED, userId], ['EXISTS(SELECT * FROM "project_members" WHERE "deletedAt" ' + 'IS NULL AND "projectId" = "Project".id AND "userId" = ? )', userId], ], diff --git a/src/routes/projects/list-db.js b/src/routes/projects/list-db.js index 6d19428f..c01b138f 100644 --- a/src/routes/projects/list-db.js +++ b/src/routes/projects/list-db.js @@ -1,7 +1,7 @@ import _ from 'lodash'; import Promise from 'bluebird'; import models from '../../models'; -import { USER_ROLE, MANAGER_ROLES } from '../../constants'; +import { MANAGER_ROLES } from '../../constants'; import util from '../../util'; /** @@ -131,13 +131,9 @@ module.exports = [ .then(result => res.json(util.wrapResponse(req.id, result.rows, result.count))) .catch(err => next(err)); } - // If user requested projects where he/she is a member or - // if they are not a copilot then return projects that they are members in. - // Copilots can view projects that they are members in or they are invited - // - const getProjectIds = !memberOnly && util.hasRole(req, USER_ROLE.COPILOT) ? - models.Project.getProjectIdsForCopilot(req.authUser.userId) : - models.ProjectMember.getProjectIdsForUser(req.authUser.userId); + + // regular users can only see projects they are members of (or invited, handled bellow) + const getProjectIds = models.ProjectMember.getProjectIdsForUser(req.authUser.userId); return getProjectIds .then((accessibleProjectIds) => { diff --git a/src/routes/projects/list.js b/src/routes/projects/list.js index 88325822..73cd3f5d 100755 --- a/src/routes/projects/list.js +++ b/src/routes/projects/list.js @@ -5,7 +5,7 @@ import _ from 'lodash'; import config from 'config'; import models from '../../models'; -import { USER_ROLE, MANAGER_ROLES } from '../../constants'; +import { MANAGER_ROLES } from '../../constants'; import util from '../../util'; const ES_PROJECT_INDEX = config.get('elasticsearchConfig.indexName'); @@ -436,13 +436,9 @@ module.exports = [ .then(result => res.json(util.wrapResponse(req.id, result.rows, result.count))) .catch(err => next(err)); } - // If user requested projects where he/she is a member or - // if they are not a copilot then return projects that they are members in. - // Copilots can view projects that they are members in or they are invited - // - const getProjectIds = !memberOnly && util.hasRole(req, USER_ROLE.COPILOT) ? - models.Project.getProjectIdsForCopilot(req.authUser.userId) : - models.ProjectMember.getProjectIdsForUser(req.authUser.userId); + + // regular users can only see projects they are members of (or invited, handled bellow) + const getProjectIds = models.ProjectMember.getProjectIdsForUser(req.authUser.userId); return getProjectIds .then((accessibleProjectIds) => { From 6df79f8dac710264691f99872c87a299632905c1 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Tue, 19 Mar 2019 15:24:08 +0800 Subject: [PATCH 6/9] fix seed projects script so it waits until ES index is done for previous request --- local/seed/seedProjects.js | 43 +++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/local/seed/seedProjects.js b/local/seed/seedProjects.js index d503a630..e1d50417 100644 --- a/local/seed/seedProjects.js +++ b/local/seed/seedProjects.js @@ -5,6 +5,9 @@ const Promise = require('bluebird'); const _ = require('lodash'); const projects = require('./projects.json'); +// we make delay after requests which has to be indexed in ES asynchronous +const ES_INDEX_DELAY = 3000; + /** * Create projects and update their statuses. */ @@ -12,12 +15,12 @@ module.exports = (targetUrl, token) => { let projectPromises; const projectsUrl = `${targetUrl}projects`; - const headers = { + const adminHeaders = { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }; - const adminHeaders = { + const connectAdminHeaders = { 'Content-Type': 'application/json', Authorization: `Bearer ${util.jwts.connectAdmin}`, }; @@ -35,18 +38,32 @@ module.exports = (targetUrl, token) => { delete project.param.acceptInvitation; return axios - .post(projectsUrl, project, { headers }) + .post(projectsUrl, project, { headers: adminHeaders }) .catch((err) => { console.log(`Failed to create project ${i}: ${err.message}`); }) .then(async (response) => { const projectId = _.get(response, 'data.result.content.id'); + // updating status + if (status !== _.get(response, 'data.result.content.status')) { + console.log(`Project #${projectId}: Wait a bit to give time ES to index before updating status...`); + await Promise.delay(ES_INDEX_DELAY); + await updateProjectStatus(projectId, { status, cancelReason }, targetUrl, adminHeaders).catch((ex) => { + console.error(`Project #${projectId}: Failed to update project status: ${ex.message}`); + }); + } + + // creating invitations if (Array.isArray(invites)) { let promises = [] invites.forEach(invite => { - promises.push(createProjectMemberInvite(projectId, invite, targetUrl, headers)) + promises.push(createProjectMemberInvite(projectId, invite, targetUrl, connectAdminHeaders)) }) + + // accepting invitations + console.log(`Project #${projectId}: Wait a bit to give time ES to index before creating invitation...`); + await Promise.delay(ES_INDEX_DELAY); const responses = await Promise.all(promises) if (acceptInvitation) { let acceptInvitationPromises = [] @@ -57,9 +74,11 @@ module.exports = (targetUrl, token) => { userId, status: 'accepted' } - }, targetUrl, adminHeaders)) + }, targetUrl, connectAdminHeaders)) }) + console.log(`Project #${projectId}: Wait a bit to give time ES to index before accepting invitation...`); + await Promise.delay(ES_INDEX_DELAY); await Promise.all(acceptInvitationPromises) } } @@ -73,20 +92,6 @@ module.exports = (targetUrl, token) => { }); return Promise.all(projectPromises) - .then((createdProjects) => { - console.log('Wait 5 seconds to give time ES to index created projects...'); - return Promise.delay(5000).then(() => { - console.log('Updating statuses...'); - - return Promise.all( - createdProjects.map(({ projectId, status, cancelReason }) => - updateProjectStatus(projectId, { status, cancelReason }, targetUrl, headers).catch((ex) => { - console.log(`Failed to update project status of project with id ${projectId}: ${ex.message}`); - }), - ), - ) - }); - }) .then(() => console.log('Done project seed.')) .catch(ex => console.error(ex)); }; From d2dad3f5ccc5806db5e3c21be795082abca071ec Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Tue, 19 Mar 2019 15:33:27 +0800 Subject: [PATCH 7/9] update README --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index c8356d59..e2790ac4 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,8 @@ Microservice to manage CRUD operations for all things Projects. *NOTE: This will first clear all the indices and than recreate them. So use with caution.* * Run + + **NOTE** If you use `config/m2m.local.js` config, you should set M2M environment variables before running the next command. ```bash npm run start:dev ``` @@ -127,6 +129,9 @@ eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI It's been signed with the secret 'secret'. This secret should match your entry in config/local.js. You can generate your own token using https://jwt.io ### Local Deployment + +**NOTE: This part of README may contain inconsistencies and requires update. Don't follow it unless you know how to properly make configuration for these steps. It's not needed for regular development process.** + Build image: `docker build -t tc_projects_services .` Run image: From 4424e700bd07cfba7b2dab8b217e62597bed2324 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Tue, 19 Mar 2019 15:48:59 +0800 Subject: [PATCH 8/9] removed special permissions for copilots to view projects and remove customer error messages for copilots as they cannot be shown anymore --- src/permissions/project.view.js | 40 ++------------------------------- 1 file changed, 2 insertions(+), 38 deletions(-) diff --git a/src/permissions/project.view.js b/src/permissions/project.view.js index 3701e059..e14049ea 100644 --- a/src/permissions/project.view.js +++ b/src/permissions/project.view.js @@ -2,7 +2,7 @@ import _ from 'lodash'; import util from '../util'; import models from '../models'; -import { USER_ROLE, PROJECT_STATUS, PROJECT_MEMBER_ROLE, MANAGER_ROLES } from '../constants'; +import { MANAGER_ROLES } from '../constants'; /** * Super admin, Topcoder Managers are allowed to view any projects @@ -24,47 +24,11 @@ module.exports = freq => new Promise((resolve, reject) => { || util.hasRoles(req, MANAGER_ROLES) || !_.isUndefined(_.find(members, m => m.userId === currentUserId)); - // if user is co-pilot and he is a member or if project is in "reviewed" status and he is invited - if (!hasAccess && util.hasRole(req, USER_ROLE.COPILOT)) { - return models.Project.getProjectIdsForCopilot(currentUserId) - .then((ids) => { - req.context.accessibleProjectIds = ids; - return Promise.resolve(_.indexOf(ids, projectId) > -1); - }); - } return Promise.resolve(hasAccess); }) .then((hasAccess) => { if (!hasAccess) { - let errorMessage = 'You do not have permissions to perform this action'; - // customize error message for copilots - if (util.hasRole(freq, USER_ROLE.COPILOT)) { - if (_.findIndex(freq.context.currentProjectMembers, m => m.role === PROJECT_MEMBER_ROLE.COPILOT) >= 0) { - errorMessage = 'Copilot: Project is already claimed by another copilot'; - return Promise.resolve(errorMessage); - } - return models.Project - .find({ - where: { id: projectId }, - attributes: ['status'], - raw: true, - }) - .then((project) => { - if (!project || [PROJECT_STATUS.DRAFT, PROJECT_STATUS.IN_REVIEW].indexOf(project.status) >= 0) { - errorMessage = 'Copilot: Project is not yet available to copilots'; - } else if (project.status !== PROJECT_STATUS.REVIEWED) { - // project status is 'active' or higher so it's not available to copilots - errorMessage = 'Copilot: Project has already started'; - } - return Promise.resolve(errorMessage); - }); - } - // user is not an admin nor is a registered project member - return Promise.resolve(errorMessage); - } - return Promise.resolve(null); - }).then((errorMessage) => { - if (errorMessage) { + 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)); } From c917f1db3029ea44b742f56c5f416ce160f75e5b Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Tue, 26 Mar 2019 17:31:31 +0800 Subject: [PATCH 9/9] removed unused method --- src/models/project.js | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/src/models/project.js b/src/models/project.js index 759c8f98..946289f2 100644 --- a/src/models/project.js +++ b/src/models/project.js @@ -61,25 +61,6 @@ module.exports = function defineProject(sequelize, DataTypes) { { fields: ['directProjectId'] }, ], classMethods: { - /* - * @Co-pilots should be able to view projects any of the following conditions are met: - * a. they are registered active project members on the project - * b. any project that is in 'reviewed' state AND copilot is invited - * @param userId the id of user - */ - getProjectIdsForCopilot(userId) { - return this.findAll({ - where: { - $or: [ - ['EXISTS(SELECT * FROM "project_members" WHERE "deletedAt" ' + - 'IS NULL AND "projectId" = "Project".id AND "userId" = ? )', userId], - ], - }, - attributes: ['id'], - raw: true, - }) - .then(res => _.map(res, 'id')); - }, /** * Get direct project id * @param id the id of project