Add API rate limit (#23)
This commit is contained in:
@@ -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",
|
||||
|
@@ -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
|
||||
|
@@ -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);
|
||||
};
|
||||
|
@@ -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": "",
|
||||
|
@@ -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
89
pnpm-lock.yaml
generated
@@ -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:
|
||||
|
Reference in New Issue
Block a user