/** * Rate Limit Metrics API * * Exposes rate limiting metrics for monitoring and analysis. * Requires admin/moderator authentication. */ import { createClient } from 'jsr:@supabase/supabase-js@2'; import { withRateLimit, rateLimiters } from '../_shared/rateLimiter.ts'; import { getRecentMetrics, getMetricsStats, getFunctionMetrics, getUserMetrics, getIPMetrics, clearMetrics, } from '../_shared/rateLimitMetrics.ts'; const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', }; interface QueryParams { action?: string; limit?: string; timeWindow?: string; functionName?: string; userId?: string; clientIP?: string; } async function handler(req: Request): Promise { // Handle CORS preflight if (req.method === 'OPTIONS') { return new Response(null, { headers: corsHeaders }); } try { // Verify authentication const authHeader = req.headers.get('Authorization'); if (!authHeader) { return new Response( JSON.stringify({ error: 'Authentication required' }), { status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); } const supabaseUrl = Deno.env.get('SUPABASE_URL')!; const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!; const supabase = createClient(supabaseUrl, supabaseServiceKey, { global: { headers: { Authorization: authHeader }, }, }); // Get authenticated user const { data: { user }, error: authError } = await supabase.auth.getUser(); if (authError || !user) { return new Response( JSON.stringify({ error: 'Invalid authentication' }), { status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); } // Check if user has admin or moderator role const { data: roles } = await supabase .from('user_roles') .select('role') .eq('user_id', user.id); const userRoles = roles?.map(r => r.role) || []; const isAuthorized = userRoles.some(role => ['admin', 'moderator', 'superuser'].includes(role) ); if (!isAuthorized) { return new Response( JSON.stringify({ error: 'Insufficient permissions' }), { status: 403, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); } // Parse query parameters const url = new URL(req.url); const action = url.searchParams.get('action') || 'stats'; const limit = parseInt(url.searchParams.get('limit') || '100', 10); const timeWindow = parseInt(url.searchParams.get('timeWindow') || '60000', 10); const functionName = url.searchParams.get('functionName'); const userId = url.searchParams.get('userId'); const clientIP = url.searchParams.get('clientIP'); let responseData: any; // Route to appropriate metrics handler switch (action) { case 'recent': responseData = { metrics: getRecentMetrics(limit), count: getRecentMetrics(limit).length, }; break; case 'stats': responseData = getMetricsStats(timeWindow); break; case 'function': if (!functionName) { return new Response( JSON.stringify({ error: 'functionName parameter required for function action' }), { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); } responseData = { functionName, metrics: getFunctionMetrics(functionName, limit), count: getFunctionMetrics(functionName, limit).length, }; break; case 'user': if (!userId) { return new Response( JSON.stringify({ error: 'userId parameter required for user action' }), { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); } responseData = { userId, metrics: getUserMetrics(userId, limit), count: getUserMetrics(userId, limit).length, }; break; case 'ip': if (!clientIP) { return new Response( JSON.stringify({ error: 'clientIP parameter required for ip action' }), { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); } responseData = { clientIP, metrics: getIPMetrics(clientIP, limit), count: getIPMetrics(clientIP, limit).length, }; break; case 'clear': // Only superusers can clear metrics const isSuperuser = userRoles.includes('superuser'); if (!isSuperuser) { return new Response( JSON.stringify({ error: 'Only superusers can clear metrics' }), { status: 403, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); } clearMetrics(); responseData = { success: true, message: 'Metrics cleared' }; break; default: return new Response( JSON.stringify({ error: 'Invalid action', validActions: ['recent', 'stats', 'function', 'user', 'ip', 'clear'] }), { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); } return new Response( JSON.stringify(responseData), { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json', } } ); } catch (error) { console.error('Error in rate-limit-metrics function:', error); return new Response( JSON.stringify({ error: 'Internal server error', message: error instanceof Error ? error.message : 'Unknown error' }), { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); } } // Apply rate limiting (lenient tier for admin monitoring) Deno.serve(withRateLimit(handler, rateLimiters.lenient, corsHeaders, 'rate-limit-metrics'));