From 3aed94e06a518792c7ebd9b13686068feae758c1 Mon Sep 17 00:00:00 2001 From: bigint Date: Thu, 11 Jul 2024 19:31:37 +0530 Subject: [PATCH] feat: add more rate limits --- apps/api/src/routes/avatar.ts | 96 ++++++++++--------- apps/api/src/routes/badges/hasHeyNft.ts | 2 +- apps/api/src/routes/badges/isHeyProfile.ts | 2 +- apps/api/src/routes/drafts/all.ts | 2 + apps/api/src/routes/drafts/delete.ts | 2 + apps/api/src/routes/drafts/update.ts | 2 + apps/api/src/routes/email/update.ts | 2 + apps/api/src/routes/ens/index.ts | 82 ++++++++-------- apps/api/src/routes/frames/post.ts | 2 + apps/api/src/routes/live/create.ts | 95 ++++++++++-------- apps/api/src/routes/metadata/index.ts | 74 +++++++------- apps/api/src/routes/polls/act.ts | 2 + apps/api/src/routes/polls/create.ts | 2 + apps/api/src/routes/preferences/update.ts | 2 + .../src/routes/preferences/updateNftStatus.ts | 2 + apps/api/src/routes/publications/pin.ts | 2 + apps/api/src/routes/staff-picks/index.ts | 54 ++++++----- .../src/routes/stats/profile/impressions.ts | 54 ++++++----- .../api/src/routes/stats/publication/views.ts | 67 +++++++------ apps/api/src/routes/sts/token.ts | 56 ++++++----- apps/api/src/routes/tips/create.ts | 2 + apps/api/src/routes/tips/get.ts | 88 +++++++++-------- 22 files changed, 381 insertions(+), 311 deletions(-) diff --git a/apps/api/src/routes/avatar.ts b/apps/api/src/routes/avatar.ts index 853e469854e8..d4fc41b7fd69 100644 --- a/apps/api/src/routes/avatar.ts +++ b/apps/api/src/routes/avatar.ts @@ -1,4 +1,4 @@ -import type { Handler } from 'express'; +import type { Request, Response } from 'express'; import { LensHub } from '@hey/abis'; import { IPFS_GATEWAY, IS_MAINNET, LENS_HUB } from '@hey/data/constants'; @@ -7,6 +7,7 @@ import logger from '@hey/helpers/logger'; import randomNumber from '@hey/helpers/randomNumber'; import { CACHE_AGE_INDEFINITE_ON_DISK } from 'src/helpers/constants'; import getRpc from 'src/helpers/getRpc'; +import { rateLimiter } from 'src/helpers/middlewares/rateLimiter'; import { getRedis, setRedis } from 'src/helpers/redisClient'; import { noBody } from 'src/helpers/responses'; import { createPublicClient } from 'viem'; @@ -15,58 +16,61 @@ import { polygon, polygonAmoy } from 'viem/chains'; const getSvgImage = (base64Image: string) => Buffer.from(base64Image, 'base64').toString('utf-8'); -export const get: Handler = async (req, res) => { - const { id } = req.query; +export const get = [ + rateLimiter({ requests: 2000, within: 1 }), + async (req: Request, res: Response) => { + const { id } = req.query; - if (!id) { - return noBody(res); - } + if (!id) { + return noBody(res); + } - try { - const cacheKey = `avatar:${id}`; - const cachedData = await getRedis(cacheKey); + try { + const cacheKey = `avatar:${id}`; + const cachedData = await getRedis(cacheKey); - if (cachedData) { - logger.info(`(cached) Downloaded Lenny avatar for ${id}`); - return res - .status(200) - .setHeader('Cache-Control', CACHE_AGE_INDEFINITE_ON_DISK) - .type('svg') - .send(getSvgImage(cachedData)); - } + if (cachedData) { + logger.info(`(cached) Downloaded Lenny avatar for ${id}`); + return res + .status(200) + .setHeader('Cache-Control', CACHE_AGE_INDEFINITE_ON_DISK) + .type('svg') + .send(getSvgImage(cachedData)); + } - const client = createPublicClient({ - chain: IS_MAINNET ? polygon : polygonAmoy, - transport: getRpc({ mainnet: IS_MAINNET }) - }); + const client = createPublicClient({ + chain: IS_MAINNET ? polygon : polygonAmoy, + transport: getRpc({ mainnet: IS_MAINNET }) + }); - const data: any = await client.readContract({ - abi: LensHub, - address: LENS_HUB, - args: [id], - functionName: 'tokenURI' - }); + const data: any = await client.readContract({ + abi: LensHub, + address: LENS_HUB, + args: [id], + functionName: 'tokenURI' + }); - const jsonData = JSON.parse( - Buffer.from(data.split(',')[1], 'base64').toString() - ); + const jsonData = JSON.parse( + Buffer.from(data.split(',')[1], 'base64').toString() + ); - const base64Image = jsonData.image.split(';base64,').pop(); + const base64Image = jsonData.image.split(';base64,').pop(); - await setRedis( - cacheKey, - base64Image, - randomNumber(daysToSeconds(400), daysToSeconds(600)) - ); - logger.info(`Downloaded Lenny avatar for ${id}`); + await setRedis( + cacheKey, + base64Image, + randomNumber(daysToSeconds(400), daysToSeconds(600)) + ); + logger.info(`Downloaded Lenny avatar for ${id}`); - return res - .status(200) - .setHeader('Cache-Control', CACHE_AGE_INDEFINITE_ON_DISK) - .type('svg') - .send(getSvgImage(base64Image)); - } catch { - const url = `${IPFS_GATEWAY}/Qmb4XppdMDCsS7KCL8nCJo8pukEWeqL4bTghURYwYiG83i/cropped_image.png`; - return res.status(302).redirect(url); + return res + .status(200) + .setHeader('Cache-Control', CACHE_AGE_INDEFINITE_ON_DISK) + .type('svg') + .send(getSvgImage(base64Image)); + } catch { + const url = `${IPFS_GATEWAY}/Qmb4XppdMDCsS7KCL8nCJo8pukEWeqL4bTghURYwYiG83i/cropped_image.png`; + return res.status(302).redirect(url); + } } -}; +]; diff --git a/apps/api/src/routes/badges/hasHeyNft.ts b/apps/api/src/routes/badges/hasHeyNft.ts index 1beb421be194..6f1517312e86 100644 --- a/apps/api/src/routes/badges/hasHeyNft.ts +++ b/apps/api/src/routes/badges/hasHeyNft.ts @@ -14,7 +14,7 @@ import { noBody } from 'src/helpers/responses'; import { getAddress } from 'viem'; export const get = [ - rateLimiter({ requests: 50, within: 1 }), + rateLimiter({ requests: 100, within: 1 }), async (req: Request, res: Response) => { const { address, id } = req.query; diff --git a/apps/api/src/routes/badges/isHeyProfile.ts b/apps/api/src/routes/badges/isHeyProfile.ts index a4ea715bfa0c..51a42f6fb732 100644 --- a/apps/api/src/routes/badges/isHeyProfile.ts +++ b/apps/api/src/routes/badges/isHeyProfile.ts @@ -14,7 +14,7 @@ import { noBody } from 'src/helpers/responses'; import { getAddress } from 'viem'; export const get = [ - rateLimiter({ requests: 50, within: 1 }), + rateLimiter({ requests: 100, within: 1 }), async (req: Request, res: Response) => { const { address, id } = req.query; diff --git a/apps/api/src/routes/drafts/all.ts b/apps/api/src/routes/drafts/all.ts index 5101a5371153..cc51a014ead5 100644 --- a/apps/api/src/routes/drafts/all.ts +++ b/apps/api/src/routes/drafts/all.ts @@ -4,10 +4,12 @@ import logger from '@hey/helpers/logger'; import parseJwt from '@hey/helpers/parseJwt'; import heyPg from 'src/db/heyPg'; import catchedError from 'src/helpers/catchedError'; +import { rateLimiter } from 'src/helpers/middlewares/rateLimiter'; import validateLensAccount from 'src/helpers/middlewares/validateLensAccount'; // TODO: add tests export const get = [ + rateLimiter({ requests: 100, within: 1 }), validateLensAccount, async (req: Request, res: Response) => { try { diff --git a/apps/api/src/routes/drafts/delete.ts b/apps/api/src/routes/drafts/delete.ts index 74e7c5ed8617..5591d1322eff 100644 --- a/apps/api/src/routes/drafts/delete.ts +++ b/apps/api/src/routes/drafts/delete.ts @@ -3,6 +3,7 @@ import type { Request, Response } from 'express'; import logger from '@hey/helpers/logger'; import heyPg from 'src/db/heyPg'; import catchedError from 'src/helpers/catchedError'; +import { rateLimiter } from 'src/helpers/middlewares/rateLimiter'; import validateLensAccount from 'src/helpers/middlewares/validateLensAccount'; import { invalidBody, noBody } from 'src/helpers/responses'; import { object, string } from 'zod'; @@ -17,6 +18,7 @@ const validationSchema = object({ // TODO: add tests export const post = [ + rateLimiter({ requests: 50, within: 1 }), validateLensAccount, async (req: Request, res: Response) => { const { body } = req; diff --git a/apps/api/src/routes/drafts/update.ts b/apps/api/src/routes/drafts/update.ts index c883a86ef931..0538d8a95c46 100644 --- a/apps/api/src/routes/drafts/update.ts +++ b/apps/api/src/routes/drafts/update.ts @@ -4,6 +4,7 @@ import logger from '@hey/helpers/logger'; import parseJwt from '@hey/helpers/parseJwt'; import heyPg from 'src/db/heyPg'; import catchedError from 'src/helpers/catchedError'; +import { rateLimiter } from 'src/helpers/middlewares/rateLimiter'; import validateLensAccount from 'src/helpers/middlewares/validateLensAccount'; import { invalidBody, noBody } from 'src/helpers/responses'; import { object, string } from 'zod'; @@ -21,6 +22,7 @@ const validationSchema = object({ }); export const post = [ + rateLimiter({ requests: 50, within: 1 }), validateLensAccount, async (req: Request, res: Response) => { const { body } = req; diff --git a/apps/api/src/routes/email/update.ts b/apps/api/src/routes/email/update.ts index bc62d459f24a..29b22026c48e 100644 --- a/apps/api/src/routes/email/update.ts +++ b/apps/api/src/routes/email/update.ts @@ -4,6 +4,7 @@ import { APP_NAME } from '@hey/data/constants'; import logger from '@hey/helpers/logger'; import parseJwt from '@hey/helpers/parseJwt'; import catchedError from 'src/helpers/catchedError'; +import { rateLimiter } from 'src/helpers/middlewares/rateLimiter'; import validateLensAccount from 'src/helpers/middlewares/validateLensAccount'; import prisma from 'src/helpers/prisma'; import { invalidBody, noBody } from 'src/helpers/responses'; @@ -22,6 +23,7 @@ const validationSchema = object({ }); export const post = [ + rateLimiter({ requests: 5, within: 60 }), validateLensAccount, async (req: Request, res: Response) => { const { body } = req; diff --git a/apps/api/src/routes/ens/index.ts b/apps/api/src/routes/ens/index.ts index 2680d3a8a711..d4e826390779 100644 --- a/apps/api/src/routes/ens/index.ts +++ b/apps/api/src/routes/ens/index.ts @@ -1,8 +1,9 @@ -import type { Handler } from 'express'; +import type { Request, Response } from 'express'; import logger from '@hey/helpers/logger'; import catchedError from 'src/helpers/catchedError'; import { resolverAbi } from 'src/helpers/ens/resolverAbi'; +import { rateLimiter } from 'src/helpers/middlewares/rateLimiter'; import { invalidBody, noBody } from 'src/helpers/responses'; import { createPublicClient, fallback, http } from 'viem'; import { mainnet } from 'viem/chains'; @@ -18,42 +19,45 @@ const validationSchema = object({ }) }); -export const post: Handler = async (req, res) => { - const { body } = req; - - if (!body) { - return noBody(res); - } - - const validation = validationSchema.safeParse(body); - - if (!validation.success) { - return invalidBody(res); +export const post = [ + rateLimiter({ requests: 100, within: 1 }), + async (req: Request, res: Response) => { + const { body } = req; + + if (!body) { + return noBody(res); + } + + const validation = validationSchema.safeParse(body); + + if (!validation.success) { + return invalidBody(res); + } + + const { addresses } = body as ExtensionRequest; + + try { + const client = createPublicClient({ + chain: mainnet, + transport: fallback([ + http('https://ethereum.publicnode.com'), + http('https://rpc.ankr.com/eth'), + http('https://cloudflare-eth.com'), + http('https://eth.merkle.io') + ]) + }); + + const result = await client.readContract({ + abi: resolverAbi, + address: '0x3671ae578e63fdf66ad4f3e12cc0c0d71ac7510c', + args: [addresses], + functionName: 'getNames' + }); + logger.info('ENS names fetched'); + + return res.status(200).json({ result, success: true }); + } catch (error) { + return catchedError(res, error); + } } - - const { addresses } = body as ExtensionRequest; - - try { - const client = createPublicClient({ - chain: mainnet, - transport: fallback([ - http('https://ethereum.publicnode.com'), - http('https://rpc.ankr.com/eth'), - http('https://cloudflare-eth.com'), - http('https://eth.merkle.io') - ]) - }); - - const result = await client.readContract({ - abi: resolverAbi, - address: '0x3671ae578e63fdf66ad4f3e12cc0c0d71ac7510c', - args: [addresses], - functionName: 'getNames' - }); - logger.info('ENS names fetched'); - - return res.status(200).json({ result, success: true }); - } catch (error) { - return catchedError(res, error); - } -}; +]; diff --git a/apps/api/src/routes/frames/post.ts b/apps/api/src/routes/frames/post.ts index 5dcc161b5082..f6dd6cfd0fd3 100644 --- a/apps/api/src/routes/frames/post.ts +++ b/apps/api/src/routes/frames/post.ts @@ -9,6 +9,7 @@ import { parseHTML } from 'linkedom'; import catchedError from 'src/helpers/catchedError'; import { HEY_USER_AGENT } from 'src/helpers/constants'; import signFrameAction from 'src/helpers/frames/signFrameAction'; +import { rateLimiter } from 'src/helpers/middlewares/rateLimiter'; import validateLensAccount from 'src/helpers/middlewares/validateLensAccount'; import getFrame from 'src/helpers/oembed/meta/getFrame'; import { invalidBody, noBody } from 'src/helpers/responses'; @@ -31,6 +32,7 @@ const validationSchema = object({ }); export const post = [ + rateLimiter({ requests: 100, within: 1 }), validateLensAccount, async (req: Request, res: Response) => { const { body } = req; diff --git a/apps/api/src/routes/live/create.ts b/apps/api/src/routes/live/create.ts index ae666c8655a9..f37441a9819a 100644 --- a/apps/api/src/routes/live/create.ts +++ b/apps/api/src/routes/live/create.ts @@ -1,9 +1,10 @@ -import type { Handler } from 'express'; +import type { Request, Response } from 'express'; import { LIVEPEER_KEY } from '@hey/data/constants'; import logger from '@hey/helpers/logger'; import parseJwt from '@hey/helpers/parseJwt'; import catchedError from 'src/helpers/catchedError'; +import { rateLimiter } from 'src/helpers/middlewares/rateLimiter'; import { invalidBody, noBody } from 'src/helpers/responses'; import { v4 as uuid } from 'uuid'; import { boolean, object } from 'zod'; @@ -16,51 +17,63 @@ const validationSchema = object({ record: boolean() }); -export const post: Handler = async (req, res) => { - const { body } = req; +export const post = [ + rateLimiter({ requests: 10, within: 60 }), + async (req: Request, res: Response) => { + const { body } = req; - if (!body) { - return noBody(res); - } + if (!body) { + return noBody(res); + } - const validation = validationSchema.safeParse(body); + const validation = validationSchema.safeParse(body); - if (!validation.success) { - return invalidBody(res); - } + if (!validation.success) { + return invalidBody(res); + } - const { record } = body as ExtensionRequest; + const { record } = body as ExtensionRequest; - try { - const identityToken = req.headers['x-identity-token'] as string; - const payload = parseJwt(identityToken); - const livepeerResponse = await fetch('https://livepeer.studio/api/stream', { - body: JSON.stringify({ - name: `${payload.id}-${uuid()}`, - profiles: [ - { bitrate: 3000000, fps: 0, height: 720, name: '720p0', width: 1280 }, - { - bitrate: 6000000, - fps: 0, - height: 1080, - name: '1080p0', - width: 1920 - } - ], - record - }), - headers: { - Authorization: `Bearer ${LIVEPEER_KEY}`, - 'content-type': 'application/json' - }, - method: 'POST' - }); + try { + const identityToken = req.headers['x-identity-token'] as string; + const payload = parseJwt(identityToken); + const livepeerResponse = await fetch( + 'https://livepeer.studio/api/stream', + { + body: JSON.stringify({ + name: `${payload.id}-${uuid()}`, + profiles: [ + { + bitrate: 3000000, + fps: 0, + height: 720, + name: '720p0', + width: 1280 + }, + { + bitrate: 6000000, + fps: 0, + height: 1080, + name: '1080p0', + width: 1920 + } + ], + record + }), + headers: { + Authorization: `Bearer ${LIVEPEER_KEY}`, + 'content-type': 'application/json' + }, + method: 'POST' + } + ); - const result = await livepeerResponse.json(); - logger.info(`Created stream live stream by ${payload.id}`); + const result = await livepeerResponse.json(); + logger.info(`Created stream live stream by ${payload.id}`); - return res.status(200).json({ result, success: true }); - } catch (error) { - return catchedError(res, error); + return res.status(200).json({ result, success: true }); + } catch (error) { + return catchedError(res, error); + } } -}; +]; diff --git a/apps/api/src/routes/metadata/index.ts b/apps/api/src/routes/metadata/index.ts index eb15308147b6..8c80c9a56463 100644 --- a/apps/api/src/routes/metadata/index.ts +++ b/apps/api/src/routes/metadata/index.ts @@ -1,44 +1,48 @@ -import type { Handler } from 'express'; +import type { Request, Response } from 'express'; import logger from '@hey/helpers/logger'; import { NodeIrys } from '@irys/sdk'; import { signMetadata } from '@lens-protocol/metadata'; import catchedError from 'src/helpers/catchedError'; +import { rateLimiter } from 'src/helpers/middlewares/rateLimiter'; import { noBody } from 'src/helpers/responses'; import { privateKeyToAccount } from 'viem/accounts'; -export const post: Handler = async (req, res) => { - const { body } = req; - - if (!body) { - return noBody(res); - } - - try { - const url = 'https://arweave.mainnet.irys.xyz/tx/matic'; - const token = 'matic'; - const client = new NodeIrys({ - key: process.env.PRIVATE_KEY, - token, - url - }); - - const account = privateKeyToAccount(`0x${process.env.PRIVATE_KEY}`); - const signed = await signMetadata(body, (message) => - account.signMessage({ message }) - ); - - const receipt = await client.upload(JSON.stringify(signed), { - tags: [ - { name: 'content-type', value: 'application/json' }, - { name: 'App-Name', value: 'Hey.xyz' } - ] - }); - - logger.info(`Uploaded metadata to Irys: ar://${receipt.id}`); - - return res.status(200).json({ id: receipt.id, success: true }); - } catch (error) { - return catchedError(res, error); +export const post = [ + rateLimiter({ requests: 30, within: 1 }), + async (req: Request, res: Response) => { + const { body } = req; + + if (!body) { + return noBody(res); + } + + try { + const url = 'https://arweave.mainnet.irys.xyz/tx/matic'; + const token = 'matic'; + const client = new NodeIrys({ + key: process.env.PRIVATE_KEY, + token, + url + }); + + const account = privateKeyToAccount(`0x${process.env.PRIVATE_KEY}`); + const signed = await signMetadata(body, (message) => + account.signMessage({ message }) + ); + + const receipt = await client.upload(JSON.stringify(signed), { + tags: [ + { name: 'content-type', value: 'application/json' }, + { name: 'App-Name', value: 'Hey.xyz' } + ] + }); + + logger.info(`Uploaded metadata to Irys: ar://${receipt.id}`); + + return res.status(200).json({ id: receipt.id, success: true }); + } catch (error) { + return catchedError(res, error); + } } -}; +]; diff --git a/apps/api/src/routes/polls/act.ts b/apps/api/src/routes/polls/act.ts index 562a4fca7978..80ba965e7eb7 100644 --- a/apps/api/src/routes/polls/act.ts +++ b/apps/api/src/routes/polls/act.ts @@ -4,6 +4,7 @@ import logger from '@hey/helpers/logger'; import parseJwt from '@hey/helpers/parseJwt'; import heyPg from 'src/db/heyPg'; import catchedError from 'src/helpers/catchedError'; +import { rateLimiter } from 'src/helpers/middlewares/rateLimiter'; import validateLensAccount from 'src/helpers/middlewares/validateLensAccount'; import { delRedis } from 'src/helpers/redisClient'; import { invalidBody, noBody } from 'src/helpers/responses'; @@ -20,6 +21,7 @@ const validationSchema = object({ }); export const post = [ + rateLimiter({ requests: 30, within: 1 }), validateLensAccount, async (req: Request, res: Response) => { const { body } = req; diff --git a/apps/api/src/routes/polls/create.ts b/apps/api/src/routes/polls/create.ts index a2f763301624..5382f683bb95 100644 --- a/apps/api/src/routes/polls/create.ts +++ b/apps/api/src/routes/polls/create.ts @@ -2,6 +2,7 @@ import type { Request, Response } from 'express'; import logger from '@hey/helpers/logger'; import catchedError from 'src/helpers/catchedError'; +import { rateLimiter } from 'src/helpers/middlewares/rateLimiter'; import validateLensAccount from 'src/helpers/middlewares/validateLensAccount'; import prisma from 'src/helpers/prisma'; import { invalidBody, noBody } from 'src/helpers/responses'; @@ -18,6 +19,7 @@ const validationSchema = object({ }); export const post = [ + rateLimiter({ requests: 30, within: 1 }), validateLensAccount, async (req: Request, res: Response) => { const { body } = req; diff --git a/apps/api/src/routes/preferences/update.ts b/apps/api/src/routes/preferences/update.ts index 93c036f773e3..edbd80484262 100644 --- a/apps/api/src/routes/preferences/update.ts +++ b/apps/api/src/routes/preferences/update.ts @@ -3,6 +3,7 @@ import type { Request, Response } from 'express'; import logger from '@hey/helpers/logger'; import parseJwt from '@hey/helpers/parseJwt'; import catchedError from 'src/helpers/catchedError'; +import { rateLimiter } from 'src/helpers/middlewares/rateLimiter'; import validateLensAccount from 'src/helpers/middlewares/validateLensAccount'; import prisma from 'src/helpers/prisma'; import { delRedis } from 'src/helpers/redisClient'; @@ -22,6 +23,7 @@ const validationSchema = object({ }); export const post = [ + rateLimiter({ requests: 50, within: 1 }), validateLensAccount, async (req: Request, res: Response) => { const { body } = req; diff --git a/apps/api/src/routes/preferences/updateNftStatus.ts b/apps/api/src/routes/preferences/updateNftStatus.ts index f9091d1f88ed..d498220beca7 100644 --- a/apps/api/src/routes/preferences/updateNftStatus.ts +++ b/apps/api/src/routes/preferences/updateNftStatus.ts @@ -3,10 +3,12 @@ import type { Request, Response } from 'express'; import logger from '@hey/helpers/logger'; import parseJwt from '@hey/helpers/parseJwt'; import catchedError from 'src/helpers/catchedError'; +import { rateLimiter } from 'src/helpers/middlewares/rateLimiter'; import validateLensAccount from 'src/helpers/middlewares/validateLensAccount'; import prisma from 'src/helpers/prisma'; export const post = [ + rateLimiter({ requests: 50, within: 1 }), validateLensAccount, async (req: Request, res: Response) => { try { diff --git a/apps/api/src/routes/publications/pin.ts b/apps/api/src/routes/publications/pin.ts index 0d402a97097f..769183c8391c 100644 --- a/apps/api/src/routes/publications/pin.ts +++ b/apps/api/src/routes/publications/pin.ts @@ -3,6 +3,7 @@ import type { Request, Response } from 'express'; import logger from '@hey/helpers/logger'; import parseJwt from '@hey/helpers/parseJwt'; import catchedError from 'src/helpers/catchedError'; +import { rateLimiter } from 'src/helpers/middlewares/rateLimiter'; import validateLensAccount from 'src/helpers/middlewares/validateLensAccount'; import prisma from 'src/helpers/prisma'; import { delRedis } from 'src/helpers/redisClient'; @@ -20,6 +21,7 @@ const validationSchema = object({ }); export const post = [ + rateLimiter({ requests: 10, within: 1 }), validateLensAccount, async (req: Request, res: Response) => { const { body } = req; diff --git a/apps/api/src/routes/staff-picks/index.ts b/apps/api/src/routes/staff-picks/index.ts index 334ae4b35c2b..45d932f5c79b 100644 --- a/apps/api/src/routes/staff-picks/index.ts +++ b/apps/api/src/routes/staff-picks/index.ts @@ -1,4 +1,4 @@ -import type { Handler } from 'express'; +import type { Request, Response } from 'express'; import logger from '@hey/helpers/logger'; import heyPg from 'src/db/heyPg'; @@ -7,6 +7,7 @@ import { CACHE_AGE_30_MINS, STAFF_PICK_FEATURE_ID } from 'src/helpers/constants'; +import { rateLimiter } from 'src/helpers/middlewares/rateLimiter'; import { generateMediumExpiry, getRedis, @@ -18,35 +19,38 @@ const getRandomPicks = (data: any[]) => { return random.slice(0, 150); }; -export const get: Handler = async (_, res) => { - try { - const cacheKey = `staff-picks`; - const cachedData = await getRedis(cacheKey); +export const get = [ + rateLimiter({ requests: 100, within: 1 }), + async (_: Request, res: Response) => { + try { + const cacheKey = `staff-picks`; + const cachedData = await getRedis(cacheKey); - if (cachedData) { - logger.info('(cached) Staff picks fetched'); - return res.status(200).json({ - result: getRandomPicks(JSON.parse(cachedData)), - success: true - }); - } + if (cachedData) { + logger.info('(cached) Staff picks fetched'); + return res.status(200).json({ + result: getRandomPicks(JSON.parse(cachedData)), + success: true + }); + } - const data = await heyPg.query( - ` + const data = await heyPg.query( + ` SELECT "profileId" FROM "ProfileFeature" WHERE enabled = TRUE AND "featureId" = $1; `, - [STAFF_PICK_FEATURE_ID] - ); + [STAFF_PICK_FEATURE_ID] + ); - await setRedis(cacheKey, data, generateMediumExpiry()); - logger.info('Staff picks fetched'); + await setRedis(cacheKey, data, generateMediumExpiry()); + logger.info('Staff picks fetched'); - return res - .status(200) - .setHeader('Cache-Control', CACHE_AGE_30_MINS) - .json({ result: getRandomPicks(data), success: true }); - } catch (error) { - return catchedError(res, error); + return res + .status(200) + .setHeader('Cache-Control', CACHE_AGE_30_MINS) + .json({ result: getRandomPicks(data), success: true }); + } catch (error) { + return catchedError(res, error); + } } -}; +]; diff --git a/apps/api/src/routes/stats/profile/impressions.ts b/apps/api/src/routes/stats/profile/impressions.ts index d2cb081f4c55..abac8ade4f1b 100644 --- a/apps/api/src/routes/stats/profile/impressions.ts +++ b/apps/api/src/routes/stats/profile/impressions.ts @@ -1,23 +1,26 @@ -import type { Handler } from 'express'; +import type { Request, Response } from 'express'; import logger from '@hey/helpers/logger'; import catchedError from 'src/helpers/catchedError'; import createClickhouseClient from 'src/helpers/createClickhouseClient'; +import { rateLimiter } from 'src/helpers/middlewares/rateLimiter'; import { noBody } from 'src/helpers/responses'; -export const get: Handler = async (req, res) => { - const { id } = req.query; +export const get = [ + rateLimiter({ requests: 250, within: 1 }), + async (req: Request, res: Response) => { + const { id } = req.query; - if (!id) { - return noBody(res); - } + if (!id) { + return noBody(res); + } - try { - const client = createClickhouseClient(); + try { + const client = createClickhouseClient(); - const rows = await client.query({ - format: 'JSONEachRow', - query: ` + const rows = await client.query({ + format: 'JSONEachRow', + query: ` WITH date_series AS ( SELECT toDate(subtractDays(now(), number)) AS date @@ -42,20 +45,21 @@ export const get: Handler = async (req, res) => { GROUP BY ds.date ORDER BY ds.date ` - }); - const result = await rows.json<{ - count: number; - date: string; - }>(); - const impressions = result.map((row) => ({ - count: Number(row.count), - date: new Date(row.date).toISOString() - })); + }); + const result = await rows.json<{ + count: number; + date: string; + }>(); + const impressions = result.map((row) => ({ + count: Number(row.count), + date: new Date(row.date).toISOString() + })); - logger.info('Fetched profile impression stats'); + logger.info('Fetched profile impression stats'); - return res.status(200).json({ impressions }); - } catch (error) { - return catchedError(res, error); + return res.status(200).json({ impressions }); + } catch (error) { + return catchedError(res, error); + } } -}; +]; diff --git a/apps/api/src/routes/stats/publication/views.ts b/apps/api/src/routes/stats/publication/views.ts index 90ae0c13cdc1..f6110682ed91 100644 --- a/apps/api/src/routes/stats/publication/views.ts +++ b/apps/api/src/routes/stats/publication/views.ts @@ -1,8 +1,9 @@ -import type { Handler } from 'express'; +import type { Request, Response } from 'express'; import logger from '@hey/helpers/logger'; import catchedError from 'src/helpers/catchedError'; import createClickhouseClient from 'src/helpers/createClickhouseClient'; +import { rateLimiter } from 'src/helpers/middlewares/rateLimiter'; import { invalidBody, noBody } from 'src/helpers/responses'; import { array, object, string } from 'zod'; @@ -14,43 +15,49 @@ const validationSchema = object({ ids: array(string().max(2000, { message: 'Too many ids!' })) }); -export const post: Handler = async (req, res) => { - const { body } = req; +export const post = [ + rateLimiter({ requests: 250, within: 1 }), + async (req: Request, res: Response) => { + const { body } = req; - if (!body) { - return noBody(res); - } + if (!body) { + return noBody(res); + } - const validation = validationSchema.safeParse(body); + const validation = validationSchema.safeParse(body); - if (!validation.success) { - return invalidBody(res); - } + if (!validation.success) { + return invalidBody(res); + } - const { ids } = body as ExtensionRequest; + const { ids } = body as ExtensionRequest; - try { - const client = createClickhouseClient(); - const rows = await client.query({ - format: 'JSONEachRow', - query: ` + try { + const client = createClickhouseClient(); + const rows = await client.query({ + format: 'JSONEachRow', + query: ` SELECT publication_id, COUNT(*) AS count FROM impressions WHERE publication_id IN (${ids.map((id) => `'${id}'`).join(',')}) GROUP BY publication_id; ` - }); - - const result = await rows.json<{ count: number; publication_id: string }>(); - - const viewCounts = result.map((row) => ({ - id: row.publication_id, - views: Number(row.count) - })); - logger.info(`Fetched publication views for ${ids.length} publications`); - - return res.status(200).json({ success: true, views: viewCounts }); - } catch (error) { - return catchedError(res, error); + }); + + const result = await rows.json<{ + count: number; + publication_id: string; + }>(); + + const viewCounts = result.map((row) => ({ + id: row.publication_id, + views: Number(row.count) + })); + logger.info(`Fetched publication views for ${ids.length} publications`); + + return res.status(200).json({ success: true, views: viewCounts }); + } catch (error) { + return catchedError(res, error); + } } -}; +]; diff --git a/apps/api/src/routes/sts/token.ts b/apps/api/src/routes/sts/token.ts index 38128e8a9d00..fa3ecdb24c7d 100644 --- a/apps/api/src/routes/sts/token.ts +++ b/apps/api/src/routes/sts/token.ts @@ -1,9 +1,10 @@ -import type { Handler } from 'express'; +import type { Request, Response } from 'express'; import { AssumeRoleCommand, STSClient } from '@aws-sdk/client-sts'; import { EVER_API, S3_BUCKET } from '@hey/data/constants'; import logger from '@hey/helpers/logger'; import catchedError from 'src/helpers/catchedError'; +import { rateLimiter } from 'src/helpers/middlewares/rateLimiter'; const params = { DurationSeconds: 900, @@ -25,30 +26,33 @@ const params = { }` }; -export const get: Handler = async (_, res) => { - try { - const accessKeyId = process.env.EVER_ACCESS_KEY as string; - const secretAccessKey = process.env.EVER_ACCESS_SECRET as string; - const stsClient = new STSClient({ - credentials: { accessKeyId, secretAccessKey }, - endpoint: EVER_API, - region: 'us-west-2' - }); - const command = new AssumeRoleCommand({ - ...params, - RoleArn: undefined, - RoleSessionName: undefined - }); - const { Credentials: credentials } = await stsClient.send(command); - logger.info('STS token generated'); +export const get = [ + rateLimiter({ requests: 50, within: 1 }), + async (_: Request, res: Response) => { + try { + const accessKeyId = process.env.EVER_ACCESS_KEY as string; + const secretAccessKey = process.env.EVER_ACCESS_SECRET as string; + const stsClient = new STSClient({ + credentials: { accessKeyId, secretAccessKey }, + endpoint: EVER_API, + region: 'us-west-2' + }); + const command = new AssumeRoleCommand({ + ...params, + RoleArn: undefined, + RoleSessionName: undefined + }); + const { Credentials: credentials } = await stsClient.send(command); + logger.info('STS token generated'); - return res.status(200).json({ - accessKeyId: credentials?.AccessKeyId, - secretAccessKey: credentials?.SecretAccessKey, - sessionToken: credentials?.SessionToken, - success: true - }); - } catch (error) { - return catchedError(res, error); + return res.status(200).json({ + accessKeyId: credentials?.AccessKeyId, + secretAccessKey: credentials?.SecretAccessKey, + sessionToken: credentials?.SessionToken, + success: true + }); + } catch (error) { + return catchedError(res, error); + } } -}; +]; diff --git a/apps/api/src/routes/tips/create.ts b/apps/api/src/routes/tips/create.ts index a00fc0bcacd9..546319524e41 100644 --- a/apps/api/src/routes/tips/create.ts +++ b/apps/api/src/routes/tips/create.ts @@ -4,6 +4,7 @@ import { Regex } from '@hey/data/regex'; import logger from '@hey/helpers/logger'; import parseJwt from '@hey/helpers/parseJwt'; import catchedError from 'src/helpers/catchedError'; +import { rateLimiter } from 'src/helpers/middlewares/rateLimiter'; import validateLensAccount from 'src/helpers/middlewares/validateLensAccount'; import prisma from 'src/helpers/prisma'; import { invalidBody, noBody } from 'src/helpers/responses'; @@ -28,6 +29,7 @@ const validationSchema = object({ }); export const post = [ + rateLimiter({ requests: 50, within: 1 }), validateLensAccount, async (req: Request, res: Response) => { const { body } = req; diff --git a/apps/api/src/routes/tips/get.ts b/apps/api/src/routes/tips/get.ts index b317a06be674..2019411f5725 100644 --- a/apps/api/src/routes/tips/get.ts +++ b/apps/api/src/routes/tips/get.ts @@ -1,8 +1,9 @@ -import type { Handler } from 'express'; +import type { Request, Response } from 'express'; import logger from '@hey/helpers/logger'; import parseJwt from '@hey/helpers/parseJwt'; import catchedError from 'src/helpers/catchedError'; +import { rateLimiter } from 'src/helpers/middlewares/rateLimiter'; import prisma from 'src/helpers/prisma'; import { invalidBody, noBody } from 'src/helpers/responses'; import { array, object, string } from 'zod'; @@ -15,55 +16,58 @@ const validationSchema = object({ ids: array(string()).min(1).max(500) }); -export const post: Handler = async (req, res) => { - const { body } = req; +export const post = [ + rateLimiter({ requests: 250, within: 1 }), + async (req: Request, res: Response) => { + const { body } = req; - if (!body) { - return noBody(res); - } + if (!body) { + return noBody(res); + } - const validation = validationSchema.safeParse(body); + const validation = validationSchema.safeParse(body); - if (!validation.success) { - return invalidBody(res); - } + if (!validation.success) { + return invalidBody(res); + } - const { ids } = body as ExtensionRequest; + const { ids } = body as ExtensionRequest; - try { - const identityToken = req.headers['x-identity-token'] as string; - const payload = parseJwt(identityToken); - const profileId = payload.id; + try { + const identityToken = req.headers['x-identity-token'] as string; + const payload = parseJwt(identityToken); + const profileId = payload.id; - const [hasTipped, tipCounts] = await prisma.$transaction([ - prisma.tip.findMany({ - select: { publicationId: true }, - where: { - fromProfileId: profileId, - publicationId: { in: ids } - } - }), - prisma.tip.groupBy({ - _count: { publicationId: true }, - by: ['publicationId'], - orderBy: { publicationId: 'asc' }, - where: { publicationId: { in: ids } } - }) - ]); + const [hasTipped, tipCounts] = await prisma.$transaction([ + prisma.tip.findMany({ + select: { publicationId: true }, + where: { + fromProfileId: profileId, + publicationId: { in: ids } + } + }), + prisma.tip.groupBy({ + _count: { publicationId: true }, + by: ['publicationId'], + orderBy: { publicationId: 'asc' }, + where: { publicationId: { in: ids } } + }) + ]); - const hasTippedMap = new Set(hasTipped.map((tip) => tip.publicationId)); + const hasTippedMap = new Set(hasTipped.map((tip) => tip.publicationId)); - const result = tipCounts.map(({ _count, publicationId }) => ({ - // @ts-ignore - count: _count.publicationId, - id: publicationId, - tipped: hasTippedMap.has(publicationId) - })); + const result = tipCounts.map(({ _count, publicationId }) => ({ + // @ts-ignore + count: _count.publicationId, + id: publicationId, + tipped: hasTippedMap.has(publicationId) + })); - logger.info(`Fetched tips for ${ids.length} publications`); + logger.info(`Fetched tips for ${ids.length} publications`); - return res.status(200).json({ result, success: true }); - } catch (error) { - return catchedError(res, error); + return res.status(200).json({ result, success: true }); + } catch (error) { + return catchedError(res, error); + } } -}; +];