Files
Calbook/app/api/public/slots/route.ts

70 lines
2.0 KiB
TypeScript

export const dynamic = "force-dynamic";
import { calculateSlotsForDisplayDate } from "@/lib/services/availability";
import { slotsQuerySchema } from "@/lib/validators/public";
import { fail, ok } from "@/lib/api";
import { enforceRateLimit } from "@/lib/rate-limit";
import { resolveTimeZone } from "@/lib/date";
type DaySlotsCacheEntry = {
slots: Awaited<ReturnType<typeof calculateSlotsForDisplayDate>>;
expiresAt: number;
};
declare global {
// eslint-disable-next-line no-var
var calbookDaySlotsCache: Map<string, DaySlotsCacheEntry> | undefined;
}
const daySlotsCache = global.calbookDaySlotsCache ?? new Map<string, DaySlotsCacheEntry>();
if (!global.calbookDaySlotsCache) {
global.calbookDaySlotsCache = daySlotsCache;
}
const DAY_SLOTS_CACHE_TTL_MS = Number(process.env.SLOTS_DAY_CACHE_TTL_MS ?? "6000");
export async function GET(req: Request) {
const limit = enforceRateLimit({
req,
scope: "public-slots",
limit: 120,
windowMs: 60_000
});
if (!limit.ok) {
return fail("Zu viele Anfragen. Bitte kurz warten.", 429, {
retryAfterSeconds: limit.retryAfterSeconds
});
}
const url = new URL(req.url);
const parsed = slotsQuerySchema.safeParse({
mitarbeiterId: url.searchParams.get("mitarbeiterId") ?? undefined,
datum: url.searchParams.get("datum"),
timezone: url.searchParams.get("timezone") ?? undefined
});
if (!parsed.success) {
return fail("Ungültige Parameter", 400, parsed.error.flatten());
}
const timezone = resolveTimeZone(parsed.data.timezone);
const cacheKey = `${parsed.data.mitarbeiterId ?? "all"}|${parsed.data.datum}|${timezone}`;
const cached = daySlotsCache.get(cacheKey);
if (cached && cached.expiresAt > Date.now()) {
return ok({ slots: cached.slots });
}
const results = await calculateSlotsForDisplayDate(
parsed.data.mitarbeiterId,
parsed.data.datum,
{ displayTimezone: timezone }
);
daySlotsCache.set(cacheKey, {
slots: results,
expiresAt: Date.now() + DAY_SLOTS_CACHE_TTL_MS
});
return ok({ slots: results });
}