85 lines
2.1 KiB
TypeScript
85 lines
2.1 KiB
TypeScript
import type { Context } from "jsr:@hono/hono@4";
|
|
import { getAuthenticatedUser } from "./session.ts";
|
|
import { valkey } from "./valkey.ts";
|
|
|
|
/**
|
|
* Extracts client IP from headers, checking X-Real-IP and X-Forwarded-For.
|
|
*/
|
|
export function getClientIp(c: Context): string {
|
|
const realIp = c.req.header("x-real-ip");
|
|
if (realIp) {
|
|
return realIp.trim();
|
|
}
|
|
|
|
let forwardedFor = c.req.header("x-forwarded-for");
|
|
if (forwardedFor) {
|
|
if (forwardedFor.length > 256) {
|
|
forwardedFor = forwardedFor.substring(0, 256);
|
|
}
|
|
const parts = forwardedFor.split(",");
|
|
return parts[parts.length - 1].trim();
|
|
}
|
|
return "unknown-ip";
|
|
}
|
|
|
|
export async function checkRateLimit(
|
|
key: string,
|
|
limit: number,
|
|
windowMs: number,
|
|
): Promise<boolean> {
|
|
try {
|
|
const current = await valkey.incr(key);
|
|
if (current === 1) {
|
|
await valkey.pexpire(key, windowMs);
|
|
}
|
|
return current <= limit;
|
|
} catch (err) {
|
|
console.warn("[Rate Limiter] Valkey error:", err);
|
|
return true; // Fail open on cache error
|
|
}
|
|
}
|
|
|
|
export const rateLimitWrapper = {
|
|
checkRateLimit,
|
|
async isRateLimited(
|
|
key: string,
|
|
limit: number,
|
|
windowMs: number,
|
|
): Promise<boolean> {
|
|
const allowed = await checkRateLimit(key, limit, windowMs);
|
|
return !allowed;
|
|
},
|
|
};
|
|
|
|
export const publicRateLimiter = async (
|
|
c: Context,
|
|
next: () => Promise<void>,
|
|
) => {
|
|
const ip = getClientIp(c);
|
|
const key = `ratelimit:public:${ip}`;
|
|
const allowed = await rateLimitWrapper.checkRateLimit(key, 10, 60000);
|
|
if (!allowed) {
|
|
return c.json({ error: "Too Many Requests" }, 429);
|
|
}
|
|
await next();
|
|
};
|
|
|
|
export const adminRateLimiter = async (
|
|
c: Context,
|
|
next: () => Promise<void>,
|
|
) => {
|
|
const auth = await getAuthenticatedUser(c);
|
|
if (!auth) {
|
|
return c.json({ error: "Missing or invalid session" }, 401);
|
|
}
|
|
|
|
const key = `ratelimit:admin:${auth.userId}`;
|
|
const allowed = await rateLimitWrapper.checkRateLimit(key, 60, 60000);
|
|
if (!allowed) {
|
|
return c.json({ error: "Too Many Requests" }, 429);
|
|
}
|
|
|
|
c.set("userId", auth.userId);
|
|
await next();
|
|
};
|