diff --git a/common/locales/en/api-v3.json b/common/locales/en/api-v3.json index cd3133ffd9..e82133c009 100644 --- a/common/locales/en/api-v3.json +++ b/common/locales/en/api-v3.json @@ -53,5 +53,6 @@ "userAlreadyInAParty": "User already in a party.", "userWithIDNotFound": "User with id \"<%= userId %>\" not found.", "uuidsMustBeAnArray": "UUIDs invites must be a an Array.", - "emailsMustBeAnArray": "Email invites must be a an Array." + "emailsMustBeAnArray": "Email invites must be a an Array.", + "canOnlyInviteMaxInvites": "You can only invite \"<%= maxInvites %>\" at a time" } diff --git a/test/api/v3/integration/groups/POST-groups_invite.test.js b/test/api/v3/integration/groups/POST-groups_invite.test.js index 1bb604bd19..2466bdf649 100644 --- a/test/api/v3/integration/groups/POST-groups_invite.test.js +++ b/test/api/v3/integration/groups/POST-groups_invite.test.js @@ -2,6 +2,9 @@ import { generateUser, translate as t, } from '../../../../helpers/api-integration.helper'; +import { v4 as generateUUID } from 'uuid'; + +const INVITES_LIMIT = 100; describe('Post /groups/:groupId/invite', () => { let inviter; @@ -18,7 +21,7 @@ describe('Post /groups/:groupId/invite', () => { describe('user id invites', () => { it('returns an error when invited user is not found', async () => { - let fakeID = '206039c6-24e4-4b9f-8a31-61cbb9aa3f66'; + let fakeID = generateUUID(); await expect(inviter.post(`/groups/${group._id}/invite`, { uuids: [fakeID], @@ -31,7 +34,7 @@ describe('Post /groups/:groupId/invite', () => { }); it('returns an error when uuids is not an array', async () => { - let fakeID = '206039c6-24e4-4b9f-8a31-61cbb9aa3f66'; + let fakeID = generateUUID(); await expect(inviter.post(`/groups/${group._id}/invite`, { uuids: {fakeID}, @@ -50,6 +53,23 @@ describe('Post /groups/:groupId/invite', () => { .to.eventually.be.empty; }); + it('returns an error when there are more than INVITES_LIMIT uuids', async () => { + let uuids = []; + + for (let i = 0; i < 101; i += 1) { + uuids.push(generateUUID()); + } + + await expect(inviter.post(`/groups/${group._id}/invite`, { + uuids, + })) + .to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: t('canOnlyInviteMaxInvites', {maxInvites: INVITES_LIMIT}), + }); + }); + it('invites a user to a group by uuid', async () => { let userToInvite = await generateUser(); @@ -85,10 +105,24 @@ describe('Post /groups/:groupId/invite', () => { await expect(userToInvite.get('/user')).to.eventually.have.deep.property('invitations.guilds[0].id', group._id); await expect(userToInvite2.get('/user')).to.eventually.have.deep.property('invitations.guilds[0].id', group._id); }); + + it('returns an error when inviting multiple users and a user is not found', async () => { + let userToInvite = await generateUser(); + let fakeID = generateUUID(); + + await expect(inviter.post(`/groups/${group._id}/invite`, { + uuids: [userToInvite._id, fakeID], + })) + .to.eventually.be.rejected.and.eql({ + code: 404, + error: 'NotFound', + message: t('userWithIDNotFound', {userId: fakeID}), + }); + }); }); describe('email invites', () => { - let testInvite = {name: 'test', email: 'test@habitca.com'}; + let testInvite = {name: 'test', email: 'test@habitica.com'}; it('returns an error when invite is missing an email', async () => { await expect(inviter.post(`/groups/${group._id}/invite`, { @@ -119,6 +153,23 @@ describe('Post /groups/:groupId/invite', () => { .to.eventually.be.empty; }); + it('returns an error when there are more than INVITES_LIMIT emails', async () => { + let emails = []; + + for (let i = 0; i < 101; i += 1) { + emails.push(`${generateUUID()}@habitica.com`); + } + + await expect(inviter.post(`/groups/${group._id}/invite`, { + emails, + })) + .to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: t('canOnlyInviteMaxInvites', {maxInvites: INVITES_LIMIT}), + }); + }); + it('invites a user to a group by email', async () => { await expect(inviter.post(`/groups/${group._id}/invite`, { emails: [testInvite], @@ -127,7 +178,7 @@ describe('Post /groups/:groupId/invite', () => { it('invites multiple users to a group by email', async () => { await expect(inviter.post(`/groups/${group._id}/invite`, { - emails: [testInvite, {name: 'test2', email: 'test2@habitca.com'}], + emails: [testInvite, {name: 'test2', email: 'test2@habitica.com'}], })).to.exist; }); }); @@ -142,11 +193,34 @@ describe('Post /groups/:groupId/invite', () => { }); }); + it('returns an error when there are more than INVITES_LIMIT uuids and emails', async () => { + let emails = []; + let uuids = []; + + for (let i = 0; i < 50; i += 1) { + emails.push(`${generateUUID()}@habitica.com`); + } + + for (let i = 0; i < 51; i += 1) { + uuids.push(generateUUID()); + } + + await expect(inviter.post(`/groups/${group._id}/invite`, { + emails, + uuids, + })) + .to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: t('canOnlyInviteMaxInvites', {maxInvites: INVITES_LIMIT}), + }); + }); + it('invites users to a group by uuid and email', async () => { let newUser = await generateUser(); let invite = await inviter.post(`/groups/${group._id}/invite`, { uuids: [newUser._id], - emails: [{name: 'test', email: 'test@habitca.com'}], + emails: [{name: 'test', email: 'test@habitica.com'}], }); let invitedUser = await newUser.get('/user'); diff --git a/website/src/controllers/api-v3/groups.js b/website/src/controllers/api-v3/groups.js index 0114240481..d1ca3f6e13 100644 --- a/website/src/controllers/api-v3/groups.js +++ b/website/src/controllers/api-v3/groups.js @@ -2,7 +2,10 @@ import { authWithHeaders } from '../../middlewares/api-v3/auth'; import Q from 'q'; import _ from 'lodash'; import cron from '../../middlewares/api-v3/cron'; -import { model as Group } from '../../models/group'; +import { + INVITES_LIMIT, + model as Group, +} from '../../models/group'; import { model as User } from '../../models/user'; import { model as EmailUnsubscription } from '../../models/emailUnsubscription'; import { @@ -407,7 +410,7 @@ async function _inviteByUUID (uuid, group, inviter, req, res) { if (_.find(userToInvite.invitations.guilds, {id: group._id})) { throw new NotAuthorized(res.t('userAlreadyInvitedToGroup')); } - userToInvite.invitations.guilds.push({id: group._id, name: group.name, inviter: res.locals.user._id}); + userToInvite.invitations.guilds.push({id: group._id, name: group.name, inviter: inviter._id}); } else if (group.type === 'party') { if (!_.isEmpty(userToInvite.invitations.party)) { throw new NotAuthorized(res.t('userAlreadyPendingInvitation')); @@ -417,7 +420,7 @@ async function _inviteByUUID (uuid, group, inviter, req, res) { } // @TODO: Why was this here? // req.body.type in 'guild', 'party' - userToInvite.invitations.party = {id: group._id, name: group.name, inviter: res.locals.user._id}; + userToInvite.invitations.party = {id: group._id, name: group.name, inviter: inviter._id}; } let groupLabel = group.type === 'guild' ? 'Guild' : 'Party'; @@ -532,13 +535,26 @@ api.inviteToGroup = { } let results = []; + let totalInvites = 0; - if (uuids && !uuidsIsArray) { - throw new BadRequest(res.t('uuidsMustBeAnArray')); + if (uuids) { + if (!uuidsIsArray) { + throw new BadRequest(res.t('uuidsMustBeAnArray')); + } else { + totalInvites += uuids.length; + } } - if (emails && !emailsIsArray) { - throw new BadRequest(res.t('emailsMustBeAnArray')); + if (emails) { + if (!emailsIsArray) { + throw new BadRequest(res.t('emailsMustBeAnArray')); + } else { + totalInvites += emails.length; + } + } + + if (totalInvites > INVITES_LIMIT) { + throw new BadRequest(res.t('canOnlyInviteMaxInvites', {maxInvites: INVITES_LIMIT})); } if (uuids) { diff --git a/website/src/models/group.js b/website/src/models/group.js index a45d2eaa60..27ca80e73f 100644 --- a/website/src/models/group.js +++ b/website/src/models/group.js @@ -520,3 +520,5 @@ model.count({_id: 'habitrpg'}, (err, ct) => { privacy: 'public', }).save(); }); + +export const INVITES_LIMIT = 100;