auth-yes/src/core/middleware.ts

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();
};