auto-claude: subtask-2-1 - Create Supabase-based rate limiting utility

This commit is contained in:
2026-01-25 06:41:28 +01:00
parent d529640d07
commit e9082be2d9
+163
View File
@@ -0,0 +1,163 @@
import { createClient } from '@/lib/supabase/server';
interface RateLimitRecord {
identifier: string;
count: number;
window_start: string;
created_at?: string;
updated_at?: string;
}
const WINDOW_SIZE_MS = 3600000; // 1 hour
const MAX_REQUESTS = 5; // Maximum requests per hour
class SupabaseRateLimiter {
/**
* Check if an identifier (IP address) is rate limited
* @param identifier - The IP address or unique identifier to check
* @returns true if rate limited, false otherwise
*/
async isRateLimited(identifier: string): Promise<boolean> {
const supabase = await createClient();
const now = Date.now();
const windowStart = new Date(now - WINDOW_SIZE_MS).toISOString();
try {
// Get the current rate limit record for this identifier
const { data: existingRecord, error: fetchError } = await supabase
.from('rate_limits')
.select('*')
.eq('identifier', identifier)
.gte('window_start', windowStart)
.order('window_start', { ascending: false })
.limit(1)
.single();
if (fetchError && fetchError.code !== 'PGRST116') {
// PGRST116 is "no rows returned" which is fine
console.error('Error fetching rate limit:', fetchError);
return false; // Fail open - don't block on errors
}
// No existing record or window expired - create new record
if (!existingRecord || new Date(existingRecord.window_start).getTime() < now - WINDOW_SIZE_MS) {
const { error: insertError } = await supabase
.from('rate_limits')
.insert({
identifier,
count: 1,
window_start: new Date(now).toISOString()
});
if (insertError) {
console.error('Error inserting rate limit:', insertError);
return false; // Fail open
}
return false; // First request in new window
}
// Check if limit exceeded
if (existingRecord.count >= MAX_REQUESTS) {
return true; // Rate limited
}
// Increment counter
const { error: updateError } = await supabase
.from('rate_limits')
.update({
count: existingRecord.count + 1,
updated_at: new Date().toISOString()
})
.eq('identifier', identifier)
.eq('window_start', existingRecord.window_start);
if (updateError) {
console.error('Error updating rate limit:', updateError);
return false; // Fail open
}
return false; // Not rate limited yet
} catch (error) {
console.error('Unexpected error in rate limiting:', error);
return false; // Fail open on unexpected errors
}
}
/**
* Get the number of remaining attempts for an identifier
* @param identifier - The IP address or unique identifier to check
* @returns Number of remaining attempts (0 if rate limited)
*/
async getRemainingAttempts(identifier: string): Promise<number> {
const supabase = await createClient();
const now = Date.now();
const windowStart = new Date(now - WINDOW_SIZE_MS).toISOString();
try {
const { data: record, error } = await supabase
.from('rate_limits')
.select('count')
.eq('identifier', identifier)
.gte('window_start', windowStart)
.order('window_start', { ascending: false })
.limit(1)
.single();
if (error && error.code !== 'PGRST116') {
console.error('Error fetching remaining attempts:', error);
return MAX_REQUESTS; // Fail open
}
if (!record) {
return MAX_REQUESTS; // No record yet
}
return Math.max(0, MAX_REQUESTS - record.count);
} catch (error) {
console.error('Unexpected error getting remaining attempts:', error);
return MAX_REQUESTS; // Fail open
}
}
/**
* Get the time in milliseconds until the rate limit resets
* @param identifier - The IP address or unique identifier to check
* @returns Milliseconds until reset (0 if no active limit)
*/
async getTimeToReset(identifier: string): Promise<number> {
const supabase = await createClient();
const now = Date.now();
const windowStart = new Date(now - WINDOW_SIZE_MS).toISOString();
try {
const { data: record, error } = await supabase
.from('rate_limits')
.select('window_start')
.eq('identifier', identifier)
.gte('window_start', windowStart)
.order('window_start', { ascending: false })
.limit(1)
.single();
if (error && error.code !== 'PGRST116') {
console.error('Error fetching time to reset:', error);
return 0;
}
if (!record) {
return 0; // No active window
}
const windowStartTime = new Date(record.window_start).getTime();
const resetTime = windowStartTime + WINDOW_SIZE_MS;
return Math.max(0, resetTime - now);
} catch (error) {
console.error('Unexpected error getting time to reset:', error);
return 0;
}
}
}
export const supabaseRateLimiter = new SupabaseRateLimiter();
export { WINDOW_SIZE_MS, MAX_REQUESTS };