mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
Drop fourteen exported helpers and constants with zero call sites: countMessages, buildGeneratePrompts, _resetSafetyContractsCacheForTests, forEachLocaleAndRule, getPulseRange, trendLinePoints, CHART_MINI_HEIGHT_PX, formatDateWithWeekday, resolveGeneralStatusLocale, isSubPageSlug, recordHeartbeat, EVENT_TYPE_LABELS, cleanupExpiredRateLimits, and sleepStageEnum. Also retire the v1.4.20 legacy insights prompt scaffold — getInsightsSystemPrompt, InsightsOutput, and the two dead INSIGHTS_SYSTEM_PROMPT_EN/DE constants — plus the matching vi.mock key in the generate route test that no longer maps to a live export. Drop the empty __testables block in glp1-snapshot.ts, the legacy helper reference in the insight-generator JSDoc, and the two stale v1.4.16 InsightsPageHero comments in hero-strip + its test file. Consume the MedicationComplianceChart compareBaseline prop via an explicit void so the destructure documents the intentional accept-and- defer pattern instead of looking like a missed wire-up. Every symbol verified zero-caller via grep across src/, scripts/, and __tests__/ before drop.
65 lines
1.7 KiB
TypeScript
65 lines
1.7 KiB
TypeScript
/**
|
|
* PostgreSQL-based sliding window rate limiter.
|
|
* Uses atomic upsert via raw SQL for correctness across multiple instances.
|
|
*/
|
|
|
|
import { prisma } from "@/lib/db";
|
|
|
|
export interface RateLimitResult {
|
|
allowed: boolean;
|
|
remaining: number;
|
|
resetAt: number;
|
|
}
|
|
|
|
/**
|
|
* Check rate limit for a given key.
|
|
* Uses a single atomic SQL upsert — safe for concurrent requests.
|
|
* @param key Unique identifier (e.g., "auth:login:1.2.3.4")
|
|
* @param limit Max requests per window
|
|
* @param windowMs Window duration in milliseconds
|
|
*/
|
|
export async function checkRateLimit(
|
|
key: string,
|
|
limit: number,
|
|
windowMs: number,
|
|
): Promise<RateLimitResult> {
|
|
const windowInterval = `${windowMs} milliseconds`;
|
|
|
|
const rows = await prisma.$queryRaw<{ count: number; reset_at: Date }[]>`
|
|
INSERT INTO rate_limits (key, count, reset_at)
|
|
VALUES (${key}, 1, NOW() + ${windowInterval}::interval)
|
|
ON CONFLICT (key) DO UPDATE SET
|
|
count = CASE
|
|
WHEN rate_limits.reset_at < NOW() THEN 1
|
|
ELSE rate_limits.count + 1
|
|
END,
|
|
reset_at = CASE
|
|
WHEN rate_limits.reset_at < NOW() THEN NOW() + ${windowInterval}::interval
|
|
ELSE rate_limits.reset_at
|
|
END
|
|
RETURNING count, reset_at
|
|
`;
|
|
|
|
const row = rows[0];
|
|
const resetAt = row.reset_at.getTime();
|
|
const count = Number(row.count);
|
|
|
|
return {
|
|
allowed: count <= limit,
|
|
remaining: Math.max(0, limit - count),
|
|
resetAt,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Rate limit response headers.
|
|
*/
|
|
export function rateLimitHeaders(
|
|
result: RateLimitResult,
|
|
): Record<string, string> {
|
|
return {
|
|
"X-RateLimit-Remaining": String(result.remaining),
|
|
"X-RateLimit-Reset": new Date(result.resetAt).toISOString(),
|
|
};
|
|
}
|