diff options
| author | Fuwn <[email protected]> | 2026-02-09 23:41:01 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-02-09 23:41:01 -0800 |
| commit | 56244758d94c14349540bd0951339fa939156204 (patch) | |
| tree | 3fba880cda09c0e8d913dc30884182df5e6a73ee /apps/web/lib | |
| parent | fix: use online networkMode for offline mutations instead of offlineFirst (diff) | |
| download | asa.news-56244758d94c14349540bd0951339fa939156204.tar.xz asa.news-56244758d94c14349540bd0951339fa939156204.zip | |
fix: P0 correctness and security fixes
- Add missing 'developer' case to check_custom_feed_limit trigger (was falling through to else 1)
- Scope user_entry_states join to authenticated user in /api/v1/entries (admin client bypasses RLS)
- Replace in-memory rate limiting with Supabase-backed solution (UNLOGGED table + check_rate_limit RPC + pg_cron cleanup)
Diffstat (limited to 'apps/web/lib')
| -rw-r--r-- | apps/web/lib/api-auth.ts | 2 | ||||
| -rw-r--r-- | apps/web/lib/rate-limit.ts | 36 |
2 files changed, 19 insertions, 19 deletions
diff --git a/apps/web/lib/api-auth.ts b/apps/web/lib/api-auth.ts index e491c11..7c1d009 100644 --- a/apps/web/lib/api-auth.ts +++ b/apps/web/lib/api-auth.ts @@ -57,7 +57,7 @@ export async function authenticateApiRequest( } } - const rateLimitResult = rateLimit(`api:${keyRow.user_id}`, 100, 60_000) + const rateLimitResult = await rateLimit(`api:${keyRow.user_id}`, 100, 60_000) if (!rateLimitResult.success) { return { diff --git a/apps/web/lib/rate-limit.ts b/apps/web/lib/rate-limit.ts index 506511d..c68f02c 100644 --- a/apps/web/lib/rate-limit.ts +++ b/apps/web/lib/rate-limit.ts @@ -1,26 +1,26 @@ -const requestTimestamps = new Map<string, number[]>() +import { createSupabaseAdminClient } from "@/lib/supabase/admin" -export function rateLimit( +export async function rateLimit( identifier: string, limit: number, windowMilliseconds: number -): { success: boolean; remaining: number } { - const now = Date.now() - const timestamps = requestTimestamps.get(identifier) ?? [] - const windowStart = now - windowMilliseconds - const recentTimestamps = timestamps.filter( - (timestamp) => timestamp > windowStart - ) +): Promise<{ success: boolean; remaining: number }> { + const windowSeconds = Math.max(Math.floor(windowMilliseconds / 1000), 1) + const adminClient = createSupabaseAdminClient() - if (recentTimestamps.length === 0) { - requestTimestamps.delete(identifier) - } else if (recentTimestamps.length >= limit) { - requestTimestamps.set(identifier, recentTimestamps) - return { success: false, remaining: 0 } - } + const { data, error } = await adminClient.rpc("check_rate_limit", { + p_identifier: identifier, + p_limit: limit, + p_window_seconds: windowSeconds, + }) - recentTimestamps.push(now) - requestTimestamps.set(identifier, recentTimestamps) + if (error) { + console.error("rate limit check failed:", error) + return { success: true, remaining: limit } + } - return { success: true, remaining: limit - recentTimestamps.length } + return { + success: data.success as boolean, + remaining: data.remaining as number, + } } |