Skip to content

Commit 11c9c65

Browse files
committed
feat: add rate limiter
Signed-off-by: Raúl Santos <[email protected]>
1 parent 0e1a3cf commit 11c9c65

File tree

7 files changed

+5173
-3439
lines changed

7 files changed

+5173
-3439
lines changed

frontend/nuxt.config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import primevue from './setup/primevue';
88
import echarts from './setup/echarts';
99
import caching from './setup/caching';
1010
import sitemap from './setup/sitemap';
11+
import rateLimiter from './setup/rate-limiter';
1112

1213
const isProduction = process.env.NUXT_APP_ENV === 'production';
1314
const isDevelopment = process.env.NODE_ENV === 'development';
@@ -52,7 +53,7 @@ export default defineNuxtConfig({
5253
primevue,
5354
echarts,
5455
runtimeConfig: {
55-
// These are are only available on the server-side and can be overridden by the .env file
56+
// These are only available on the server-side and can be overridden by the .env file
5657
appEnv: process.env.APP_ENV,
5758
tinybirdBaseUrl: 'https://api.us-west-2.aws.tinybird.co',
5859
tinybirdToken: '',
@@ -78,6 +79,7 @@ export default defineNuxtConfig({
7879
cmDbPassword: 'example',
7980
cmDbDatabase: 'crowd-web',
8081
dataCopilotDefaultSegmentId: '',
82+
rateLimiter: rateLimiter,
8183
// These are also exposed on the client-side
8284
public: {
8385
apiBase: '/api',

frontend/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"@ai-sdk/amazon-bedrock": "^2.2.12",
2929
"@ai-sdk/openai": "^2.0.53",
3030
"@ai-sdk/vue": "^2.0.78",
31+
"@crowd/redis": "workspace:*",
3132
"@linuxfoundation/lfx-ui-core": "^0.0.20",
3233
"@modelcontextprotocol/sdk": "^1.20.2",
3334
"@nuxt/eslint": "^1.9.0",
@@ -39,6 +40,7 @@
3940
"@pinia/nuxt": "^0.11.2",
4041
"@popperjs/core": "^2.11.8",
4142
"@primevue/themes": "^4.4.1",
43+
"@redis/client": "^5.9.0",
4244
"@tanstack/vue-query": "^5.90.5",
4345
"@types/jsonwebtoken": "^9.0.10",
4446
"@vuelidate/core": "^2.0.3",
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright (c) 2025 The Linux Foundation and each contributor.
2+
// SPDX-License-Identifier: MIT
3+
4+
import type { H3Event } from 'h3';
5+
import { checkRateLimit, setRateLimitHeaders } from '../utils/rate-limiter';
6+
import { RateLimiterConfig } from '~~/server/types/rate-limiter';
7+
8+
/**
9+
* This is a rate-limiting middleware that checks incoming requests against the configured rate limits and blocks
10+
* requests that exceed the limits.
11+
*
12+
* Features:
13+
* - Uses Redis for distributed rate limiting
14+
* - Hashes IP addresses for GDPR compliance
15+
* - Supports per-route and per-method limits
16+
* - Adds rate limit headers to responses
17+
*/
18+
export default defineEventHandler(async (event: H3Event) => {
19+
const config = useRuntimeConfig();
20+
const rateLimiterConfig = config.rateLimiter as RateLimiterConfig;
21+
22+
// Skip rate limiting if disabled
23+
if (!rateLimiterConfig.enabled) {
24+
return;
25+
}
26+
27+
try {
28+
// Check rate limit
29+
const result = await checkRateLimit(
30+
event,
31+
rateLimiterConfig.rules,
32+
rateLimiterConfig.defaultLimit.maxRequests,
33+
rateLimiterConfig.defaultLimit.windowSeconds,
34+
);
35+
36+
// Set rate limit headers
37+
setRateLimitHeaders(event, result);
38+
39+
// Block request if rate limit exceeded
40+
if (!result.allowed) {
41+
throw createError({
42+
statusCode: 429,
43+
statusMessage: 'Too Many Requests',
44+
message: `Rate limit exceeded. Please wait ${result.resetIn} seconds before trying again.`,
45+
});
46+
}
47+
} catch (error) {
48+
// If it's already a 429 error, re-throw it
49+
if (error && typeof error === 'object' && 'statusCode' in error && error.statusCode === 429) {
50+
throw error;
51+
}
52+
53+
// Log other errors but don't block the request.
54+
// This ensures the app keeps working even if Redis is down.
55+
console.error('Rate limiter error:', error);
56+
}
57+
});
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// Copyright (c) 2025 The Linux Foundation and each contributor.
2+
// SPDX-License-Identifier: MIT
3+
4+
/**
5+
* HTTP methods supported by the rate limiter.
6+
*/
7+
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
8+
9+
/**
10+
* Rate limit configuration for a specific route.
11+
*/
12+
export interface RateLimitRule {
13+
/**
14+
* The route pattern to match (supports wildcards).
15+
* Examples: '/api/*', '/api/report', '/api/auth/*'
16+
*/
17+
route: string;
18+
19+
/**
20+
* HTTP methods this rule applies to. If not specified, it applies to all methods.
21+
*/
22+
methods?: HttpMethod[];
23+
24+
/**
25+
* Maximum number of requests allowed within the window.
26+
*/
27+
maxRequests: number;
28+
29+
/**
30+
* Time window in seconds.
31+
*/
32+
windowSeconds: number;
33+
}
34+
35+
/**
36+
* Global rate limiter configuration.
37+
*/
38+
export interface RateLimiterConfig {
39+
/**
40+
* Whether to enable the rate limiter.
41+
* @default true
42+
*/
43+
enabled?: boolean;
44+
45+
/**
46+
* Default rate limit applied to all routes not matching specific rules.
47+
*/
48+
defaultLimit: {
49+
maxRequests: number;
50+
windowSeconds: number;
51+
};
52+
53+
/**
54+
* Secret used for hashing IP addresses for GDPR compliance.
55+
*/
56+
secret: string;
57+
58+
/**
59+
* Route-specific rate limit rules. Rules are evaluated in order; first match wins.
60+
*/
61+
rules: RateLimitRule[];
62+
}
63+
64+
/**
65+
* Rate limit check result.
66+
*/
67+
export interface RateLimitResult {
68+
/**
69+
* Whether the request is allowed.
70+
*/
71+
allowed: boolean;
72+
73+
/**
74+
* Maximum requests allowed in the window.
75+
*/
76+
limit: number;
77+
78+
/**
79+
* Remaining requests in the current window.
80+
*/
81+
remaining: number;
82+
83+
/**
84+
* Time until the rate limit resets (in seconds).
85+
*/
86+
resetIn: number;
87+
88+
/**
89+
* Total number of requests made in the current window.
90+
*/
91+
current: number;
92+
}

0 commit comments

Comments
 (0)