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 { try { const supabase = await createClient(); const now = Date.now(); const windowStart = new Date(now - WINDOW_SIZE_MS).toISOString(); // 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('[RateLimiter] Unexpected error in isRateLimited - FAILING OPEN:', error); return false; // Fail open on ALL 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 { try { const supabase = await createClient(); const now = Date.now(); const windowStart = new Date(now - WINDOW_SIZE_MS).toISOString(); 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('[RateLimiter] Unexpected error in getRemainingAttempts - FAILING OPEN:', error); return MAX_REQUESTS; // Fail open on ALL errors } } /** * 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 { try { const supabase = await createClient(); const now = Date.now(); const windowStart = new Date(now - WINDOW_SIZE_MS).toISOString(); 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('[RateLimiter] Unexpected error in getTimeToReset - FAILING OPEN:', error); return 0; // Fail open on ALL errors } } } export const supabaseRateLimiter = new SupabaseRateLimiter(); export { WINDOW_SIZE_MS, MAX_REQUESTS };