From 9812d20d22f4e1d8ee683784ef9e9aa005314f5d Mon Sep 17 00:00:00 2001 From: Benjamin Piouffle Date: Sun, 16 Dec 2018 22:54:07 +0100 Subject: [PATCH] feat(VirtualCard): Add cache limitations for create --- config/default.json | 3 +- config/development.json | 3 +- .../opencollective/virtualcard.js | 47 ++++++++++++++++--- 3 files changed, 44 insertions(+), 9 deletions(-) diff --git a/config/default.json b/config/default.json index f3730bf356a..0cd7cf0cb07 100644 --- a/config/default.json +++ b/config/default.json @@ -42,7 +42,8 @@ "perEmail": 10, "perEmailForCollective": 2, "perIp": 5 - } + }, + "virtualCardsPerHour": 100 }, "email": { "from": "Open Collective " diff --git a/config/development.json b/config/development.json index 267728cdb79..697848f37a3 100644 --- a/config/development.json +++ b/config/development.json @@ -17,7 +17,8 @@ "perEmail": 100, "perEmailForCollective": 20, "perIp": 50 - } + }, + "virtualCardsPerHour": 1000 }, "maintenancedb": { "database": "postgres", diff --git a/server/paymentProviders/opencollective/virtualcard.js b/server/paymentProviders/opencollective/virtualcard.js index 819a8ea7a23..7b7f2c2479a 100644 --- a/server/paymentProviders/opencollective/virtualcard.js +++ b/server/paymentProviders/opencollective/virtualcard.js @@ -1,11 +1,14 @@ import moment from 'moment'; import uuidv4 from 'uuid/v4'; import { get, times } from 'lodash'; +import config from 'config'; + import models, { Op, sequelize } from '../../models'; import * as libpayments from '../../lib/payments'; import * as currency from '../../lib/currency'; import { formatCurrency, isValidEmail } from '../../lib/utils'; import emailLib from '../../lib/email'; +import cache from '../../lib/cache'; /** * Virtual Card Payment method - This payment Method works basically as an alias @@ -14,6 +17,9 @@ import emailLib from '../../lib/email'; * the virtual card payment method that first processed the order. */ +const LIMIT_REACHED_ERROR = + 'Gift card create failed because you reached limit. Please try again later or contact support@opencollective.com'; + /** Get the balance of a virtual card card * @param {models.PaymentMethod} paymentMethod is the instance of the * virtual card payment method. @@ -144,11 +150,16 @@ async function processOrder(order) { an extra property "code" that is basically the last 8 digits of the UUID */ async function create(args, remoteUser) { + if (!(await checkCreateLimit(args.CollectiveId, 1))) { + throw new Error(LIMIT_REACHED_ERROR); + } + const collective = await models.Collective.findById(args.CollectiveId); const sourcePaymentMethod = await getSourcePaymentMethodFromCreateArgs(args, collective); const createParams = getCreateParams(args, collective, sourcePaymentMethod, remoteUser); const virtualCard = await models.PaymentMethod.create(createParams); sendVirtualCardCreatedEmail(virtualCard, collective); + registerCreateInCache(args.CollectiveId, 1); return virtualCard; } @@ -161,16 +172,18 @@ async function create(args, remoteUser) { * @param {integer} count */ export async function bulkCreateVirtualCards(args, remoteUser, count) { - if (count > 100) { - throw new Error('Cannot create more than 100 virtual cards in one pass.'); - } else if (!count) { + if (!count) { return []; + } else if (!(await checkCreateLimit(args.CollectiveId, count))) { + throw new Error(LIMIT_REACHED_ERROR); } const collective = await models.Collective.findById(args.CollectiveId); const sourcePaymentMethod = await getSourcePaymentMethodFromCreateArgs(args, collective); const virtualCardsParams = times(count, () => getCreateParams(args, collective, sourcePaymentMethod, remoteUser)); - return await models.PaymentMethod.bulkCreate(virtualCardsParams); + const virtualCards = await models.PaymentMethod.bulkCreate(virtualCardsParams); + registerCreateInCache(args.CollectiveId, virtualCards.length); + return virtualCards; } /** @@ -181,10 +194,10 @@ export async function bulkCreateVirtualCards(args, remoteUser, count) { * @param {integer} count */ export async function createVirtualCardsForEmails(args, remoteUser, emails) { - if (emails.length > 100) { - throw new Error('Cannot create more than 100 virtual cards in one pass.'); - } else if (emails.length === 0) { + if (emails.length === 0) { return []; + } else if (!(await checkCreateLimit(args.CollectiveId, emails.length))) { + throw new Error(LIMIT_REACHED_ERROR); } const collective = await models.Collective.findById(args.CollectiveId); @@ -194,6 +207,7 @@ export async function createVirtualCardsForEmails(args, remoteUser, emails) { ); const virtualCards = models.PaymentMethod.bulkCreate(virtualCardsParams); virtualCards.map(vc => sendVirtualCardCreatedEmail(vc, collective)); + registerCreateInCache(args.CollectiveId, virtualCards.length); return virtualCards; } @@ -374,6 +388,25 @@ async function claim(args, remoteUser) { return virtualCardPaymentMethod; } +function createCacheKey(collectiveId) { + return `virtualcard_create_limit_on_collective_${collectiveId}`; +} + +/** Return false if create limit has been reached */ +async function checkCreateLimit(collectiveId, count) { + const cacheKey = createCacheKey(collectiveId); + const existingCount = (await cache.get(cacheKey)) || 0; + return existingCount + count <= config.limits.virtualCardsPerHour; +} + +/** `checkCreateLimit`'s best friend - register `count` create actions in cache */ +async function registerCreateInCache(collectiveId, count) { + const oneHourInSeconds = 60 * 60; + const cacheKey = createCacheKey(collectiveId); + const existingCount = (await cache.get(cacheKey)) || 0; + cache.set(cacheKey, existingCount + count, oneHourInSeconds); +} + /* Expected API of a Payment Method Type */ export default { features: {