5b40968c9d
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.
52 lines
1.4 KiB
TypeScript
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;
|
|
}
|