From 1d7d66d65c4d69597dabc1694e12ce64d487716f Mon Sep 17 00:00:00 2001 From: Naman Singh Date: Sat, 11 Jul 2026 13:05:39 +0530 Subject: [PATCH 1/2] fix: add rate limiting to non-friends leaderboard scopes Closes #657 Only the friends scope had rate limiting via requireUser. All other scopes (global, cohort, language, tag, monthly) could be called at high frequency, enabling database resource exhaustion. Added a shared rate limit (30 req/60s) for non-friends scopes before any expensive DB queries execute. --- src/app/actions/leaderboard.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/app/actions/leaderboard.ts b/src/app/actions/leaderboard.ts index 85b7b145..15b456aa 100644 --- a/src/app/actions/leaderboard.ts +++ b/src/app/actions/leaderboard.ts @@ -7,6 +7,7 @@ import { ok, err, type Result } from '@/lib/result'; import { getServerSupabase } from '@/lib/supabase/server'; import { getAppOctokit, getInstallOctokit } from '@/lib/github/app'; import { requireUser } from '@/lib/action-auth'; +import { RATE_LIMIT_TIERS } from '@/lib/rate-limit'; export type LeaderboardScope = 'global' | 'cohort' | 'language' | 'tag' | 'monthly' | 'friends'; @@ -123,6 +124,15 @@ export async function getLeaderboard( const db = tryGetDb(); if (!db) return err('not_configured', 'database not configured'); + // Rate-limit non-friends scopes to prevent database resource exhaustion. + if (scope !== 'friends') { + const rlRes = await requireUser({ + rateLimit: { namespace: 'leaderboard', limit: 30, windowSec: 60 }, + rateLimitMessage: 'too many leaderboard requests, slow down', + }); + if (!rlRes.ok) return rlRes; + } + let rows: { id: string; github_handle: string; From aed452b117714fae6b90068000502491cb14542b Mon Sep 17 00:00:00 2001 From: Naman Singh Date: Mon, 13 Jul 2026 13:09:05 +0530 Subject: [PATCH 2/2] fix: move rate limit inside cache check, use RATE_LIMIT_TIERS.STANDARD --- src/app/actions/leaderboard.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/app/actions/leaderboard.ts b/src/app/actions/leaderboard.ts index 15b456aa..396335f8 100644 --- a/src/app/actions/leaderboard.ts +++ b/src/app/actions/leaderboard.ts @@ -124,15 +124,6 @@ export async function getLeaderboard( const db = tryGetDb(); if (!db) return err('not_configured', 'database not configured'); - // Rate-limit non-friends scopes to prevent database resource exhaustion. - if (scope !== 'friends') { - const rlRes = await requireUser({ - rateLimit: { namespace: 'leaderboard', limit: 30, windowSec: 60 }, - rateLimitMessage: 'too many leaderboard requests, slow down', - }); - if (!rlRes.ok) return rlRes; - } - let rows: { id: string; github_handle: string; @@ -146,6 +137,15 @@ export async function getLeaderboard( }[] = []; if (!cached) { + // Rate-limit non-friends scopes to prevent database resource exhaustion. + if (scope !== 'friends') { + const rlRes = await requireUser({ + rateLimit: { namespace: 'leaderboard', ...RATE_LIMIT_TIERS.STANDARD }, + rateLimitMessage: 'too many leaderboard requests, slow down', + }); + if (!rlRes.ok) return rlRes; + } + if (scope === 'global') { rows = (await db.execute(sql` select id, github_handle, display_name, avatar_url, xp, level, github_total_merges, github_streak,