|
1 | | -/** |
2 | | - * Simple in-memory rate limiter — no external dependencies. |
3 | | - * |
4 | | - * Strategy: sliding window per IP. |
5 | | - * Limit: 60 requests / 60 seconds for public policy endpoints. |
6 | | - * |
7 | | - * For production, replace with a Redis-backed solution (e.g. rate-limiter-flexible). |
8 | | - */ |
9 | | - |
10 | | -import { Request, Response, NextFunction } from "express"; |
11 | | - |
12 | | -interface WindowEntry { |
13 | | - count: number; |
14 | | - windowStart: number; |
15 | | -} |
16 | | - |
17 | | -const store = new Map<string, WindowEntry>(); |
18 | | -const WINDOW_MS = 60_000; // 1 minute |
19 | | -const MAX_REQUESTS = 60; |
20 | | - |
21 | | -function getClientIp(req: Request): string { |
22 | | - const forwarded = req.headers["x-forwarded-for"]; |
23 | | - if (typeof forwarded === "string") return forwarded.split(",")[0].trim(); |
24 | | - return req.socket.remoteAddress ?? "unknown"; |
25 | | -} |
26 | | - |
27 | | -export function publicRateLimit( |
28 | | - req: Request, |
29 | | - res: Response, |
30 | | - next: NextFunction |
31 | | -): void { |
32 | | - const ip = getClientIp(req); |
33 | | - const now = Date.now(); |
34 | | - const entry = store.get(ip); |
35 | | - |
36 | | - if (!entry || now - entry.windowStart > WINDOW_MS) { |
37 | | - store.set(ip, { count: 1, windowStart: now }); |
38 | | - res.setHeader("X-RateLimit-Limit", MAX_REQUESTS); |
39 | | - res.setHeader("X-RateLimit-Remaining", MAX_REQUESTS - 1); |
40 | | - next(); |
41 | | - return; |
42 | | - } |
43 | | - |
44 | | - entry.count += 1; |
45 | | - const remaining = Math.max(0, MAX_REQUESTS - entry.count); |
46 | | - res.setHeader("X-RateLimit-Limit", MAX_REQUESTS); |
47 | | - res.setHeader("X-RateLimit-Remaining", remaining); |
48 | | - |
49 | | - if (entry.count > MAX_REQUESTS) { |
50 | | - const retryAfter = Math.ceil((WINDOW_MS - (now - entry.windowStart)) / 1000); |
51 | | - res.setHeader("Retry-After", retryAfter); |
52 | | - res.status(429).json({ |
53 | | - error: "rate_limit_exceeded", |
54 | | - message: `Too many requests. Retry after ${retryAfter}s.`, |
55 | | - }); |
56 | | - return; |
57 | | - } |
58 | | - |
59 | | - next(); |
60 | | -} |
| 1 | +/** |
| 2 | + * Legacy in-memory rate limiter — kept for reference only. |
| 3 | + * |
| 4 | + * The active rate limiting is handled by WalletAwareThrottlerGuard (Redis-backed) |
| 5 | + * registered globally in AppModule via APP_GUARD. Per-route overrides use |
| 6 | + * @Throttle({ default: { limit, ttl } }) on individual controller methods. |
| 7 | + * |
| 8 | + * This file is NOT wired into the application. Do not import it. |
| 9 | + */ |
| 10 | + |
| 11 | +import { Request, Response, NextFunction } from "express"; |
| 12 | + |
| 13 | +interface WindowEntry { |
| 14 | + count: number; |
| 15 | + windowStart: number; |
| 16 | +} |
| 17 | + |
| 18 | +const store = new Map<string, WindowEntry>(); |
| 19 | +const WINDOW_MS = 60_000; |
| 20 | +const MAX_REQUESTS = 60; |
| 21 | + |
| 22 | +function getClientIp(req: Request): string { |
| 23 | + const forwarded = req.headers["x-forwarded-for"]; |
| 24 | + if (typeof forwarded === "string") return forwarded.split(",")[0].trim(); |
| 25 | + return req.socket.remoteAddress ?? "unknown"; |
| 26 | +} |
| 27 | + |
| 28 | +export function publicRateLimit( |
| 29 | + req: Request, |
| 30 | + res: Response, |
| 31 | + next: NextFunction |
| 32 | +): void { |
| 33 | + const ip = getClientIp(req); |
| 34 | + const now = Date.now(); |
| 35 | + const entry = store.get(ip); |
| 36 | + |
| 37 | + if (!entry || now - entry.windowStart > WINDOW_MS) { |
| 38 | + store.set(ip, { count: 1, windowStart: now }); |
| 39 | + res.setHeader("X-RateLimit-Limit", MAX_REQUESTS); |
| 40 | + res.setHeader("X-RateLimit-Remaining", MAX_REQUESTS - 1); |
| 41 | + next(); |
| 42 | + return; |
| 43 | + } |
| 44 | + |
| 45 | + entry.count += 1; |
| 46 | + const remaining = Math.max(0, MAX_REQUESTS - entry.count); |
| 47 | + res.setHeader("X-RateLimit-Limit", MAX_REQUESTS); |
| 48 | + res.setHeader("X-RateLimit-Remaining", remaining); |
| 49 | + |
| 50 | + if (entry.count > MAX_REQUESTS) { |
| 51 | + const retryAfter = Math.ceil((WINDOW_MS - (now - entry.windowStart)) / 1000); |
| 52 | + res.setHeader("Retry-After", retryAfter); |
| 53 | + res.status(429).json({ |
| 54 | + error: "rate_limit_exceeded", |
| 55 | + message: `Too many requests. Retry after ${retryAfter}s.`, |
| 56 | + }); |
| 57 | + return; |
| 58 | + } |
| 59 | + |
| 60 | + next(); |
| 61 | +} |
0 commit comments