feature(api): add rate limiter
This commit is contained in:
@@ -15,6 +15,7 @@
|
|||||||
"@fastify/compress": "^7.0.3",
|
"@fastify/compress": "^7.0.3",
|
||||||
"@fastify/cookie": "^9.3.1",
|
"@fastify/cookie": "^9.3.1",
|
||||||
"@fastify/cors": "^9.0.0",
|
"@fastify/cors": "^9.0.0",
|
||||||
|
"@fastify/rate-limit": "^9.1.0",
|
||||||
"@fastify/websocket": "^8.3.1",
|
"@fastify/websocket": "^8.3.1",
|
||||||
"@openpanel/common": "workspace:*",
|
"@openpanel/common": "workspace:*",
|
||||||
"@openpanel/db": "workspace:*",
|
"@openpanel/db": "workspace:*",
|
||||||
|
|||||||
@@ -209,8 +209,16 @@ const startServer = async () => {
|
|||||||
fastify.register(importRouter, { prefix: '/import' });
|
fastify.register(importRouter, { prefix: '/import' });
|
||||||
fastify.register(trackRouter, { prefix: '/track' });
|
fastify.register(trackRouter, { prefix: '/track' });
|
||||||
fastify.setErrorHandler((error, request, reply) => {
|
fastify.setErrorHandler((error, request, reply) => {
|
||||||
request.log.error('request error', { error });
|
if (error.statusCode === 429) {
|
||||||
reply.status(500).send('Internal server error');
|
reply.status(429).send({
|
||||||
|
status: 429,
|
||||||
|
error: 'Too Many Requests',
|
||||||
|
message: 'You have exceeded the rate limit for this endpoint.',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
request.log.error('request error', { error });
|
||||||
|
reply.status(500).send('Internal server error');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
fastify.get('/', (_request, reply) => {
|
fastify.get('/', (_request, reply) => {
|
||||||
reply.send({ name: 'openpanel sdk api' });
|
reply.send({ name: 'openpanel sdk api' });
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import * as controller from '@/controllers/export.controller';
|
import * as controller from '@/controllers/export.controller';
|
||||||
import { validateExportRequest } from '@/utils/auth';
|
import { validateExportRequest } from '@/utils/auth';
|
||||||
|
import { activateRateLimiter } from '@/utils/rate-limiter';
|
||||||
|
import { Prisma } from '@openpanel/db';
|
||||||
import type { FastifyPluginCallback, FastifyRequest } from 'fastify';
|
import type { FastifyPluginCallback, FastifyRequest } from 'fastify';
|
||||||
|
|
||||||
import { Prisma } from '@openpanel/db';
|
const exportRouter: FastifyPluginCallback = async (fastify, opts, done) => {
|
||||||
|
await activateRateLimiter({
|
||||||
|
fastify,
|
||||||
|
max: 10,
|
||||||
|
timeWindow: '10 seconds',
|
||||||
|
});
|
||||||
|
|
||||||
const eventRouter: FastifyPluginCallback = (fastify, opts, done) => {
|
|
||||||
fastify.addHook('preHandler', async (req: FastifyRequest, reply) => {
|
fastify.addHook('preHandler', async (req: FastifyRequest, reply) => {
|
||||||
try {
|
try {
|
||||||
const client = await validateExportRequest(req.headers);
|
const client = await validateExportRequest(req.headers);
|
||||||
@@ -43,4 +49,4 @@ const eventRouter: FastifyPluginCallback = (fastify, opts, done) => {
|
|||||||
done();
|
done();
|
||||||
};
|
};
|
||||||
|
|
||||||
export default eventRouter;
|
export default exportRouter;
|
||||||
|
|||||||
34
apps/api/src/utils/rate-limiter.ts
Normal file
34
apps/api/src/utils/rate-limiter.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { getRedisCache } from '@openpanel/redis';
|
||||||
|
import type { FastifyInstance } from 'fastify';
|
||||||
|
|
||||||
|
export async function activateRateLimiter({
|
||||||
|
fastify,
|
||||||
|
max,
|
||||||
|
timeWindow,
|
||||||
|
}: {
|
||||||
|
fastify: FastifyInstance;
|
||||||
|
max: number;
|
||||||
|
timeWindow?: string;
|
||||||
|
}) {
|
||||||
|
await fastify.register(import('@fastify/rate-limit'), {
|
||||||
|
max,
|
||||||
|
timeWindow: timeWindow || '1 minute',
|
||||||
|
errorResponseBuilder: (req, reply) => {
|
||||||
|
return {
|
||||||
|
statusCode: 429,
|
||||||
|
error: 'Too Many Requests',
|
||||||
|
message: 'You have exceeded the rate limit for this endpoint.',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
redis: getRedisCache(),
|
||||||
|
keyGenerator(req) {
|
||||||
|
return (req.headers['openpanel-client-id'] ||
|
||||||
|
req.headers['x-real-ip'] ||
|
||||||
|
req.headers['x-client-ip'] ||
|
||||||
|
req.headers['x-forwarded-for']) as string;
|
||||||
|
},
|
||||||
|
onExceeded: (req, reply) => {
|
||||||
|
req.log.warn('Rate limit exceeded');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
16
pnpm-lock.yaml
generated
16
pnpm-lock.yaml
generated
@@ -42,6 +42,9 @@ importers:
|
|||||||
'@fastify/cors':
|
'@fastify/cors':
|
||||||
specifier: ^9.0.0
|
specifier: ^9.0.0
|
||||||
version: 9.0.1
|
version: 9.0.1
|
||||||
|
'@fastify/rate-limit':
|
||||||
|
specifier: ^9.1.0
|
||||||
|
version: 9.1.0
|
||||||
'@fastify/websocket':
|
'@fastify/websocket':
|
||||||
specifier: ^8.3.1
|
specifier: ^8.3.1
|
||||||
version: 8.3.1
|
version: 8.3.1
|
||||||
@@ -3805,6 +3808,14 @@ packages:
|
|||||||
fast-deep-equal: 3.1.3
|
fast-deep-equal: 3.1.3
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@fastify/rate-limit@9.1.0:
|
||||||
|
resolution: {integrity: sha512-h5dZWCkuZXN0PxwqaFQLxeln8/LNwQwH9popywmDCFdKfgpi4b/HoMH1lluy6P+30CG9yzzpSpwTCIPNB9T1JA==}
|
||||||
|
dependencies:
|
||||||
|
'@lukeed/ms': 2.0.2
|
||||||
|
fastify-plugin: 4.5.1
|
||||||
|
toad-cache: 3.7.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@fastify/websocket@8.3.1:
|
/@fastify/websocket@8.3.1:
|
||||||
resolution: {integrity: sha512-hsQYHHJme/kvP3ZS4v/WMUznPBVeeQHHwAoMy1LiN6m/HuPfbdXq1MBJ4Nt8qX1YI+eVbog4MnOsU7MTozkwYA==}
|
resolution: {integrity: sha512-hsQYHHJme/kvP3ZS4v/WMUznPBVeeQHHwAoMy1LiN6m/HuPfbdXq1MBJ4Nt8qX1YI+eVbog4MnOsU7MTozkwYA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -4311,6 +4322,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==}
|
resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@lukeed/ms@2.0.2:
|
||||||
|
resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@mapbox/node-pre-gyp@1.0.11:
|
/@mapbox/node-pre-gyp@1.0.11:
|
||||||
resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==}
|
resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|||||||
Reference in New Issue
Block a user