From 8b17f5317f258d1c6a8b3fdc79cec114a99f7eea Mon Sep 17 00:00:00 2001 From: maxceem Date: Tue, 2 Feb 2021 11:29:31 +0200 Subject: [PATCH 1/8] chore: update PostgreSQL to 12.3 for local development --- local/postgres-db/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/local/postgres-db/Dockerfile b/local/postgres-db/Dockerfile index 844b3f09..acc88fc7 100644 --- a/local/postgres-db/Dockerfile +++ b/local/postgres-db/Dockerfile @@ -1,2 +1,2 @@ -FROM postgres:9.5 -COPY create-multiple-postgresql-databases.sh /docker-entrypoint-initdb.d/ \ No newline at end of file +FROM postgres:12.3 +COPY create-multiple-postgresql-databases.sh /docker-entrypoint-initdb.d/ From 76d88f97992fc75e96b3a34e8d5b8880ed5a360e Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Mon, 8 Feb 2021 20:13:20 +0530 Subject: [PATCH 2/8] feat: git#620-New API endpoint to bring Billing Accounts available to the logged in user - Initial draft --- config/custom-environment-variables.json | 8 +- config/default.json | 8 +- src/constants.js | 1 + src/permissions/constants.js | 22 ++++ src/permissions/index.js | 4 + src/routes/billingAccounts/list.js | 39 +++++++ src/routes/billingAccounts/list.spec.js | 133 +++++++++++++++++++++++ src/routes/index.js | 3 + src/services/salesforceService.js | 77 +++++++++++++ src/tests/util.js | 2 + 10 files changed, 295 insertions(+), 2 deletions(-) create mode 100644 src/routes/billingAccounts/list.js create mode 100644 src/routes/billingAccounts/list.spec.js create mode 100644 src/services/salesforceService.js diff --git a/config/custom-environment-variables.json b/config/custom-environment-variables.json index c414fbce..8dd29cf4 100644 --- a/config/custom-environment-variables.json +++ b/config/custom-environment-variables.json @@ -70,5 +70,11 @@ "EMBED_REPORTS_MAPPING": "EMBED_REPORTS_MAPPING", "ALLOWED_USERS": "REPORTS_ALLOWED_USERS" }, - "DEFAULT_M2M_USERID": "DEFAULT_M2M_USERID" + "DEFAULT_M2M_USERID": "DEFAULT_M2M_USERID", + "salesforce": { + "CLIENT_AUDIENCE": "SALESFORCE_AUDIENCE", + "CLIENT_KEY": "SALESFORCE_CLIENT_KEY", + "SUBJECT": "SALESFORCE_SUBJECT", + "CLIENT_ID": "SALESFORCE_CLIENT_ID" + } } diff --git a/config/default.json b/config/default.json index b302a52f..c67a8bdb 100644 --- a/config/default.json +++ b/config/default.json @@ -76,5 +76,11 @@ "ALLOWED_USERS": "[]" }, "DEFAULT_M2M_USERID": -101, - "taasJobApiUrl": "https://api.topcoder.com/v5/jobs" + "taasJobApiUrl": "https://api.topcoder.com/v5/jobs", + "salesforce": { + "CLIENT_KEY": "", + "CLIENT_AUDIENCE": "", + "SUBJECT": "", + "CLIENT_ID": "" + } } diff --git a/src/constants.js b/src/constants.js index 97b0371f..8b8a1f9d 100644 --- a/src/constants.js +++ b/src/constants.js @@ -274,6 +274,7 @@ export const M2M_SCOPES = { ALL: 'all:projects', READ: 'read:projects', WRITE: 'write:projects', + READ_BILLING_ACCOUNTS: 'read:user-billing-accounts', WRITE_BILLING_ACCOUNTS: 'write:projects-billing-accounts', }, PROJECT_MEMBERS: { diff --git a/src/permissions/constants.js b/src/permissions/constants.js index aa9f4ee9..02c7c2ca 100644 --- a/src/permissions/constants.js +++ b/src/permissions/constants.js @@ -91,6 +91,15 @@ const SCOPES_PROJECTS_WRITE = [ M2M_SCOPES.PROJECTS.WRITE, ]; +/** + * M2M scopes to "read" available Billing Accounts for the project + */ +const SCOPES_PROJECTS_READ_AVL_BILLING_ACCOUNTS = [ + M2M_SCOPES.CONNECT_PROJECT_ADMIN, + M2M_SCOPES.READ_BILLING_ACCOUNTS, + M2M_SCOPES.PROJECTS.ALL, +]; + /** * M2M scopes to "write" billingAccountId property */ @@ -252,6 +261,19 @@ export const PERMISSION = { // eslint-disable-line import/prefer-default-export scopes: SCOPES_PROJECTS_WRITE, }, + /* + * Project Invite + */ + READ_AVL_PROJECT_BILLING_ACCOUNTS: { + meta: { + title: 'Read Available Project Billing Accounts', + group: 'Project Billing Accounts', + description: 'Who can view the Billing Accounts available for the project', + }, + topcoderRoles: ALL, + scopes: SCOPES_PROJECTS_READ_AVL_BILLING_ACCOUNTS, + }, + /* * Project Member */ diff --git a/src/permissions/index.js b/src/permissions/index.js index a37fdb04..2e62dc7f 100644 --- a/src/permissions/index.js +++ b/src/permissions/index.js @@ -20,6 +20,10 @@ module.exports = () => { Authorizer.setPolicy('project.edit', generalPermission(PERMISSION.UPDATE_PROJECT)); Authorizer.setPolicy('project.delete', generalPermission(PERMISSION.DELETE_PROJECT)); + Authorizer.setPolicy('projectBillingAccounts.view', generalPermission([ + PERMISSION.READ_AVL_PROJECT_BILLING_ACCOUNTS, + ])); + Authorizer.setPolicy('projectMember.create', generalPermission([ PERMISSION.CREATE_PROJECT_MEMBER_OWN, PERMISSION.CREATE_PROJECT_MEMBER_NOT_OWN, diff --git a/src/routes/billingAccounts/list.js b/src/routes/billingAccounts/list.js new file mode 100644 index 00000000..77166d56 --- /dev/null +++ b/src/routes/billingAccounts/list.js @@ -0,0 +1,39 @@ +// import _ from 'lodash'; +import validate from 'express-validation'; +import Joi from 'joi'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import SalesforceService from '../../services/salesforceService'; + +/** + * API to get project attachments. + * + */ + +const permissions = tcMiddleware.permissions; + +const schema = { + params: { + projectId: Joi.number().integer().positive().required(), + }, +}; + +module.exports = [ + validate(schema), + permissions('projectBillingAccounts.view'), + async (req, res, next) => { + // const projectId = _.parseInt(req.params.projectId); + const userId = req.authUser.userId; + try { + const { accessToken, instanceUrl } = await SalesforceService.authenticate(); + // eslint-disable-next-line + const sql = `SELECT Topcoder_Billing_Account__r.id, Topcoder_Billing_Account__r.TopCoder_Billing_Account_Id__c, Topcoder_Billing_Account__r.Billing_Account_Name__c, Topcoder_Billing_Account__r.Start_Date__c, Topcoder_Billing_Account__r.End_Date__c from Topcoder_Billing_Account_Resource__c tbar where UserID__c='${userId}'`; + // and Topcoder_Billing_Account__r.TC_Connect_Project_ID__c='${projectId}' + req.log.debug(sql); + const billingAccounts = await SalesforceService.query(sql, accessToken, instanceUrl, req.log); + res.json(billingAccounts); + } catch (error) { + req.log.error(error); + next(error); + } + }, +]; diff --git a/src/routes/billingAccounts/list.spec.js b/src/routes/billingAccounts/list.spec.js new file mode 100644 index 00000000..1bf4144f --- /dev/null +++ b/src/routes/billingAccounts/list.spec.js @@ -0,0 +1,133 @@ +/* eslint-disable no-unused-expressions */ +// import chai from 'chai'; +import request from 'supertest'; +import sinon from 'sinon'; + +import models from '../../models'; +import server from '../../app'; +import testUtil from '../../tests/util'; +import SalesforceService from '../../services/salesforceService'; + +// const should = chai.should(); + +describe('Project Billing Accounts list', () => { + let project1; + let salesforceAuthenticate; + let salesforceQuery; + + beforeEach((done) => { + testUtil.clearDb() + .then(() => testUtil.clearES()) + .then(() => { + models.Project.create({ + type: 'generic', + directProjectId: 1, + billingAccountId: 1, + name: 'test1', + description: 'test project1', + status: 'draft', + details: {}, + createdBy: 1, + updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', + }).then((p) => { + project1 = p; + // create members + return models.ProjectMember.create({ + userId: testUtil.userIds.copilot, + projectId: project1.id, + role: 'copilot', + isPrimary: true, + createdBy: 1, + updatedBy: 1, + }).then(() => models.ProjectMember.create({ + userId: testUtil.userIds.member, + projectId: project1.id, + role: 'customer', + isPrimary: false, + createdBy: 1, + updatedBy: 1, + })); + }).then(() => { + salesforceAuthenticate = sinon.stub(SalesforceService, 'authenticate', () => Promise.resolve({ + accessToken: 'mock', + instanceUrl: 'mock_url', + })); + salesforceQuery = sinon.stub(SalesforceService, 'query', () => Promise.resolve([{ + accessToken: 'mock', + instanceUrl: 'mock_url', + }])); + done(); + }); + }); + }); + + afterEach((done) => { + salesforceAuthenticate.restore(); + salesforceQuery.restore(); + done(); + }); + + after((done) => { + testUtil.clearDb(done); + }); + + describe('List /projects/{id}/billingAccounts', () => { + it('should return 403 for anonymous user', (done) => { + request(server) + .get(`/v5/projects/${project1.id}/billingAccounts`) + .expect(403, done); + }); + + it('should return 403 for a regular user who is not a member of the project', (done) => { + request(server) + .get(`/v5/projects/${project1.id}/billingAccounts`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member2}`, + }) + .send() + .expect(403, done); + }); + + it('should return all attachments to admin', (done) => { + request(server) + .get(`/v5/projects/${project1.id}/billingAccounts`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send() + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body; + resJson.should.have.length(2); + // TODO verify BA fields + done(); + } + }); + }); + + xit('should return all attachments using M2M token with "read:user-billing-accounts" scope', (done) => { + request(server) + .get(`/v5/projects/${project1.id}/billingAccounts`) + .set({ + Authorization: `Bearer ${testUtil.m2m['read:user-billing-accounts']}`, + }) + .send() + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body; + resJson.should.have.length(2); + // TODO verify BA fields + done(); + } + }); + }); + }); +}); diff --git a/src/routes/index.js b/src/routes/index.js index 29a180a0..5a28ee64 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -121,6 +121,9 @@ router.route('/v5/projects/:projectId(\\d+)/scopeChangeRequests/:requestId(\\d+) .patch(require('./scopeChangeRequests/update')); // .delete(require('./scopeChangeRequests/delete')); +router.route('/v5/projects/:projectId(\\d+)/billingAccounts') + .get(require('./billingAccounts/list')); + router.route('/v5/projects/:projectId(\\d+)/members') .get(require('./projectMembers/list')) .post(require('./projectMembers/create')); diff --git a/src/services/salesforceService.js b/src/services/salesforceService.js new file mode 100644 index 00000000..ea3f775f --- /dev/null +++ b/src/services/salesforceService.js @@ -0,0 +1,77 @@ +/** + * Represents the Salesforce service + */ +import _ from 'lodash'; +import config from 'config'; +import jwt from 'jsonwebtoken'; + +const axios = require('axios'); + +const loginBaseUrl = config.salesforce.CLIENT_AUDIENCE || 'https://login.salesforce.com'; +// we are using dummy private key to fail safe when key is not provided in env +let privateKey = config.salesforce.CLIENT_KEY || 'privateKey'; +privateKey = privateKey.replace(/\\n/g, '\n'); + +const urlEncodeForm = k => + Object.keys(k).reduce((a, b) => `${a}&${b}=${encodeURIComponent(k[b])}`, ''); + +/** + * Helper class to abstract salesforce API calls + */ +class SalesforceService { + /** + * Authenticate to Salesforce with pre-configured credentials + * @returns {{accessToken: String, instanceUrl: String}} the result + */ + static authenticate() { + const jwtToken = jwt.sign({}, privateKey, { + expiresIn: '1h', // any expiration + issuer: config.salesforce.CLIENT_ID, + audience: config.salesforce.CLIENT_AUDIENCE, + subject: config.salesforce.SUBJECT, + algorithm: 'RS256', + }); + return axios({ + method: 'post', + url: `${loginBaseUrl}/services/oauth2/token`, + data: urlEncodeForm({ + grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', + assertion: jwtToken, + }), + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + }).then(res => ({ + accessToken: res.data.access_token, + instanceUrl: res.data.instance_url, + })); + } + + /** + * Run the query statement + * @param {String} sql the Saleforce sql statement + * @param {String} accessToken the access token + * @param {String} instanceUrl the salesforce instance url + * @param {Object} logger logger to be used for logging + * @returns {{totalSize: Number, done: Boolean, records: Array}} the result + */ + static query(sql, accessToken, instanceUrl, logger) { + return axios({ + url: `${instanceUrl}/services/data/v37.0/query?q=${sql}`, + method: 'get', + headers: { authorization: `Bearer ${accessToken}` }, + }).then((res) => { + if (logger) { + logger.debug(_.get(res, 'data.records', [])); + } + const billingAccounts = _.get(res, 'data.records', []).map(o => ({ + sfBillingAccountId: _.get(o, 'Topcoder_Billing_Account__r.Id'), + tcBillingAccountId: _.get(o, 'Topcoder_Billing_Account__r.TopCoder_Billing_Account_Id__c'), + name: _.get(o, 'Topcoder_Billing_Account__r.Billing_Account_Name__c'), + startDate: _.get(o, 'Topcoder_Billing_Account__r.Start_Date__c'), + endDate: _.get(o, 'Topcoder_Billing_Account__r.End_Date__c'), + })); + return billingAccounts; + }); + } +} + +export default new SalesforceService(); diff --git a/src/tests/util.js b/src/tests/util.js index a98e7b5c..7a8fcf23 100644 --- a/src/tests/util.js +++ b/src/tests/util.js @@ -39,6 +39,8 @@ export default { }, m2m: { [M2M_SCOPES.CONNECT_PROJECT_ADMIN]: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoidGVzdEBjbGllbnRzIiwiYXVkIjoiaHR0cHM6Ly9tMm0udG9wY29kZXItZGV2LmNvbS8iLCJpYXQiOjE1ODc3MzI0NTksImV4cCI6MjU4NzgxODg1OSwiYXpwIjoidGVzdCIsInNjb3BlIjoiYWxsOmNvbm5lY3RfcHJvamVjdCIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyJ9.q34b2IC1pw3ksl5RtnSEW5_HGwN0asx2MD3LV9-Wffg', + // TODO update token to have correct scope, as of now it is copied from CONNECT_PROJECT_ADMIN + [M2M_SCOPES.PROJECTS.READ_BILLING_ACCOUNTS]: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoidGVzdEBjbGllbnRzIiwiYXVkIjoiaHR0cHM6Ly9tMm0udG9wY29kZXItZGV2LmNvbS8iLCJpYXQiOjE1ODc3MzI0NTksImV4cCI6MjU4NzgxODg1OSwiYXpwIjoidGVzdCIsInNjb3BlIjoiYWxsOmNvbm5lY3RfcHJvamVjdCIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyJ9.q34b2IC1pw3ksl5RtnSEW5_HGwN0asx2MD3LV9-Wffg', [M2M_SCOPES.PROJECTS.ALL]: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoidGVzdEBjbGllbnRzIiwiYXVkIjoiaHR0cHM6Ly9tMm0udG9wY29kZXItZGV2LmNvbS8iLCJpYXQiOjE1ODc3MzI0NTksImV4cCI6MjU4NzgxODg1OSwiYXpwIjoidGVzdCIsInNjb3BlIjoiYWxsOnByb2plY3RzIiwiZ3R5IjoiY2xpZW50LWNyZWRlbnRpYWxzIn0.ixFXMCsBmIN9mQ9Z3s-Apkg20A3d86Pm9RouL7bZMV4', [M2M_SCOPES.PROJECTS.READ]: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoidGVzdEBjbGllbnRzIiwiYXVkIjoiaHR0cHM6Ly9tMm0udG9wY29kZXItZGV2LmNvbS8iLCJpYXQiOjE1ODc3MzI0NTksImV4cCI6MjU4NzgxODg1OSwiYXpwIjoidGVzdCIsInNjb3BlIjoicmVhZDpwcm9qZWN0cyIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyJ9.IpYgfbem-eR6tGjBoxQBPDw6YIulBTZLBn48NuyJT_g', [M2M_SCOPES.PROJECTS.WRITE]: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoidGVzdEBjbGllbnRzIiwiYXVkIjoiaHR0cHM6Ly9tMm0udG9wY29kZXItZGV2LmNvbS8iLCJpYXQiOjE1ODc3MzI0NTksImV4cCI6MjU4NzgxODg1OSwiYXpwIjoidGVzdCIsInNjb3BlIjoid3JpdGU6cHJvamVjdHMiLCJndHkiOiJjbGllbnQtY3JlZGVudGlhbHMifQ.cAMbmnSKXB8Xl4s4Nlo1LduPySBcvKz2Ygilq5b0OD0', From f986af1bd57aa45d935da718457b65df5bcbff29 Mon Sep 17 00:00:00 2001 From: maxceem Date: Thu, 11 Feb 2021 10:07:05 +0200 Subject: [PATCH 3/8] fix: sfdc billing accounts - fixed permissions - renamed variables to keep them clear - fixed and implemented unit tests - fixed code for the Salesforce Service --- src/constants.js | 4 +- src/permissions/constants.js | 15 +++-- src/routes/billingAccounts/list.spec.js | 73 +++++++++++++++++++++---- src/services/salesforceService.js | 2 +- src/tests/util.js | 2 +- 5 files changed, 74 insertions(+), 22 deletions(-) diff --git a/src/constants.js b/src/constants.js index 8b8a1f9d..6fe0011b 100644 --- a/src/constants.js +++ b/src/constants.js @@ -274,8 +274,8 @@ export const M2M_SCOPES = { ALL: 'all:projects', READ: 'read:projects', WRITE: 'write:projects', - READ_BILLING_ACCOUNTS: 'read:user-billing-accounts', - WRITE_BILLING_ACCOUNTS: 'write:projects-billing-accounts', + READ_USER_BILLING_ACCOUNTS: 'read:user-billing-accounts', + WRITE_PROJECTS_BILLING_ACCOUNTS: 'write:projects-billing-accounts', }, PROJECT_MEMBERS: { ALL: 'all:project-members', diff --git a/src/permissions/constants.js b/src/permissions/constants.js index 02c7c2ca..41b7d3be 100644 --- a/src/permissions/constants.js +++ b/src/permissions/constants.js @@ -96,16 +96,15 @@ const SCOPES_PROJECTS_WRITE = [ */ const SCOPES_PROJECTS_READ_AVL_BILLING_ACCOUNTS = [ M2M_SCOPES.CONNECT_PROJECT_ADMIN, - M2M_SCOPES.READ_BILLING_ACCOUNTS, - M2M_SCOPES.PROJECTS.ALL, + M2M_SCOPES.READ_USER_BILLING_ACCOUNTS, ]; /** * M2M scopes to "write" billingAccountId property */ -const SCOPES_PROJECTS_WRITE_BILLING_ACCOUNTS = [ +const SCOPES_PROJECTS_WRITE_PROJECTS_BILLING_ACCOUNTS = [ M2M_SCOPES.CONNECT_PROJECT_ADMIN, - M2M_SCOPES.PROJECTS.WRITE_BILLING_ACCOUNTS, + M2M_SCOPES.PROJECTS.WRITE_PROJECTS_BILLING_ACCOUNTS, ]; /** @@ -240,7 +239,7 @@ export const PERMISSION = { // eslint-disable-line import/prefer-default-export USER_ROLE.MANAGER, USER_ROLE.TOPCODER_ADMIN, ], - scopes: SCOPES_PROJECTS_WRITE_BILLING_ACCOUNTS, + scopes: SCOPES_PROJECTS_WRITE_PROJECTS_BILLING_ACCOUNTS, }, DELETE_PROJECT: { @@ -270,7 +269,11 @@ export const PERMISSION = { // eslint-disable-line import/prefer-default-export group: 'Project Billing Accounts', description: 'Who can view the Billing Accounts available for the project', }, - topcoderRoles: ALL, + projectRoles: [ + ...PROJECT_ROLES_MANAGEMENT, + PROJECT_MEMBER_ROLE.COPILOT, + ], + topcoderRoles: TOPCODER_ROLES_ADMINS, scopes: SCOPES_PROJECTS_READ_AVL_BILLING_ACCOUNTS, }, diff --git a/src/routes/billingAccounts/list.spec.js b/src/routes/billingAccounts/list.spec.js index 1bf4144f..8b0bd316 100644 --- a/src/routes/billingAccounts/list.spec.js +++ b/src/routes/billingAccounts/list.spec.js @@ -1,5 +1,5 @@ /* eslint-disable no-unused-expressions */ -// import chai from 'chai'; +import chai from 'chai'; import request from 'supertest'; import sinon from 'sinon'; @@ -8,7 +8,24 @@ import server from '../../app'; import testUtil from '../../tests/util'; import SalesforceService from '../../services/salesforceService'; -// const should = chai.should(); +chai.should(); + +// demo data which might be returned by the `SalesforceService.query` +const billingAccountsData = [ + { + sfBillingAccountId: 123, + tcBillingAccountId: 123123, + name: 'Billing Account 1', + startDate: '2021-02-10T18:51:27Z', + endDate: '2021-03-10T18:51:27Z', + }, { + sfBillingAccountId: 456, + tcBillingAccountId: 456456, + name: 'Billing Account 2', + startDate: '2011-02-10T18:51:27Z', + endDate: '2011-03-10T18:51:27Z', + }, +]; describe('Project Billing Accounts list', () => { let project1; @@ -54,10 +71,7 @@ describe('Project Billing Accounts list', () => { accessToken: 'mock', instanceUrl: 'mock_url', })); - salesforceQuery = sinon.stub(SalesforceService, 'query', () => Promise.resolve([{ - accessToken: 'mock', - instanceUrl: 'mock_url', - }])); + salesforceQuery = sinon.stub(SalesforceService, 'query', () => Promise.resolve(billingAccountsData)); done(); }); }); @@ -80,17 +94,48 @@ describe('Project Billing Accounts list', () => { .expect(403, done); }); - it('should return 403 for a regular user who is not a member of the project', (done) => { + it('should return 403 for a customer user who is a member of the project', (done) => { request(server) .get(`/v5/projects/${project1.id}/billingAccounts`) .set({ - Authorization: `Bearer ${testUtil.jwts.member2}`, + Authorization: `Bearer ${testUtil.jwts.member}`, }) .send() .expect(403, done); }); - it('should return all attachments to admin', (done) => { + it('should return 403 for a topcoder user who is not a member of the project', (done) => { + request(server) + .get(`/v5/projects/${project1.id}/billingAccounts`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilotManager}`, + }) + .send() + .expect(403, done); + }); + + it('should return all billing accounts for a topcoder user who is a member of the project', (done) => { + request(server) + .get(`/v5/projects/${project1.id}/billingAccounts`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send() + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body; + resJson.should.have.length(2); + resJson.should.include(billingAccountsData[0]); + resJson.should.include(billingAccountsData[1]); + done(); + } + }); + }); + + it('should return all billing accounts to admin', (done) => { request(server) .get(`/v5/projects/${project1.id}/billingAccounts`) .set({ @@ -104,13 +149,15 @@ describe('Project Billing Accounts list', () => { } else { const resJson = res.body; resJson.should.have.length(2); - // TODO verify BA fields + resJson.should.have.length(2); + resJson.should.include(billingAccountsData[0]); + resJson.should.include(billingAccountsData[1]); done(); } }); }); - xit('should return all attachments using M2M token with "read:user-billing-accounts" scope', (done) => { + it('should return all billing accounts using M2M token with "read:user-billing-accounts" scope', (done) => { request(server) .get(`/v5/projects/${project1.id}/billingAccounts`) .set({ @@ -124,7 +171,9 @@ describe('Project Billing Accounts list', () => { } else { const resJson = res.body; resJson.should.have.length(2); - // TODO verify BA fields + resJson.should.have.length(2); + resJson.should.include(billingAccountsData[0]); + resJson.should.include(billingAccountsData[1]); done(); } }); diff --git a/src/services/salesforceService.js b/src/services/salesforceService.js index ea3f775f..8ddc283c 100644 --- a/src/services/salesforceService.js +++ b/src/services/salesforceService.js @@ -74,4 +74,4 @@ class SalesforceService { } } -export default new SalesforceService(); +export default SalesforceService; diff --git a/src/tests/util.js b/src/tests/util.js index 7a8fcf23..85a4bc64 100644 --- a/src/tests/util.js +++ b/src/tests/util.js @@ -40,7 +40,7 @@ export default { m2m: { [M2M_SCOPES.CONNECT_PROJECT_ADMIN]: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoidGVzdEBjbGllbnRzIiwiYXVkIjoiaHR0cHM6Ly9tMm0udG9wY29kZXItZGV2LmNvbS8iLCJpYXQiOjE1ODc3MzI0NTksImV4cCI6MjU4NzgxODg1OSwiYXpwIjoidGVzdCIsInNjb3BlIjoiYWxsOmNvbm5lY3RfcHJvamVjdCIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyJ9.q34b2IC1pw3ksl5RtnSEW5_HGwN0asx2MD3LV9-Wffg', // TODO update token to have correct scope, as of now it is copied from CONNECT_PROJECT_ADMIN - [M2M_SCOPES.PROJECTS.READ_BILLING_ACCOUNTS]: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoidGVzdEBjbGllbnRzIiwiYXVkIjoiaHR0cHM6Ly9tMm0udG9wY29kZXItZGV2LmNvbS8iLCJpYXQiOjE1ODc3MzI0NTksImV4cCI6MjU4NzgxODg1OSwiYXpwIjoidGVzdCIsInNjb3BlIjoiYWxsOmNvbm5lY3RfcHJvamVjdCIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyJ9.q34b2IC1pw3ksl5RtnSEW5_HGwN0asx2MD3LV9-Wffg', + [M2M_SCOPES.PROJECTS.READ_USER_BILLING_ACCOUNTS]: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoidGVzdEBjbGllbnRzIiwiYXVkIjoiaHR0cHM6Ly9tMm0udG9wY29kZXItZGV2LmNvbS8iLCJpYXQiOjE1ODc3MzI0NTksImV4cCI6MjU4NzgxODg1OSwiYXpwIjoidGVzdCIsInNjb3BlIjoiYWxsOmNvbm5lY3RfcHJvamVjdCIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyJ9.q34b2IC1pw3ksl5RtnSEW5_HGwN0asx2MD3LV9-Wffg', [M2M_SCOPES.PROJECTS.ALL]: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoidGVzdEBjbGllbnRzIiwiYXVkIjoiaHR0cHM6Ly9tMm0udG9wY29kZXItZGV2LmNvbS8iLCJpYXQiOjE1ODc3MzI0NTksImV4cCI6MjU4NzgxODg1OSwiYXpwIjoidGVzdCIsInNjb3BlIjoiYWxsOnByb2plY3RzIiwiZ3R5IjoiY2xpZW50LWNyZWRlbnRpYWxzIn0.ixFXMCsBmIN9mQ9Z3s-Apkg20A3d86Pm9RouL7bZMV4', [M2M_SCOPES.PROJECTS.READ]: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoidGVzdEBjbGllbnRzIiwiYXVkIjoiaHR0cHM6Ly9tMm0udG9wY29kZXItZGV2LmNvbS8iLCJpYXQiOjE1ODc3MzI0NTksImV4cCI6MjU4NzgxODg1OSwiYXpwIjoidGVzdCIsInNjb3BlIjoicmVhZDpwcm9qZWN0cyIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyJ9.IpYgfbem-eR6tGjBoxQBPDw6YIulBTZLBn48NuyJT_g', [M2M_SCOPES.PROJECTS.WRITE]: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoidGVzdEBjbGllbnRzIiwiYXVkIjoiaHR0cHM6Ly9tMm0udG9wY29kZXItZGV2LmNvbS8iLCJpYXQiOjE1ODc3MzI0NTksImV4cCI6MjU4NzgxODg1OSwiYXpwIjoidGVzdCIsInNjb3BlIjoid3JpdGU6cHJvamVjdHMiLCJndHkiOiJjbGllbnQtY3JlZGVudGlhbHMifQ.cAMbmnSKXB8Xl4s4Nlo1LduPySBcvKz2Ygilq5b0OD0', From 06afee8b3717748f3bedc17ee75808aa1a197bae Mon Sep 17 00:00:00 2001 From: imcaizheng Date: Fri, 12 Feb 2021 12:53:54 +0800 Subject: [PATCH 4/8] Create Jobs in TaaS using user token instead of M2M --- src/events/busApi.js | 9 ++++ src/events/projects/index.js | 58 ----------------------- src/events/projects/postTaasJobs.js | 73 +++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 58 deletions(-) create mode 100644 src/events/projects/postTaasJobs.js diff --git a/src/events/busApi.js b/src/events/busApi.js index 7b01e055..4e51c98f 100644 --- a/src/events/busApi.js +++ b/src/events/busApi.js @@ -15,6 +15,7 @@ import { import { createEvent } from '../services/busApi'; import models from '../models'; import util from '../util'; +import createTaasJobsFromProject from '../events/projects/postTaasJobs'; /** * Map of project status and event name sent to bus api @@ -57,6 +58,14 @@ module.exports = (app, logger) => { app.on(EVENT.ROUTING_KEY.PROJECT_DRAFT_CREATED, ({ req, project }) => { logger.debug('receive PROJECT_DRAFT_CREATED event'); + // create taas jobs from project of type `talent-as-a-service` + if (project.type === 'talent-as-a-service') { + createTaasJobsFromProject(req, project, logger) + .catch((error) => { + logger.error(`Error while creating TaaS jobs: ${error}`); + }); + } + // send event to bus api createEvent(BUS_API_EVENT.PROJECT_CREATED, _.assign(project, { refCode: _.get(project, 'details.utm.code'), diff --git a/src/events/projects/index.js b/src/events/projects/index.js index a3db47c5..b3401098 100644 --- a/src/events/projects/index.js +++ b/src/events/projects/index.js @@ -5,7 +5,6 @@ import _ from 'lodash'; import Joi from 'joi'; import Promise from 'bluebird'; import config from 'config'; -import axios from 'axios'; import util from '../../util'; import models from '../../models'; import { createPhaseTopic } from '../projectPhases'; @@ -15,27 +14,6 @@ const ES_PROJECT_INDEX = config.get('elasticsearchConfig.indexName'); const ES_PROJECT_TYPE = config.get('elasticsearchConfig.docType'); const eClient = util.getElasticSearchClient(); -/** - * creates taas job - * @param {Object} data the job data - * @return {Object} the job created - */ -const createTaasJob = async (data) => { - const token = await util.getM2MToken(); - const headers = { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}`, - }; - const res = await axios - .post(config.taasJobApiUrl, data, { headers }) - .catch((err) => { - const error = new Error(); - error.message = _.get(err, 'response.data.message', error.message); - throw error; - }); - return res.data; -}; - /** * Payload for deprecated BUS events like `connect.notification.project.updated`. */ @@ -186,42 +164,6 @@ async function projectCreatedKafkaHandler(app, topic, payload) { await Promise.all(topicPromises); app.logger.debug('Topics for phases are successfully created.'); } - try { - if (project.type === 'talent-as-a-service') { - const jobs = _.get(project, 'details.taasDefinition.taasJobs'); - if (!jobs || !jobs.length) { - app.logger.debug(`no jobs found in the project id: ${project.id}`); - return; - } - app.logger.debug(`${jobs.length} jobs found in the project id: ${project.id}`); - await Promise.all( - _.map( - jobs, - (job) => { - // make sure that skills would be unique in the list and only include ones with 'skillId' (actually they all suppose to be with skillId) - const skills = _.chain(job.skills).map('skillId').uniq().compact() - .value(); - return createTaasJob({ - projectId: project.id, - title: job.title, - description: job.description, - skills, - numPositions: Number(job.people), - resourceType: _.get(job, 'role.value', ''), - rateType: 'weekly', // hardcode for now - workload: _.get(job, 'workLoad.title', '').toLowerCase(), - }).then((createdJob) => { - app.logger.debug(`jobId: ${createdJob.id} job created with title "${createdJob.title}"`); - }).catch((err) => { - app.logger.error(`Unable to create job with title "${job.title}": ${err.message}`); - }); - }, - ), - ); - } - } catch (error) { - app.logger.error(`Error while creating TaaS jobs: ${error}`); - } } module.exports = { diff --git a/src/events/projects/postTaasJobs.js b/src/events/projects/postTaasJobs.js new file mode 100644 index 00000000..757c810b --- /dev/null +++ b/src/events/projects/postTaasJobs.js @@ -0,0 +1,73 @@ +/** + * Event handler for the creation of `talent-as-a-service` projects. + */ + +import _ from 'lodash'; +import config from 'config'; +import axios from 'axios'; + +/** + * Create taas job. + * + * @param {String} authToken the authorization token + * @param {Object} data the job data + * @return {Object} the job created + */ +async function createTaasJob(authToken, data) { + const headers = { + 'Content-Type': 'application/json', + Authorization: authToken, + }; + const res = await axios + .post(config.taasJobApiUrl, data, { headers }) + .catch((err) => { + const error = new Error(); + error.message = _.get(err, 'response.data.message', error.message); + throw error; + }); + return res.data; +} + +/** + * Create taas jobs from project of type `talent-as-a-service` using the token from current user. + * + * @param {Object} req the request object + * @param {Object} project the project data + * @param {Object} logger the logger object + * @return {Object} the taas jobs created + */ +async function createTaasJobsFromProject(req, project, logger) { + const jobs = _.get(project, 'details.taasDefinition.taasJobs'); + if (!jobs || !jobs.length) { + logger.debug(`no jobs found in the project id: ${project.id}`); + return; + } + logger.debug(`${jobs.length} jobs found in the project id: ${project.id}`); + await Promise.all( + _.map( + jobs, + (job) => { + // make sure that skills would be unique in the list and only include ones with 'skillId' (actually they all suppose to be with skillId) + const skills = _.chain(job.skills).map('skillId').uniq().compact() + .value(); + return createTaasJob(req.headers.authorization, { + projectId: project.id, + title: job.title, + description: job.description, + duration: job.duration, + skills, + numPositions: Number(job.people), + resourceType: _.get(job, 'role.value', ''), + rateType: 'weekly', // hardcode for now + workload: _.get(job, 'workLoad.title', '').toLowerCase(), + }).then((createdJob) => { + logger.debug(`jobId: ${createdJob.id} job created with title "${createdJob.title}"`); + }).catch((err) => { + logger.error(`Unable to create job with title "${job.title}": ${err.message}`); + }); + }, + ), + ); +} + +module.exports = createTaasJobsFromProject; From 854f8c3576f1cbf14fda56f416ca8c12287ef8a3 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Sat, 13 Feb 2021 12:01:54 +0200 Subject: [PATCH 5/8] fix: return billing account id as interger ref issue #620 --- docs/permissions.html | 37 +++++++++++++++++++++++++ src/routes/billingAccounts/list.spec.js | 4 +-- src/services/salesforceService.js | 7 ++++- src/util.js | 28 +++++++++++++++++++ src/util.spec.js | 28 ++++++++++++++++++- 5 files changed, 100 insertions(+), 4 deletions(-) diff --git a/docs/permissions.html b/docs/permissions.html index b494f485..cbd762a4 100644 --- a/docs/permissions.html +++ b/docs/permissions.html @@ -375,6 +375,43 @@

+
+
+

+ Project Billing Accounts +

+
+
+
+
+
+ Read Available Project Billing Accounts +
+
READ_AVL_PROJECT_BILLING_ACCOUNTS
+
Who can view the Billing Accounts available for the project
+
+
+
+ manager + account_manager + program_manager + account_executive + solution_architect + project_manager + copilot +
+ +
+ Connect Admin + administrator +
+ +
+ all:connect_project + +
+
+

diff --git a/src/routes/billingAccounts/list.spec.js b/src/routes/billingAccounts/list.spec.js index 8b0bd316..bd76869c 100644 --- a/src/routes/billingAccounts/list.spec.js +++ b/src/routes/billingAccounts/list.spec.js @@ -13,13 +13,13 @@ chai.should(); // demo data which might be returned by the `SalesforceService.query` const billingAccountsData = [ { - sfBillingAccountId: 123, + sfBillingAccountId: '123', tcBillingAccountId: 123123, name: 'Billing Account 1', startDate: '2021-02-10T18:51:27Z', endDate: '2021-03-10T18:51:27Z', }, { - sfBillingAccountId: 456, + sfBillingAccountId: '456', tcBillingAccountId: 456456, name: 'Billing Account 2', startDate: '2011-02-10T18:51:27Z', diff --git a/src/services/salesforceService.js b/src/services/salesforceService.js index 8ddc283c..165be34e 100644 --- a/src/services/salesforceService.js +++ b/src/services/salesforceService.js @@ -4,6 +4,7 @@ import _ from 'lodash'; import config from 'config'; import jwt from 'jsonwebtoken'; +import util from '../util'; const axios = require('axios'); @@ -64,7 +65,11 @@ class SalesforceService { } const billingAccounts = _.get(res, 'data.records', []).map(o => ({ sfBillingAccountId: _.get(o, 'Topcoder_Billing_Account__r.Id'), - tcBillingAccountId: _.get(o, 'Topcoder_Billing_Account__r.TopCoder_Billing_Account_Id__c'), + tcBillingAccountId: util.parseIntStrinctly( + _.get(o, 'Topcoder_Billing_Account__r.TopCoder_Billing_Account_Id__c'), + 10, + null, // fallback to null if cannot parse + ), name: _.get(o, 'Topcoder_Billing_Account__r.Billing_Account_Name__c'), startDate: _.get(o, 'Topcoder_Billing_Account__r.Start_Date__c'), endDate: _.get(o, 'Topcoder_Billing_Account__r.End_Date__c'), diff --git a/src/util.js b/src/util.js index 1cdd8cfd..a5ab739a 100644 --- a/src/util.js +++ b/src/util.js @@ -1473,6 +1473,34 @@ const projectServiceUtils = { }); }, + /** + * Parse integer value inside string or return fallback value. + * Unlike original parseInt, this method parses the whole string + * and fails if there are non-integer characters inside the string. + * + * @param {String} str number to parse + * @param {Number} radix radix of the number to parse + * @param {Any} fallbackValue value to return if we cannot parse successfully + * + * @returns {Number} parsed number + */ + parseIntStrictly: (str, radix, fallbackValue) => { + const int = parseInt(str, radix); + + if (_.isNaN(int)) { + return fallbackValue; + } + + // if we parsed only the part of value and it's not the same as intial value + // example: "12x" => 12 which is not the same as initial value "12x", which means + // we cannot parse the full value sucessfully and treat it like we cannot parse at all + if (int.toString() !== str) { + return fallbackValue; + } + + return int; + }, + }; _.assignIn(util, projectServiceUtils); diff --git a/src/util.spec.js b/src/util.spec.js index 0fe574ed..e1a9ba8d 100644 --- a/src/util.spec.js +++ b/src/util.spec.js @@ -1,12 +1,38 @@ /** * Tests for util.js */ -import chai from 'chai'; +import chai, { expect } from 'chai'; import util from './util'; chai.should(); describe('Util method', () => { + describe('parseIntStrictly', () => { + it('should parse a good integer value sucessfully', () => { + util.parseIntStrictly('1234567890', 10, null).should.equal(1234567890); + }); + + it('should return fallback value if the initial value is a float number', () => { + expect(util.parseIntStrictly('1.1', 10, null)).be.equal(null); + }); + + it('should return fallback value if string can be parsed partially only', () => { + expect(util.parseIntStrictly('123XXX', 10, null)).be.equal(null); + }); + + it('should return fallback value if the initial value is `null`', () => { + util.parseIntStrictly(null, 10, 0).should.equal(0); + }); + + it('should return fallback value if the initial value is `undefined`', () => { + expect(util.parseIntStrictly(undefined, 10, null)).be.equal(null); + }); + + it('should return fallback value if the initial value is `""` (emtpy string)', () => { + expect(util.parseIntStrictly('', 10, null)).be.equal(null); + }); + }); + describe('maskEmail', () => { it('should return the original value if the email is non-string', () => { chai.should().not.exist(util.maskEmail(null)); From 9d8f31794841ed69c883aff0c72885b83730c7d2 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Sat, 13 Feb 2021 12:24:02 +0200 Subject: [PATCH 6/8] fix:type --- src/services/salesforceService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/salesforceService.js b/src/services/salesforceService.js index 165be34e..09dc66e2 100644 --- a/src/services/salesforceService.js +++ b/src/services/salesforceService.js @@ -65,7 +65,7 @@ class SalesforceService { } const billingAccounts = _.get(res, 'data.records', []).map(o => ({ sfBillingAccountId: _.get(o, 'Topcoder_Billing_Account__r.Id'), - tcBillingAccountId: util.parseIntStrinctly( + tcBillingAccountId: util.parseIntStrictly( _.get(o, 'Topcoder_Billing_Account__r.TopCoder_Billing_Account_Id__c'), 10, null, // fallback to null if cannot parse From 8f0e752d83f57b70cf12a84f2f542202387d1ccd Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Sat, 13 Feb 2021 13:22:10 +0200 Subject: [PATCH 7/8] fix: small improvements --- src/events/projects/postTaasJobs.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/events/projects/postTaasJobs.js b/src/events/projects/postTaasJobs.js index 757c810b..f2bedfff 100644 --- a/src/events/projects/postTaasJobs.js +++ b/src/events/projects/postTaasJobs.js @@ -9,14 +9,14 @@ import axios from 'axios'; /** * Create taas job. * - * @param {String} authToken the authorization token + * @param {String} authHeader the authorization header * @param {Object} data the job data * @return {Object} the job created */ -async function createTaasJob(authToken, data) { +async function createTaasJob(authHeader, data) { const headers = { 'Content-Type': 'application/json', - Authorization: authToken, + Authorization: authHeader, }; const res = await axios .post(config.taasJobApiUrl, data, { headers }) @@ -54,7 +54,7 @@ async function createTaasJobsFromProject(req, project, logger) { projectId: project.id, title: job.title, description: job.description, - duration: job.duration, + duration: Number(job.duration), skills, numPositions: Number(job.people), resourceType: _.get(job, 'role.value', ''), From 7910d14799a653d3dcf7996a4a9b48d974aedbfd Mon Sep 17 00:00:00 2001 From: xxcxy Date: Mon, 15 Feb 2021 21:13:54 +0800 Subject: [PATCH 8/8] Create member endpoint should return member details #628 --- docs/Project API.postman_collection.json | 49 ++++++++++++++++++++++++ src/routes/projectMembers/create.js | 7 ++++ 2 files changed, 56 insertions(+) diff --git a/docs/Project API.postman_collection.json b/docs/Project API.postman_collection.json index 8035b068..71167a99 100644 --- a/docs/Project API.postman_collection.json +++ b/docs/Project API.postman_collection.json @@ -925,6 +925,55 @@ }, "response": [] }, + { + "name": "Create project member with no payload and return member details", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 201\", function () {", + " pm.response.to.have.status(201);", + " pm.environment.set(\"memberId\", pm.response.json().id);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{api-url}}/projects/{{projectId}}/members?fields=id,userId,role,isPrimary,deletedAt,createdAt,updatedAt,deletedBy,createdBy,updatedBy,handle,photoURL,workingHourStart,workingHourEnd,timeZone,email", + "host": [ + "{{api-url}}" + ], + "path": [ + "projects", + "{{projectId}}", + "members" + ], + "query": [ + { + "key": "fields", + "value": "id,userId,role,isPrimary,deletedAt,createdAt,updatedAt,deletedBy,createdBy,updatedBy,handle,photoURL,workingHourStart,workingHourEnd,timeZone,email" + } + ] + }, + "description": "If the request payload is valid, than project customer should be added. This should sync with the direct project is project is associated with direct project." + }, + "response": [] + }, { "name": "Update project member", "request": { diff --git a/src/routes/projectMembers/create.js b/src/routes/projectMembers/create.js index 1d15991a..fd35fd26 100644 --- a/src/routes/projectMembers/create.js +++ b/src/routes/projectMembers/create.js @@ -80,6 +80,13 @@ module.exports = [ newMember = await util.addUserToProject(req, member, transaction); }); + try { + const fields = req.query.fields ? req.query.fields.split(',') : null; + [newMember] = await util.getObjectsWithMemberDetails([newMember], fields, req); + } catch (err) { + req.log.error('Cannot get user details for member.'); + req.log.debug('Error during getting user details for member.', err); + } return res.status(201).json(newMember); } catch (err) { return next(err);