From 59039e7f2803c0191198ee7de5a33d07713702d1 Mon Sep 17 00:00:00 2001 From: arsam Date: Fri, 18 Sep 2026 23:19:34 +0330 Subject: [PATCH] Import source: app/backend/src/lib/rate-limit Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality. --- app/backend/src/lib/rate-limit.ts | 43 +++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 app/backend/src/lib/rate-limit.ts diff --git a/app/backend/src/lib/rate-limit.ts b/app/backend/src/lib/rate-limit.ts new file mode 100644 index 0000000..d10c87e --- /dev/null +++ b/app/backend/src/lib/rate-limit.ts @@ -0,0 +1,43 @@ +import { redis } from '../redis.js'; + +export interface RateLimitResult { + limited: boolean; + retryAfter: number; +} + +export async function checkRateLimit( + ip: string, + action: string, + max: number, + windowSeconds: number, +): Promise { + const key = `rl:${action}:${ip}`; + const hits = await redis.incr(key); + if (hits === 1) await redis.expire(key, windowSeconds); + if (hits > max) { + const ttl = await redis.ttl(key); + return { limited: true, retryAfter: Math.max(ttl, 1) }; + } + return { limited: false, retryAfter: 0 }; +} + +export async function checkRateLimitBySubject( + subject: string, + action: string, + max: number, + windowSeconds: number, +): Promise { + const key = `rl:${action}:${subject}`; + const hits = await redis.incr(key); + if (hits === 1) await redis.expire(key, windowSeconds); + if (hits > max) { + const ttl = await redis.ttl(key); + return { limited: true, retryAfter: Math.max(ttl, 1) }; + } + return { limited: false, retryAfter: 0 }; +} + +export function rateLimitResponse(reply: import('fastify').FastifyReply, retryAfter: number) { + reply.header('Retry-After', String(retryAfter)); + return reply.code(429).send({ error: 'rate_limited', retryAfter }); +}