Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/services/auth/auth-rate-limit.server.ts
T

40 lines
1.5 KiB
TypeScript

import { createHash } from "node:crypto";
import { cache } from "~/utils/cache.server";
import { getClientIPAddress } from "~/utils/server/IpUtils";
type AuthRateLimitOptions = {
request: Request;
action: "login" | "forgot-password" | "reset-password" | "mfa";
identifier?: string | null;
limit: number;
windowMs: number;
};
type AuthRateLimitResult =
| { allowed: true }
| { allowed: false; retryAfterSeconds: number };
export function getAuthRateLimitKey({ request, action, identifier }: Omit<AuthRateLimitOptions, "limit" | "windowMs">) {
const ipAddress = getClientIPAddress(request.headers)?.toString() ?? "unknown";
const normalizedIdentifier = identifier?.trim().toLowerCase() ?? "";
const digest = createHash("sha256").update(`${action}:${ipAddress}:${normalizedIdentifier}`).digest("hex");
return `auth-rate-limit:${action}:${digest}`;
}
export function checkAuthRateLimit(options: AuthRateLimitOptions): AuthRateLimitResult {
const key = getAuthRateLimitKey(options);
const now = Date.now();
const entry = cache.get(key)?.value as { count: number; resetAt: number } | undefined;
const current = entry && entry.resetAt > now ? entry : { count: 0, resetAt: now + options.windowMs };
const count = current.count + 1;
cache.set(key, { value: { count, resetAt: current.resetAt }, metadata: { createdTime: now } }, { ttl: Math.max(1, current.resetAt - now) });
if (count > options.limit) {
return { allowed: false, retryAfterSeconds: Math.max(1, Math.ceil((current.resetAt - now) / 1000)) };
}
return { allowed: true };
}