Add API rate limit (#23)

This commit is contained in:
KM Koushik
2024-05-30 20:01:20 +10:00
committed by GitHub
parent d7b8a9cca6
commit 8b61223153
6 changed files with 86 additions and 45 deletions

View File

@@ -23,6 +23,7 @@
"@hono/swagger-ui": "^0.2.1",
"@hono/zod-openapi": "^0.10.0",
"@hookform/resolvers": "^3.3.4",
"@isaacs/ttlcache": "^1.4.1",
"@prisma/client": "^5.11.0",
"@t3-oss/env-nextjs": "^0.9.2",
"@tanstack/react-query": "^5.25.0",

View File

@@ -40,6 +40,10 @@ export const env = createEnv({
GOOGLE_CLIENT_SECRET: z.string(),
SES_QUEUE_LIMIT: z.string().transform((str) => parseInt(str, 10)),
AWS_DEFAULT_REGION: z.string().default("us-east-1"),
API_RATE_LIMIT: z
.string()
.transform((str) => parseInt(str, 10))
.default(2),
},
/**
@@ -72,6 +76,7 @@ export const env = createEnv({
GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET,
SES_QUEUE_LIMIT: process.env.SES_QUEUE_LIMIT,
AWS_DEFAULT_REGION: process.env.AWS_DEFAULT_REGION,
API_RATE_LIMIT: process.env.API_RATE_LIMIT,
},
/**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially

View File

@@ -1,20 +1,30 @@
import TTLCache from "@isaacs/ttlcache";
import { Context } from "hono";
import { hashToken } from "../auth";
import { db } from "../db";
import { UnsendApiError } from "./api-error";
import { env } from "~/env";
const rateLimitCache = new TTLCache({
ttl: 1000, // 1 second
max: env.API_RATE_LIMIT,
});
/**
* Gets the team from the token. Also will check if the token is valid.
*/
export const getTeamFromToken = async (c: Context) => {
const authHeader = c.req.header("Authorization");
if (!authHeader) {
throw new UnsendApiError({
code: "UNAUTHORIZED",
message: "No Authorization header provided",
});
}
const token = authHeader.split(" ")[1];
if (!token) {
throw new UnsendApiError({
code: "UNAUTHORIZED",
@@ -22,6 +32,8 @@ export const getTeamFromToken = async (c: Context) => {
});
}
checkRateLimit(token);
const hashedToken = hashToken(token);
const team = await db.team.findFirst({
@@ -55,3 +67,18 @@ export const getTeamFromToken = async (c: Context) => {
return team;
};
const checkRateLimit = (token: string) => {
let rateLimit = rateLimitCache.get<number>(token);
rateLimit = rateLimit ?? 0;
if (rateLimit >= 2) {
throw new UnsendApiError({
code: "RATE_LIMITED",
message: `Rate limit exceeded, ${env.API_RATE_LIMIT} requests per second`,
});
}
rateLimitCache.set(token, rateLimit + 1);
};

View File

@@ -1,6 +1,6 @@
{
"name": "unsend",
"version": "0.0.5",
"version": "0.0.6",
"description": "",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
@@ -8,7 +8,8 @@
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"lint": "eslint . --max-warnings 0",
"build": "rm -rf dist && tsup index.ts --format esm,cjs --dts"
"build": "rm -rf dist && tsup index.ts --format esm,cjs --dts",
"publish-sdk": "pnpm run build && pnpm publish"
},
"keywords": [],
"author": "",

View File

@@ -3,7 +3,7 @@ import { cn } from "..";
export const Spinner: React.FC<
React.SVGProps<SVGSVGElement> & { innerSvgClass?: string }
> = (props) => {
> = ({ innerSvgClass, ...props }) => {
return (
<svg
version="1.1"
@@ -18,7 +18,7 @@ export const Spinner: React.FC<
<g
strokeWidth="200"
strokeLinecap="round"
className={cn("stroke-primary-foreground", props.innerSvgClass)}
className={cn("stroke-primary-foreground", innerSvgClass)}
fill="none"
id="spinner"
>

89
pnpm-lock.yaml generated
View File

@@ -112,6 +112,9 @@ importers:
'@hookform/resolvers':
specifier: ^3.3.4
version: 3.3.4(react-hook-form@7.51.3)
'@isaacs/ttlcache':
specifier: ^1.4.1
version: 1.4.1
'@prisma/client':
specifier: ^5.11.0
version: 5.11.0(prisma@5.11.0)
@@ -2091,6 +2094,11 @@ packages:
wrap-ansi: 8.1.0
wrap-ansi-cjs: /wrap-ansi@7.0.0
/@isaacs/ttlcache@1.4.1:
resolution: {integrity: sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==}
engines: {node: '>=12'}
dev: false
/@jridgewell/gen-mapping@0.3.5:
resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==}
engines: {node: '>=6.0.0'}
@@ -6141,7 +6149,7 @@ packages:
eslint: 8.57.0
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
eslint-plugin-import: 2.29.1(eslint@8.57.0)
eslint-plugin-jsx-a11y: 6.8.0(eslint@8.57.0)
eslint-plugin-react: 7.34.0(eslint@8.57.0)
eslint-plugin-react-hooks: 4.6.0(eslint@8.57.0)
@@ -6199,7 +6207,7 @@ packages:
enhanced-resolve: 5.16.0
eslint: 8.57.0
eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0)(eslint@8.57.0)
fast-glob: 3.3.2
get-tsconfig: 4.7.3
is-core-module: 2.13.1
@@ -6333,41 +6341,6 @@ packages:
ignore: 5.3.1
dev: true
/eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0):
resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==}
engines: {node: '>=4'}
peerDependencies:
'@typescript-eslint/parser': '*'
eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8
peerDependenciesMeta:
'@typescript-eslint/parser':
optional: true
dependencies:
'@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.4.2)
array-includes: 3.1.7
array.prototype.findlastindex: 1.2.4
array.prototype.flat: 1.3.2
array.prototype.flatmap: 1.3.2
debug: 3.2.7
doctrine: 2.1.0
eslint: 8.57.0
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
hasown: 2.0.2
is-core-module: 2.13.1
is-glob: 4.0.3
minimatch: 3.1.2
object.fromentries: 2.0.7
object.groupby: 1.0.2
object.values: 1.1.7
semver: 6.3.1
tsconfig-paths: 3.15.0
transitivePeerDependencies:
- eslint-import-resolver-typescript
- eslint-import-resolver-webpack
- supports-color
dev: true
/eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.2.0)(eslint@8.57.0):
resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==}
engines: {node: '>=4'}
@@ -6403,6 +6376,40 @@ packages:
- supports-color
dev: true
/eslint-plugin-import@2.29.1(eslint@8.57.0):
resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==}
engines: {node: '>=4'}
peerDependencies:
'@typescript-eslint/parser': '*'
eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8
peerDependenciesMeta:
'@typescript-eslint/parser':
optional: true
dependencies:
array-includes: 3.1.7
array.prototype.findlastindex: 1.2.4
array.prototype.flat: 1.3.2
array.prototype.flatmap: 1.3.2
debug: 3.2.7
doctrine: 2.1.0
eslint: 8.57.0
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
hasown: 2.0.2
is-core-module: 2.13.1
is-glob: 4.0.3
minimatch: 3.1.2
object.fromentries: 2.0.7
object.groupby: 1.0.2
object.values: 1.1.7
semver: 6.3.1
tsconfig-paths: 3.15.0
transitivePeerDependencies:
- eslint-import-resolver-typescript
- eslint-import-resolver-webpack
- supports-color
dev: true
/eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint@8.57.0)(typescript@5.4.2):
resolution: {integrity: sha512-QIT7FH7fNmd9n4se7FFKHbsLKGQiw885Ds6Y/sxKgCZ6natwCsXdgPOADnYVxN2QrRweF0FZWbJ6S7Rsn7llug==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -8265,8 +8272,8 @@ packages:
highlight.js: 10.7.3
dev: false
/lru-cache@10.2.0:
resolution: {integrity: sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==}
/lru-cache@10.2.2:
resolution: {integrity: sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==}
engines: {node: 14 || >=16.14}
/lru-cache@5.1.1:
@@ -9565,7 +9572,7 @@ packages:
resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==}
engines: {node: '>=16 || 14 >=14.17'}
dependencies:
lru-cache: 10.2.0
lru-cache: 10.2.2
minipass: 7.0.4
dev: true
@@ -9573,7 +9580,7 @@ packages:
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
engines: {node: '>=16 || 14 >=14.18'}
dependencies:
lru-cache: 10.2.0
lru-cache: 10.2.2
minipass: 7.0.4
/path-to-regexp@0.1.7: