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 ecaea4c74f..cab63ac970 100644 --- a/test/api/v3/integration/groups/POST-groups_invite.test.js +++ b/test/api/v3/integration/groups/POST-groups_invite.test.js @@ -4,9 +4,11 @@ import { translate as t, } from '../../../../helpers/api-v3-integration.helper'; import { v4 as generateUUID } from 'uuid'; +import nconf from 'nconf'; const INVITES_LIMIT = 100; const PARTY_LIMIT_MEMBERS = 30; +const MAX_EMAIL_INVITES_BY_USER = 200; describe('Post /groups/:groupId/invite', () => { let inviter; @@ -205,13 +207,37 @@ describe('Post /groups/:groupId/invite', () => { }); }); + it('returns an error when a user has sent the max number of email invites', async () => { + let inviterWithMax = await generateUser({ + invitesSent: MAX_EMAIL_INVITES_BY_USER, + balance: 4, + }); + let tmpGroup = await inviterWithMax.post('/groups', { + name: groupName, + type: 'guild', + }); + + await expect(inviterWithMax.post(`/groups/${tmpGroup._id}/invite`, { + emails: [testInvite], + inviter: 'inviter name', + })) + .to.eventually.be.rejected.and.eql({ + code: 401, + error: 'NotAuthorized', + message: t('inviteLimitReached', {techAssistanceEmail: nconf.get('EMAILS:TECH_ASSISTANCE_EMAIL')}), + }); + }); + it('invites a user to a group by email', async () => { let res = await inviter.post(`/groups/${group._id}/invite`, { emails: [testInvite], inviter: 'inviter name', }); + let updatedUser = await inviter.sync(); + expect(res).to.exist; + expect(updatedUser.invitesSent).to.eql(1); }); it('invites multiple users to a group by email', async () => { @@ -219,7 +245,10 @@ describe('Post /groups/:groupId/invite', () => { emails: [testInvite, {name: 'test2', email: 'test2@habitica.com'}], }); + let updatedUser = await inviter.sync(); + expect(res).to.exist; + expect(updatedUser.invitesSent).to.eql(2); }); }); diff --git a/website/common/locales/en/groups.json b/website/common/locales/en/groups.json index 87c82d5152..201a28d123 100644 --- a/website/common/locales/en/groups.json +++ b/website/common/locales/en/groups.json @@ -148,6 +148,7 @@ "invitationsSent": "Invitations sent!", "invitationSent": "Invitation sent!", "inviteAlertInfo2": "Or share this link (copy/paste):", + "inviteLimitReached": "You have already sent the maximum number of email invitations. We have a limit to prevent spamming, however if you would like more, please contact us at <%= techAssistanceEmail %> and we'll be happy to discuss it!", "sendGiftHeading": "Send Gift to <%= name %>", "sendGiftGemsBalance": "From <%= number %> Gems", "sendGiftCost": "Total: $<%= cost %> USD", diff --git a/website/server/controllers/api-v3/groups.js b/website/server/controllers/api-v3/groups.js index cc4997384e..6641d07bec 100644 --- a/website/server/controllers/api-v3/groups.js +++ b/website/server/controllers/api-v3/groups.js @@ -1,6 +1,7 @@ import { authWithHeaders } from '../../middlewares/auth'; import Bluebird from 'bluebird'; import _ from 'lodash'; +import nconf from 'nconf'; import { model as Group, basicFields as basicGroupFields, @@ -27,6 +28,9 @@ import amzLib from '../../libs/amazonPayments'; import shared from '../../../common'; import apiMessages from '../../libs/apiMessages'; +const MAX_EMAIL_INVITES_BY_USER = 200; +const TECH_ASSISTANCE_EMAIL = nconf.get('EMAILS:TECH_ASSISTANCE_EMAIL'); + /** * @apiDefine GroupBodyInvalid * @apiError (400) {BadRequest} GroupBodyInvalid A parameter in the group body was invalid. @@ -1056,6 +1060,8 @@ api.inviteToGroup = { req.checkParams('groupId', res.t('groupIdRequired')).notEmpty(); + if (user.invitesSent >= MAX_EMAIL_INVITES_BY_USER) throw new NotAuthorized(res.t('inviteLimitReached', { techAssistanceEmail: TECH_ASSISTANCE_EMAIL })); + let validationErrors = req.validationErrors(); if (validationErrors) throw validationErrors; @@ -1079,6 +1085,8 @@ api.inviteToGroup = { if (emails) { let emailInvites = emails.map((invite) => _inviteByEmail(invite, group, user, req, res)); + user.invitesSent += emails.length; + await user.save(); let emailResults = await Bluebird.all(emailInvites); results.push(...emailResults); } diff --git a/website/server/models/group.js b/website/server/models/group.js index 2400243284..2675424d0e 100644 --- a/website/server/models/group.js +++ b/website/server/models/group.js @@ -35,7 +35,7 @@ import stripePayments from '../libs/stripePayments'; const questScrolls = shared.content.quests; const Schema = mongoose.Schema; -export const INVITES_LIMIT = 100; +export const INVITES_LIMIT = 100; // must not be greater than MAX_EMAIL_INVITES_BY_USER export const TAVERN_ID = shared.TAVERN_ID; const NO_CHAT_NOTIFICATIONS = [TAVERN_ID]; diff --git a/website/server/models/user/schema.js b/website/server/models/user/schema.js index 3cb7c9ecd4..364d4d1958 100644 --- a/website/server/models/user/schema.js +++ b/website/server/models/user/schema.js @@ -550,6 +550,7 @@ let schema = new Schema({ }}, webhooks: [WebhookSchema], loginIncentives: {type: Number, default: 0}, + invitesSent: {type: Number, default: 0}, }, { strict: true, minimize: false, // So empty objects are returned