auto-claude: subtask-2-1 - Create Supabase-based rate limiting utility
This commit is contained in:
@@ -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 };
|
||||||
Reference in New Issue
Block a user