forked from Goutam/lynkeduppro-crm
62 lines
1.9 KiB
TypeScript
62 lines
1.9 KiB
TypeScript
/**
|
|
* Shared entry gate for every /api/gallery/ai/* route: authenticate, then
|
|
* throttle. Lives in a `_`-prefixed file so the App Router never treats it as a
|
|
* route (only `route.ts` defines an endpoint).
|
|
*
|
|
* Order matters: we authenticate FIRST so the rate limit can be keyed by
|
|
* principal rather than by a spoofable `x-forwarded-for` hop wherever possible.
|
|
* The session check is one cheap BFF round trip; the work it guards is a GPU
|
|
* call, so paying it before throttling is the right trade.
|
|
*
|
|
* SERVER-ONLY.
|
|
*/
|
|
|
|
import { NextResponse } from "next/server";
|
|
|
|
import { limit } from "@/lib/server/rate-limit";
|
|
import { rateLimitKey, requireGallerySession } from "@/lib/server/session";
|
|
|
|
/** Per-minute budgets, per the Smart Gallery route contract. */
|
|
export const RATE_LIMITS = {
|
|
classify: 30,
|
|
edit: 12,
|
|
tilt: 30,
|
|
transcribe: 20,
|
|
denoise: 20,
|
|
} as const;
|
|
|
|
const WINDOW_MS = 60_000;
|
|
|
|
export type GuardResult =
|
|
| { ok: true; principalId?: string }
|
|
/** Ready-to-return error response — the route should return it unchanged. */
|
|
| { ok: false; response: NextResponse };
|
|
|
|
/**
|
|
* @param route Which budget to apply (also namespaces the limiter key so a
|
|
* caller's `edit` spend does not consume their `classify` budget).
|
|
*/
|
|
export async function guard(req: Request, route: keyof typeof RATE_LIMITS): Promise<GuardResult> {
|
|
const session = await requireGallerySession(req);
|
|
if (!session.ok) {
|
|
return {
|
|
ok: false,
|
|
response: NextResponse.json({ error: session.error }, { status: session.status }),
|
|
};
|
|
}
|
|
|
|
const key = `${route}:${rateLimitKey(req, session.principalId)}`;
|
|
const { ok, retryAfter } = limit(key, RATE_LIMITS[route], WINDOW_MS);
|
|
if (!ok) {
|
|
return {
|
|
ok: false,
|
|
response: NextResponse.json(
|
|
{ error: "Too many requests — slow down." },
|
|
{ status: 429, headers: { "Retry-After": String(retryAfter) } },
|
|
),
|
|
};
|
|
}
|
|
|
|
return { ok: true, principalId: session.principalId };
|
|
}
|