Files
Arnaud 5b40968c9d feat(web): Next.js 15 app shell, auth, i18n, base UI
App Router setup with next-intl (en/fr), Better Auth wiring, shadcn/ui components,
Tailwind, AES-256-GCM encrypt util, Redis rate limiter, tier limit checker,
site_settings helper for runtime .env overrides.
2026-07-01 08:09:31 +02:00

52 lines
1.4 KiB
TypeScript

import { getRedis } from "./redis";
import { NextResponse } from "next/server";
export async function rateLimit(
key: string,
limit: number,
windowSeconds: number
): Promise<{ ok: boolean; remaining: number; reset: number }> {
const redis = getRedis();
const now = Date.now();
const windowStart = now - windowSeconds * 1000;
const reset = Math.ceil((now + windowSeconds * 1000) / 1000);
const pipeline = redis.pipeline();
pipeline.zremrangebyscore(key, "-inf", windowStart);
pipeline.zadd(key, now, `${now}-${Math.random()}`);
pipeline.zcard(key);
pipeline.expire(key, windowSeconds);
const results = await pipeline.exec();
const count = (results?.[2]?.[1] as number) ?? 0;
const remaining = Math.max(0, limit - count);
const ok = count <= limit;
return { ok, remaining, reset };
}
export async function applyRateLimit(
key: string,
limit: number,
windowSeconds: number
): Promise<NextResponse | null> {
const { ok, remaining, reset } = await rateLimit(key, limit, windowSeconds);
if (!ok) {
return NextResponse.json(
{ error: "Too many requests", retryAfter: reset },
{
status: 429,
headers: {
"X-RateLimit-Limit": String(limit),
"X-RateLimit-Remaining": "0",
"X-RateLimit-Reset": String(reset),
"Retry-After": String(reset - Math.floor(Date.now() / 1000)),
},
}
);
}
return null;
}