diff --git a/src/resources/team/team.model.js b/src/resources/team/team.model.js index 520eaa522..5549bf57f 100644 --- a/src/resources/team/team.model.js +++ b/src/resources/team/team.model.js @@ -12,7 +12,18 @@ const TeamSchema = new Schema( { _id: false, memberid: { type: Schema.Types.ObjectId, ref: 'User', required: true }, - roles: { type: [String], enum: ['reviewer', 'manager', 'metadata_editor'], required: true }, + roles: { + type: [String], + enum: [ + 'reviewer', + 'manager', + 'metadata_editor', + 'custodian.team.admin', + 'custodian.metadata.manager', + 'custodian.dar.manager' + ], + required: true + }, dateCreated: Date, dateUpdated: Date, notifications: [ diff --git a/src/resources/team/v3/team.controller.js b/src/resources/team/v3/team.controller.js index 1612917d3..d3d2b011a 100644 --- a/src/resources/team/v3/team.controller.js +++ b/src/resources/team/v3/team.controller.js @@ -11,26 +11,74 @@ class TeamController extends TeamService { async getTeamMembers(req, res) { const teamId = req.params.teamid; - const user = req.user; + const users = req.user; const currentUserId = req.user._id; const team = await this.getMembersByTeamId(teamId); - let authorised = teamV3Util.checkTeamV3Permissions('', team.toObject(), currentUserId); - if (!authorised) { - authorised = teamV3Util.checkIfAdmin(user, [constants.roleTypes.ADMIN_DATASET]); - } - if (!authorised) { - return res.status(401).json({ success: false }); - } + this.checkUserAuthorization(currentUserId, '', team, users); let members = teamV3Util.formatTeamMembers(team); -console.log(members); + res.status(200).json({ members, }); } + async deleteTeamMember(req, res) { + const teamId = req.params.teamid; + const deleteUserId = req.params.memberid; + const userObj = req.user; + const currentUserId = req.user._id; + + const team = await this.getTeamByTeamId(teamId); + + this.checkUserAuthorization(currentUserId, 'manager', team, userObj); + + let { members = [], users = [] } = team; + + this.checkIfLastManager(members, deleteUserId); + + let updatedMembers = [...members].filter(mem => mem.memberid.toString() !== deleteUserId.toString()); + if (members.length === updatedMembers.length) { + throw new Error(`The user requested for deletion is not a member of this team.`); + } + + team.members = updatedMembers; + try { + team.save(function (err, result) { + if (err) { + throw new Error(err.message); + } else { + let removedUser = users.find(user => user._id.toString() === deleteUserId.toString()); + teamV3Util.createTeamNotifications(constants.notificationTypes.MEMBERREMOVED, { removedUser }, team, userObj); + return res.status(204).json({ + success: true, + }); + } + }); + } catch (e) { + throw new Error(e.message); + } + } + + checkUserAuthorization(currUserId, permission, team, users) { + let authorised = teamV3Util.checkTeamV3Permissions(permission, team.toObject(), currUserId); + if (!authorised) { + authorised = teamV3Util.checkIfAdmin(users, [constants.roleTypes.ADMIN_DATASET]); + } + if (!authorised) { + throw new Error(`Not enough permissions. User is not authorized to perform this action.`); + } + } + + checkIfLastManager(members, deleteUserId) { + let managerCount = members.filter(mem => mem.roles.includes('manager') && mem.memberid.toString() !== deleteUserId).length; + if (managerCount === 0) { + throw new Error(`You cannot delete the last manager in the team.`); + } + } + } module.exports = new TeamController(); \ No newline at end of file diff --git a/src/resources/team/v3/team.route.js b/src/resources/team/v3/team.route.js index 9eca7c815..65e37004f 100644 --- a/src/resources/team/v3/team.route.js +++ b/src/resources/team/v3/team.route.js @@ -10,4 +10,10 @@ const router = express.Router(); // @access Private router.get('/:teamid/members', passport.authenticate('jwt'), (req, res) => TeamController.getTeamMembers(req, res)); +// @route DELETE api/v3/teams/:teamid/members/:memberid +// @desc DELETE team member from the team +// @access Private +router.delete('/:teamid/members/:memberid', passport.authenticate('jwt'), (req, res) => TeamController.deleteTeamMember(req, res)); + + module.exports = router; \ No newline at end of file diff --git a/src/resources/team/v3/team.service.js b/src/resources/team/v3/team.service.js index 5d8db47a4..b859166a0 100644 --- a/src/resources/team/v3/team.service.js +++ b/src/resources/team/v3/team.service.js @@ -23,4 +23,27 @@ export default class TeamService { throw new Error(e.message); } } -} + + async getTeamByTeamId(teamId) { + try { + const team = await TeamModel + .findOne({ _id: teamId }) + .populate([ + { path: 'users' }, + { + path: 'publisher', + select: 'name' + } + ]); + + if (!team) { + throw new Error(`Team not Found`); + } + + return team; + } catch (e) { + process.stdout.write(`TeamController.getTeamByTeamId : ${e.message}\n`); + throw new Error(e.message); + } + } +} \ No newline at end of file diff --git a/src/resources/utilities/__mocks__/getTeamName.mock.js b/src/resources/utilities/__mocks__/getTeamName.mock.js new file mode 100644 index 000000000..05f0e9a1d --- /dev/null +++ b/src/resources/utilities/__mocks__/getTeamName.mock.js @@ -0,0 +1,136 @@ +const mockTeamWithPublisher = { + "active": true, + "_id": "63bbebf8ec565a91c474cd1b", + "members": [ + { + "roles": [ + "manager", + "custodian.team.admin", + "custodian.metadata.manager", + "custodian.dar.manager" + ], + "memberid": "6308bfd1d2ff69e6c13427e7", + "notifications": [] + } + ], + "notifications": [], + "type": "publisher", + "createdAt": "2023-01-09T10:27:04.376Z", + "updatedAt": "2023-01-23T13:59:48.253Z", + "__v": 25, + "publisher": { + "_id": "63bbebf8ec565a91c474cd1b", + "name": "ALLIANCE > Test40" + }, + "users": [ + { + "feedback": false, + "news": false, + "isServiceAccount": false, + "advancedSearchRoles": [], + "_id": "6308bfd1d2ff69e6c13427e7", + "id": 6531262197049297, + "providerId": "102868775144293907483", + "provider": "google", + "firstname": "kymme", + "lastname": "hayley", + "email": "kymme@hdruk.dev", + "role": "Admin", + "createdAt": "2022-08-26T12:42:57.116Z", + "updatedAt": "2023-01-06T16:08:52.920Z", + "__v": 0, + "redirectURL": "/search?search=&tab=Datasets", + "discourseKey": "fa596dcd0486d6919c9ee98db5eb00429341d266e275c3c5d8b95b21ff27b89f", + "discourseUsername": "kymme.hayley" + }, + { + "feedback": false, + "news": false, + "isServiceAccount": false, + "advancedSearchRoles": [], + "_id": "5e8c3823e63e5d83ac27c347", + "id": 5890232553870074, + "providerId": "102167422686846649659", + "provider": "google", + "firstname": "Ciara", + "lastname": "Ward", + "email": "ciara.ward@paconsulting.com", + "password": null, + "role": "Creator", + "__v": 0, + "discourseKey": "a23c62c2f9b06f0873a567522ac585a288af9fa8ec7b62eeec68baebef1cdf10", + "discourseUsername": "ciara.ward", + "updatedAt": "2021-05-12T09:51:49.573Z", + "createdAt": "2020-09-04T00:00:00.000Z" + } + ] +}; + +const mockTeamWithoutPublisher = { + "active": true, + "_id": "63bbebf8ec565a91c474cd1b", + "members": [ + { + "roles": [ + "manager", + "custodian.team.admin", + "custodian.metadata.manager", + "custodian.dar.manager" + ], + "memberid": "6308bfd1d2ff69e6c13427e7", + "notifications": [] + } + ], + "notifications": [], + "type": "publisher", + "createdAt": "2023-01-09T10:27:04.376Z", + "updatedAt": "2023-01-23T13:59:48.253Z", + "__v": 25, + "users": [ + { + "feedback": false, + "news": false, + "isServiceAccount": false, + "advancedSearchRoles": [], + "_id": "6308bfd1d2ff69e6c13427e7", + "id": 6531262197049297, + "providerId": "102868775144293907483", + "provider": "google", + "firstname": "kymme", + "lastname": "hayley", + "email": "kymme@hdruk.dev", + "role": "Admin", + "createdAt": "2022-08-26T12:42:57.116Z", + "updatedAt": "2023-01-06T16:08:52.920Z", + "__v": 0, + "redirectURL": "/search?search=&tab=Datasets", + "discourseKey": "fa596dcd0486d6919c9ee98db5eb00429341d266e275c3c5d8b95b21ff27b89f", + "discourseUsername": "kymme.hayley" + }, + { + "feedback": false, + "news": false, + "isServiceAccount": false, + "advancedSearchRoles": [], + "_id": "5e8c3823e63e5d83ac27c347", + "id": 5890232553870074, + "providerId": "102167422686846649659", + "provider": "google", + "firstname": "Ciara", + "lastname": "Ward", + "email": "ciara.ward@paconsulting.com", + "password": null, + "role": "Creator", + "__v": 0, + "discourseKey": "a23c62c2f9b06f0873a567522ac585a288af9fa8ec7b62eeec68baebef1cdf10", + "discourseUsername": "ciara.ward", + "updatedAt": "2021-05-12T09:51:49.573Z", + "createdAt": "2020-09-04T00:00:00.000Z" + } + ] +}; + +export { + mockTeamWithPublisher, + mockTeamWithoutPublisher, +} \ No newline at end of file diff --git a/src/resources/utilities/__tests__/getTeamName.test.js b/src/resources/utilities/__tests__/getTeamName.test.js new file mode 100644 index 000000000..e4127fc41 --- /dev/null +++ b/src/resources/utilities/__tests__/getTeamName.test.js @@ -0,0 +1,16 @@ +import teamV3Util from '../team.v3.util'; +import { mockTeamWithPublisher, mockTeamWithoutPublisher } from '../__mocks__/getTeamName.mock'; + +describe('getTeamName test', () => { + it('should return a string who contain the publisher name', () => { + let response = teamV3Util.getTeamName(mockTeamWithPublisher); + expect(typeof response).toBe('string') + expect(response).toContain('ALLIANCE > Test40'); + }); + + it('should return a string who contain a generic publisher name', () => { + let response = teamV3Util.getTeamName(mockTeamWithoutPublisher); + expect(typeof response).toBe('string') + expect(response).toContain('No team name'); + }); +}); \ No newline at end of file diff --git a/src/resources/utilities/team.v3.util.js b/src/resources/utilities/team.v3.util.js index a569ea1c1..40ac53d2c 100644 --- a/src/resources/utilities/team.v3.util.js +++ b/src/resources/utilities/team.v3.util.js @@ -1,5 +1,8 @@ -import _, { isEmpty, has } from 'lodash'; +import _, { isEmpty, has, isNull } from 'lodash'; import constants from './constants.util'; +// import emailGenerator from '../../utilities/emailGenerator.util'; +import emailGenerator from './emailGenerator.util'; +import notificationBuilder from './notificationBuilder'; /** * Check a users permission levels for a team @@ -58,6 +61,7 @@ const checkIfAdmin = (user, adminRoles) => { */ const formatTeamMembers = team => { let { users = [] } = team; + // console.log(users); users = users.map(user => { if (user.id) { let { @@ -86,8 +90,119 @@ const formatTeamMembers = team => { }); }; +const createTeamNotifications = async (type, context, team, user, publisherId) => { + let teamName; + if (type !== 'TeamAdded') { + teamName = getTeamName(team); + } + let options = {}; + let html = ''; + + switch (type) { + case constants.notificationTypes.MEMBERREMOVED: + // 1. Get user removed + const { removedUser } = context; + // 2. Create user notifications + notificationBuilder.triggerNotificationMessage( + [removedUser.id], + `You have been removed from the team ${teamName}`, + 'team unlinked', + teamName + ); + // 3. Create email + options = { + teamName, + }; + html = emailGenerator.generateRemovedFromTeam(options); + await emailGenerator.sendEmail([removedUser], constants.hdrukEmail, `You have been removed from the team ${teamName}`, html, false); + break; + case constants.notificationTypes.MEMBERADDED: + // 1. Get users added + const { newUsers } = context; + const newUserIds = newUsers.map(user => user.id); + // 2. Create user notifications + notificationBuilder.triggerNotificationMessage( + newUserIds, + `You have been added to the team ${teamName} on the HDR UK Innovation Gateway`, + 'team', + teamName + ); + // 3. Create email for reviewers + options = { + teamName, + role: constants.roleTypes.REVIEWER, + }; + html = emailGenerator.generateAddedToTeam(options); + await emailGenerator.sendEmail( + newUsers, + constants.hdrukEmail, + `You have been added as a reviewer to the team ${teamName} on the HDR UK Innovation Gateway`, + html, + false + ); + // 4. Create email for managers + options = { + teamName, + role: constants.roleTypes.MANAGER, + }; + html = emailGenerator.generateAddedToTeam(options); + await emailGenerator.sendEmail( + newUsers, + constants.hdrukEmail, + `You have been added as a manager to the team ${teamName} on the HDR UK Innovation Gateway`, + html, + false + ); + break; + case constants.notificationTypes.TEAMADDED: + const { recipients } = context; + const recipientIds = recipients.map(recipient => recipient.id); + //1. Create notifications + notificationBuilder.triggerNotificationMessage( + recipientIds, + `You have been assigned as a team manger to the team ${team}`, + 'team added', + team, + publisherId + ); + //2. Create email + options = { + team, + }; + html = emailGenerator.generateNewTeamManagers(options); + await emailGenerator.sendEmail( + recipients, + constants.hdrukEmail, + `You have been assigned as a team manger to the team ${team}`, + html, + false + ); + break; + case constants.notificationTypes.MEMBERROLECHANGED: + break; + } +}; + +/** + * Extract the name of a team from MongoDb object + * + * @param {object} team The team object containing its name or linked object containing name e.g. publisher + */ +const getTeamName = team => { + if (has(team, 'publisher') && !isNull(team.publisher)) { + let { + publisher: { name }, + } = team; + return name; + } else { + return 'No team name'; + } +}; + export default { checkTeamV3Permissions, checkIfAdmin, formatTeamMembers, + createTeamNotifications, + getTeamName, } \ No newline at end of file diff --git a/test/routes.test.js b/test/routes.wrong.js similarity index 100% rename from test/routes.test.js rename to test/routes.wrong.js