diff --git a/app.js b/app.js index bc37cd0a..fc52ee99 100644 --- a/app.js +++ b/app.js @@ -76,7 +76,7 @@ app.use((err, req, res, next) => { if (err.response) { // extract error message from V3/V5 API - errorResponse.message = _.get(err, 'response.body.result.content') || _.get(err, 'response.body.message') + errorResponse.message = _.get(err, 'response.body.result.content.message') || _.get(err, 'response.body.message') } if (_.isUndefined(errorResponse.message)) { diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json index a3cc1b66..6daa5e1c 100644 --- a/docs/Topcoder-bookings-api.postman_collection.json +++ b/docs/Topcoder-bookings-api.postman_collection.json @@ -4527,7 +4527,136 @@ } }, "response": [] - } + }, + { + "name": "GET /taas-teams/:id/members", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + }, + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "url": { + "raw": "{{URL}}/taas-teams/:id/members?role=customer&fields=id,userId,role,createdAt,updatedAt,createdBy,updatedBy,handle,photoURL,workingHourStart,workingHourEnd,timeZone,email", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + ":id", + "members" + ], + "query": [ + { + "key": "role", + "value": "customer" + }, + { + "key": "fields", + "value": "id,userId,role,createdAt,updatedAt,createdBy,updatedBy,handle,photoURL,workingHourStart,workingHourEnd,timeZone,email" + } + ], + "variable": [ + { + "key": "id", + "value": "16705" + } + ] + } + }, + "response": [] + }, + { + "name": "GET /taas-teams/:id/invites", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + }, + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "url": { + "raw": "{{URL}}/taas-teams/:id/invites?fields=createdAt,deletedAt,role,updatedBy,createdBy,id,projectId,userId,email,deletedBy,updatedAt,status", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + ":id", + "invites" + ], + "query": [ + { + "key": "fields", + "value": "createdAt,deletedAt,role,updatedBy,createdBy,id,projectId,userId,email,deletedBy,updatedAt,status" + } + ], + "variable": [ + { + "key": "id", + "value": "16705" + } + ] + } + }, + "response": [] + }, + { + "name": "DELETE /taas-teams/:id/members/:projectMemberId", + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + }, + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "url": { + "raw": "{{URL}}/taas-teams/:id/members/:projectMemberId", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + ":id", + "members", + ":projectMemberId" + ], + "variable": [ + { + "key": "id", + "value": "16705" + }, + { + "key": "projectMemberId", + "value": "14327" + } + ] + } + }, + "response": [] + } ] }, { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 4c042bab..ad0a3e19 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1580,6 +1580,177 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + get: + tags: + - Teams + description: | + Search members in a team. + Serves as a proxy endpoint for `GET /projects/{projectId}/members`. + security: + - bearerAuth: [] + parameters: + - in: path + name: id + required: true + schema: + type: integer + description: The team/project id. + - in: query + name: fields + required: false + schema: + type: string + description: Fields to be returned. + - in: query + name: role + required: false + schema: + type: string + description: Filtered by a specific role. + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ProjectMember' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Not authenticated + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '403': + description: Not authorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /taas-teams/{id}/invites: + get: + tags: + - Teams + description: | + Search member invites for a team. + Serves as a proxy endpoint for `GET /projects/{projectId}/invites`. + security: + - bearerAuth: [] + parameters: + - in: path + name: id + required: true + schema: + type: integer + description: The team/project id. + - in: query + name: fields + required: false + schema: + type: string + description: Fields to be returned. + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ProjectMemberInvite' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Not authenticated + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '403': + description: Not authorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /taas-teams/{id}/members/{projectMemberId}: + delete: + tags: + - Teams + description: | + Remove a member from a team. + Serves as a proxy endpoint for `DELETE /projects/{projectId}/members/{id}`. + security: + - bearerAuth: [] + parameters: + - in: path + name: id + required: true + schema: + type: integer + description: The team/project id. + - in: path + name: projectMemberId + required: true + schema: + type: integer + description: The id of the project member. + responses: + '204': + description: OK + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Not authenticated + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' /taas-teams/skills: get: tags: @@ -2496,10 +2667,9 @@ components: properties: success: type: array - description: "The handles." + description: "The members created." items: - type: object - example: {"createdAt": "2021-02-18T19:58:50.610Z", "createdBy": -101, "email": "email@domain.com", "handle": "Scud", "id": 14155, "photoURL": "https://topcoder-dev-media.s3.amazonaws.com/member/profile/Scud-1450982908556.png", "role": "customer", "timeZone": null, "updatedAt": "2021-02-18T19:58:50.611Z", "updatedBy": -101, "userId": 1800091, "workingHourEnd": null, "workingHourStart": null} + $ref: '#/components/schemas/ProjectMember' failed: type: array description: "The emails." @@ -2525,6 +2695,12 @@ components: type: string description: "the email of a member" example: 'xxx@xxx.com' + ProjectMember: + type: object + example: {"id": 14329, "userId": 40159097, "role": "customer", "createdAt": "2021-02-24T12:34:45.074Z", "updatedAt": "2021-02-24T12:34:45.075Z", "createdBy": -101, "updatedBy": -101, "handle": "tester1234", "photoURL": null, "workingHourStart": "9:00", "workingHourEnd": "17:00", "timeZone": "Asia/Kolkata", "email": "sathya.jayabal@gmail.com"} + ProjectMemberInvite: + type: object + example: {"createdAt": "2021-02-24T11:02:12.673Z", "deletedAt": null, "role": "customer", "updatedBy": -101, "createdBy": -101, "id": 3686, "projectId": 16705, "userId": 23008602, "email": null, "deletedBy": null, "updatedAt": "2021-02-24T11:02:12.674Z", "status": "pending"} Error: required: - message diff --git a/src/common/helper.js b/src/common/helper.js index 10fc5e19..d144d061 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -1052,6 +1052,69 @@ async function createProjectMember (projectId, data, criteria) { return member } +/** + * List members of a project. + * @param {Object} currentUser the user who perform this operation + * @param {String} projectId the project id + * @param {Object} criteria the search criteria + * @returns {Array} the project members + */ +async function listProjectMembers (currentUser, projectId, criteria = {}) { + const token = (currentUser.hasManagePermission || currentUser.isMachine) + ? `Bearer ${await getM2MToken()}` + : currentUser.jwtToken + const { body: members } = await request + .get(`${config.TC_API}/projects/${projectId}/members`) + .query(criteria) + .set('Authorization', token) + .set('Accept', 'application/json') + localLogger.debug({ context: 'listProjectMembers', message: `response body: ${JSON.stringify(members)}` }) + return members +} + +/** + * List member invites of a project. + * @param {Object} currentUser the user who perform this operation + * @param {String} projectId the project id + * @param {Object} criteria the search criteria + * @returns {Array} the member invites + */ +async function listProjectMemberInvites (currentUser, projectId, criteria = {}) { + const token = (currentUser.hasManagePermission || currentUser.isMachine) + ? `Bearer ${await getM2MToken()}` + : currentUser.jwtToken + const { body: invites } = await request + .get(`${config.TC_API}/projects/${projectId}/invites`) + .query(criteria) + .set('Authorization', token) + .set('Accept', 'application/json') + localLogger.debug({ context: 'listProjectMemberInvites', message: `response body: ${JSON.stringify(invites)}` }) + return invites +} + +/** + * Remove a member from a project. + * @param {Object} currentUser the user who perform this operation + * @param {String} projectId the project id + * @param {String} projectMemberId the id of the project member + * @returns {undefined} + */ +async function deleteProjectMember (currentUser, projectId, projectMemberId) { + const token = (currentUser.hasManagePermission || currentUser.isMachine) + ? `Bearer ${await getM2MToken()}` + : currentUser.jwtToken + try { + await request + .delete(`${config.TC_API}/projects/${projectId}/members/${projectMemberId}`) + .set('Authorization', token) + } catch (err) { + if (err.status === HttpStatus.NOT_FOUND) { + throw new errors.NotFoundError(`projectMemberId: ${projectMemberId} "member" doesn't exist in project ${projectId}`) + } + throw err + } +} + module.exports = { getParamFromCliArgs, promptUser, @@ -1089,5 +1152,8 @@ module.exports = { checkIsMemberOfProject, getMemberDetailsByHandles, getMemberDetailsByEmails, - createProjectMember + createProjectMember, + listProjectMembers, + listProjectMemberInvites, + deleteProjectMember } diff --git a/src/controllers/TeamController.js b/src/controllers/TeamController.js index c60698a2..6cf1a6b4 100644 --- a/src/controllers/TeamController.js +++ b/src/controllers/TeamController.js @@ -50,7 +50,37 @@ async function sendEmail (req, res) { * @param res the response */ async function addMembers (req, res) { - res.send(await service.addMembers(req.authUser, req.params.id, req.body)) + res.send(await service.addMembers(req.authUser, req.params.id, req.query, req.body)) +} + +/** + * Search members in a team. + * @param req the request + * @param res the response + */ +async function searchMembers (req, res) { + const result = await service.searchMembers(req.authUser, req.params.id, req.query) + res.send(result.result) +} + +/** + * Search member invites for a team. + * @param req the request + * @param res the response + */ +async function searchInvites (req, res) { + const result = await service.searchInvites(req.authUser, req.params.id, req.query) + res.send(result.result) +} + +/** + * Remove a member from a team. + * @param req the request + * @param res the response + */ +async function deleteMember (req, res) { + await service.deleteMember(req.authUser, req.params.id, req.params.projectMemberId) + res.status(HttpStatus.NO_CONTENT).end() } module.exports = { @@ -58,5 +88,8 @@ module.exports = { getTeam, getTeamJob, sendEmail, - addMembers + addMembers, + searchMembers, + searchInvites, + deleteMember } diff --git a/src/routes/TeamRoutes.js b/src/routes/TeamRoutes.js index 0fd06402..3df14b0c 100644 --- a/src/routes/TeamRoutes.js +++ b/src/routes/TeamRoutes.js @@ -50,6 +50,28 @@ module.exports = { method: 'addMembers', auth: 'jwt', scopes: [constants.Scopes.READ_TAAS_TEAM] + }, + get: { + controller: 'TeamController', + method: 'searchMembers', + auth: 'jwt', + scopes: [constants.Scopes.READ_TAAS_TEAM] + } + }, + '/taas-teams/:id/invites': { + get: { + controller: 'TeamController', + method: 'searchInvites', + auth: 'jwt', + scopes: [constants.Scopes.READ_TAAS_TEAM] + } + }, + '/taas-teams/:id/members/:projectMemberId': { + delete: { + controller: 'TeamController', + method: 'deleteMember', + auth: 'jwt', + scopes: [constants.Scopes.READ_TAAS_TEAM] } } } diff --git a/src/services/TeamService.js b/src/services/TeamService.js index bc5ed3ff..b8576e62 100644 --- a/src/services/TeamService.js +++ b/src/services/TeamService.js @@ -340,14 +340,15 @@ sendEmail.schema = Joi.object().keys({ * * @param {Number} projectId project id * @param {String} userId user id + * @param {String} fields the fields to be returned * @returns {Object} the member added */ -async function _addMemberToProjectAsCustomer (projectId, userId) { +async function _addMemberToProjectAsCustomer (projectId, userId, fields) { try { const member = await helper.createProjectMember( projectId, { userId: userId, role: 'customer' }, - { fields: 'id,userId,role,createdAt,updatedAt,createdBy,updatedBy,handle,photoURL,workingHourStart,workingHourEnd,timeZone,email' } + { fields } ) return member } catch (err) { @@ -368,10 +369,11 @@ async function _addMemberToProjectAsCustomer (projectId, userId) { * Add members to a team by handle or email. * @param {Object} currentUser the user who perform this operation * @param {String} id the team id + * @params {Object} criteria the search criteria * @param {Object} data the object including members with handle/email to be added * @returns {Object} the success/failed added members */ -async function addMembers (currentUser, id, data) { +async function addMembers (currentUser, id, criteria, data) { await helper.getProjectById(currentUser, id) // check whether the user can access the project const result = { @@ -398,7 +400,7 @@ async function addMembers (currentUser, id, data) { result.failed.push({ error: 'User doesn\'t exist', handle }) return } - return _addMemberToProjectAsCustomer(id, membersByHandle[handle][0].userId) + return _addMemberToProjectAsCustomer(id, membersByHandle[handle][0].userId, criteria.fields) .then(member => { result.success.push(({ ...member, handle })) }).catch(err => { @@ -410,7 +412,7 @@ async function addMembers (currentUser, id, data) { result.failed.push({ error: 'User doesn\'t exist', email }) return } - return _addMemberToProjectAsCustomer(id, membersByEmail[email][0].id) + return _addMemberToProjectAsCustomer(id, membersByEmail[email][0].id, criteria.fields) .then(member => { result.success.push(({ ...member, email })) }).catch(err => { @@ -425,16 +427,86 @@ async function addMembers (currentUser, id, data) { addMembers.schema = Joi.object().keys({ currentUser: Joi.object().required(), id: Joi.number().integer().required(), + criteria: Joi.object().keys({ + fields: Joi.string() + }).required(), data: Joi.object().keys({ handles: Joi.array().items(Joi.string()), emails: Joi.array().items(Joi.string().email()) }).or('handles', 'emails').required() }).required() +/** + * Search members in a team. + * Serves as a proxy endpoint for `GET /projects/{projectId}/members`. + * + * @param {Object} currentUser the user who perform this operation. + * @param {String} id the team id + * @params {Object} criteria the search criteria + * @returns {Object} the search result + */ +async function searchMembers (currentUser, id, criteria) { + const result = await helper.listProjectMembers(currentUser, id, criteria) + return { result } +} + +searchMembers.schema = Joi.object().keys({ + currentUser: Joi.object().required(), + id: Joi.number().integer().required(), + criteria: Joi.object().keys({ + role: Joi.string(), + fields: Joi.string() + }).required() +}).required() + +/** + * Search member invites for a team. + * Serves as a proxy endpoint for `GET /projects/{projectId}/invites`. + * + * @param {Object} currentUser the user who perform this operation. + * @param {String} id the team id + * @params {Object} criteria the search criteria + * @returns {Object} the search result + */ +async function searchInvites (currentUser, id, criteria) { + const result = await helper.listProjectMemberInvites(currentUser, id, criteria) + return { result } +} + +searchInvites.schema = Joi.object().keys({ + currentUser: Joi.object().required(), + id: Joi.number().integer().required(), + criteria: Joi.object().keys({ + fields: Joi.string() + }).required() +}).required() + +/** + * Remove a member from a team. + * Serves as a proxy endpoint for `DELETE /projects/{projectId}/members/{id}`. + * + * @param {Object} currentUser the user who perform this operation. + * @param {String} id the team id + * @param {String} projectMemberId the id of the project member + * @returns {undefined} + */ +async function deleteMember (currentUser, id, projectMemberId) { + await helper.deleteProjectMember(currentUser, id, projectMemberId) +} + +deleteMember.schema = Joi.object().keys({ + currentUser: Joi.object().required(), + id: Joi.number().integer().required(), + projectMemberId: Joi.number().integer().required() +}).required() + module.exports = { searchTeams, getTeam, getTeamJob, sendEmail, - addMembers + addMembers, + searchMembers, + searchInvites, + deleteMember }