From 4305b0c374c332fb264ca37acc474ce430ecdade Mon Sep 17 00:00:00 2001 From: 49659410+tx0c <> Date: Tue, 16 Apr 2024 07:23:23 +0000 Subject: [PATCH] feat(publish-quota): per user publishArticle quota resolves #3842 --- ...40417164251_alter_user_add_publish_rate.js | 13 ++++++++++ src/mutations/article/publishArticle.ts | 24 ++++++++++++++++++- src/types/directives/rateLimit.ts | 2 +- 3 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 db/migrations/20240417164251_alter_user_add_publish_rate.js diff --git a/db/migrations/20240417164251_alter_user_add_publish_rate.js b/db/migrations/20240417164251_alter_user_add_publish_rate.js new file mode 100644 index 000000000..39d884b1e --- /dev/null +++ b/db/migrations/20240417164251_alter_user_add_publish_rate.js @@ -0,0 +1,13 @@ +const table = 'user' + +exports.up = async (knex) => { + await knex.schema.table(table, (t) => { + t.jsonb('publish_rate') + }) +} + +exports.down = async (knex) => { + await knex.schema.table(table, (t) => { + t.dropColumn('publish_rate') + }) +} diff --git a/src/mutations/article/publishArticle.ts b/src/mutations/article/publishArticle.ts index f695c9782..916514812 100644 --- a/src/mutations/article/publishArticle.ts +++ b/src/mutations/article/publishArticle.ts @@ -1,6 +1,12 @@ import type { GQLMutationResolvers } from 'definitions' +import type { Redis } from 'ioredis' -import { PUBLISH_STATE, USER_STATE } from 'common/enums' +import { + PUBLISH_ARTICLE_RATE_LIMIT, + PUBLISH_ARTICLE_RATE_PERIOD, + PUBLISH_STATE, + USER_STATE, +} from 'common/enums' import { DraftNotFoundError, ForbiddenByStateError, @@ -8,6 +14,7 @@ import { UserInputError, } from 'common/errors' import { fromGlobalId } from 'common/utils' +import { checkOperationLimit } from 'types/directives' const resolver: GQLMutationResolvers['publishArticle'] = async ( _, @@ -18,6 +25,7 @@ const resolver: GQLMutationResolvers['publishArticle'] = async ( draftService, atomService, queues: { publicationQueue }, + redis, }, } ) => { @@ -50,6 +58,20 @@ const resolver: GQLMutationResolvers['publishArticle'] = async ( throw new UserInputError('content is required') } + const pass = await checkOperationLimit({ + user: viewer.id || viewer.ip, + operation: 'publishArticle', + limit: viewer?.publishRate?.limit ?? (isProd ? 1 : 1000), // PUBLISH_ARTICLE_RATE_LIMIT, + period: viewer?.publishRate?.period ?? 720, // PUBLISH_ARTICLE_RATE_PERIOD, + redis, // : connections.redis, + }) + + if (!pass) { + throw new ActionLimitExceededError( + `rate exceeded for operation ${fieldName}` + ) + } + if ( draft.publishState === PUBLISH_STATE.pending || (draft.archived && isPublished) diff --git a/src/types/directives/rateLimit.ts b/src/types/directives/rateLimit.ts index 4042a28f8..0b55fd07c 100644 --- a/src/types/directives/rateLimit.ts +++ b/src/types/directives/rateLimit.ts @@ -7,7 +7,7 @@ import { CACHE_PREFIX } from 'common/enums' import { ActionLimitExceededError } from 'common/errors' import { genCacheKey } from 'connectors' -const checkOperationLimit = async ({ +export const checkOperationLimit = async ({ user, operation, limit,