Added email invite limit (#8664)

* Added email invite limit

* change error message for sending too many invitations to instruct them to email us

* fix test error message to use variable in locales string

* add comment to warn about keeping INVITES_LIMIT low

If INVITES_LIMIT is allowed to be greater than MAX_EMAIL_INVITES_BY_USER
then the inviter can send more than MAX_EMAIL_INVITES_BY_USER invitations
at once.
This commit is contained in:
Keith Holliday
2017-04-12 14:54:35 -06:00
committed by Sabe Jones
parent 0442b87608
commit 7d42e8fc71
5 changed files with 40 additions and 1 deletions

View File

@@ -4,9 +4,11 @@ import {
translate as t, translate as t,
} from '../../../../helpers/api-v3-integration.helper'; } from '../../../../helpers/api-v3-integration.helper';
import { v4 as generateUUID } from 'uuid'; import { v4 as generateUUID } from 'uuid';
import nconf from 'nconf';
const INVITES_LIMIT = 100; const INVITES_LIMIT = 100;
const PARTY_LIMIT_MEMBERS = 30; const PARTY_LIMIT_MEMBERS = 30;
const MAX_EMAIL_INVITES_BY_USER = 200;
describe('Post /groups/:groupId/invite', () => { describe('Post /groups/:groupId/invite', () => {
let inviter; 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 () => { it('invites a user to a group by email', async () => {
let res = await inviter.post(`/groups/${group._id}/invite`, { let res = await inviter.post(`/groups/${group._id}/invite`, {
emails: [testInvite], emails: [testInvite],
inviter: 'inviter name', inviter: 'inviter name',
}); });
let updatedUser = await inviter.sync();
expect(res).to.exist; expect(res).to.exist;
expect(updatedUser.invitesSent).to.eql(1);
}); });
it('invites multiple users to a group by email', async () => { 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'}], emails: [testInvite, {name: 'test2', email: 'test2@habitica.com'}],
}); });
let updatedUser = await inviter.sync();
expect(res).to.exist; expect(res).to.exist;
expect(updatedUser.invitesSent).to.eql(2);
}); });
}); });

View File

@@ -148,6 +148,7 @@
"invitationsSent": "Invitations sent!", "invitationsSent": "Invitations sent!",
"invitationSent": "Invitation sent!", "invitationSent": "Invitation sent!",
"inviteAlertInfo2": "Or share this link (copy/paste):", "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 %>", "sendGiftHeading": "Send Gift to <%= name %>",
"sendGiftGemsBalance": "From <%= number %> Gems", "sendGiftGemsBalance": "From <%= number %> Gems",
"sendGiftCost": "Total: $<%= cost %> USD", "sendGiftCost": "Total: $<%= cost %> USD",

View File

@@ -1,6 +1,7 @@
import { authWithHeaders } from '../../middlewares/auth'; import { authWithHeaders } from '../../middlewares/auth';
import Bluebird from 'bluebird'; import Bluebird from 'bluebird';
import _ from 'lodash'; import _ from 'lodash';
import nconf from 'nconf';
import { import {
model as Group, model as Group,
basicFields as basicGroupFields, basicFields as basicGroupFields,
@@ -27,6 +28,9 @@ import amzLib from '../../libs/amazonPayments';
import shared from '../../../common'; import shared from '../../../common';
import apiMessages from '../../libs/apiMessages'; import apiMessages from '../../libs/apiMessages';
const MAX_EMAIL_INVITES_BY_USER = 200;
const TECH_ASSISTANCE_EMAIL = nconf.get('EMAILS:TECH_ASSISTANCE_EMAIL');
/** /**
* @apiDefine GroupBodyInvalid * @apiDefine GroupBodyInvalid
* @apiError (400) {BadRequest} GroupBodyInvalid A parameter in the group body was invalid. * @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(); 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(); let validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors; if (validationErrors) throw validationErrors;
@@ -1079,6 +1085,8 @@ api.inviteToGroup = {
if (emails) { if (emails) {
let emailInvites = emails.map((invite) => _inviteByEmail(invite, group, user, req, res)); let emailInvites = emails.map((invite) => _inviteByEmail(invite, group, user, req, res));
user.invitesSent += emails.length;
await user.save();
let emailResults = await Bluebird.all(emailInvites); let emailResults = await Bluebird.all(emailInvites);
results.push(...emailResults); results.push(...emailResults);
} }

View File

@@ -35,7 +35,7 @@ import stripePayments from '../libs/stripePayments';
const questScrolls = shared.content.quests; const questScrolls = shared.content.quests;
const Schema = mongoose.Schema; 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; export const TAVERN_ID = shared.TAVERN_ID;
const NO_CHAT_NOTIFICATIONS = [TAVERN_ID]; const NO_CHAT_NOTIFICATIONS = [TAVERN_ID];

View File

@@ -550,6 +550,7 @@ let schema = new Schema({
}}, }},
webhooks: [WebhookSchema], webhooks: [WebhookSchema],
loginIncentives: {type: Number, default: 0}, loginIncentives: {type: Number, default: 0},
invitesSent: {type: Number, default: 0},
}, { }, {
strict: true, strict: true,
minimize: false, // So empty objects are returned minimize: false, // So empty objects are returned