Skip to content
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion config/m2m.local.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion local/seed/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ async function seed() {
await seedProjects(targetUrl, token);
}

seed();
seed().then(() => process.exit());
86 changes: 86 additions & 0 deletions local/seed/projects.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [40152855],
"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": [40152855],
"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"
}
}
]
91 changes: 77 additions & 14 deletions local/seed/seedProjects.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,88 @@
import util from '../../src/tests/util';

const axios = require('axios');
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.
*/
module.exports = (targetUrl, token) => {
let projectPromises;

const projectsUrl = `${targetUrl}projects`;
const headers = {
const adminHeaders = {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
};

const connectAdminHeaders = {
'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 })
.post(projectsUrl, project, { headers: adminHeaders })
.catch((err) => {
console.log(`Failed to create project ${i}: ${err.message}`);
})
.then((response) => {
.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, 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 = []
responses.forEach(response => {
const userId = _.get(response, 'data.result.content.success[0].userId')
acceptInvitationPromises.push(updateProjectMemberInvite(projectId, {
param: {
userId,
status: 'accepted'
}
}, 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)
}
}

return {
projectId,
status,
Expand All @@ -40,16 +92,6 @@ 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}`);
}),
),
);
})
.then(() => console.log('Done project seed.'))
.catch(ex => console.error(ex));
};
Expand All @@ -72,3 +114,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}`);
})
}

24 changes: 1 addition & 23 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, PROJECT_MEMBER_ROLE } from '../constants';
import { PROJECT_STATUS } from '../constants';

module.exports = function defineProject(sequelize, DataTypes) {
const Project = sequelize.define('Project', {
Expand Down Expand Up @@ -61,28 +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 does not yet have a co-pilot assigned
* @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],
['"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],
],
},
attributes: ['id'],
raw: true,
})
.then(res => _.map(res, 'id'));
},
/**
* Get direct project id
* @param id the id of project
Expand Down
41 changes: 2 additions & 39 deletions src/permissions/project.view.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -24,48 +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 the project doesn't have any copilots then
// user can access the project
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 {
// 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));
}
Expand Down
27 changes: 18 additions & 9 deletions src/routes/projectMemberInvites/create.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
}));
});
Expand Down
Loading