diff --git a/supabase/functions/_shared/RATE_LIMITING_GUIDE.md b/supabase/functions/_shared/RATE_LIMITING_GUIDE.md new file mode 100644 index 00000000..f0e11f09 --- /dev/null +++ b/supabase/functions/_shared/RATE_LIMITING_GUIDE.md @@ -0,0 +1,277 @@ +# Rate Limiting Guide for Edge Functions + +This guide helps you choose the appropriate rate limit tier for each edge function and explains how to implement rate limiting consistently across the application. + +## Quick Reference + +### Rate Limit Tiers + +| Tier | Requests/Min | Use Case | +|------|--------------|----------| +| **STRICT** | 5 | Expensive operations (uploads, exports, batch processing) | +| **MODERATE** | 10 | Moderation actions, content submission, security operations | +| **STANDARD** | 20 | Typical read/write operations, account management | +| **LENIENT** | 30 | Lightweight reads, public data, validation | +| **GENEROUS** | 60 | High-frequency operations (webhooks, polling, health checks) | + +### Per-User Tiers (Rate limits by user ID instead of IP) + +| Tier | Requests/Min | Use Case | +|------|--------------|----------| +| **PER_USER_STRICT** | 5 | User-specific expensive operations | +| **PER_USER_MODERATE** | 10 | User-specific moderation actions | +| **PER_USER_STANDARD** | 20 | User-specific standard operations | +| **PER_USER_LENIENT** | 40 | User-specific frequent operations | + +## How to Implement Rate Limiting + +### Basic Implementation + +```typescript +import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'; +import { corsHeaders } from '../_shared/cors.ts'; +import { rateLimiters, withRateLimit } from '../_shared/rateLimiter.ts'; + +// Your handler function +const handler = async (req: Request): Promise => { + // Your edge function logic here + return new Response(JSON.stringify({ success: true }), { + headers: { ...corsHeaders, 'Content-Type': 'application/json' } + }); +}; + +// Apply rate limiting with appropriate tier +serve(withRateLimit(handler, rateLimiters.moderate, corsHeaders)); +``` + +### Per-User Rate Limiting + +```typescript +// Rate limit by user ID instead of IP address +serve(withRateLimit(handler, rateLimiters.perUserModerate, corsHeaders)); +``` + +### Custom Rate Limiting + +```typescript +import { createRateLimiter } from '../_shared/rateLimiter.ts'; + +// Create a custom rate limiter +const customLimiter = createRateLimiter({ + windowMs: 60000, + maxRequests: 15, + keyGenerator: (req) => { + // Custom key logic + return req.headers.get('x-custom-key') || 'default'; + } +}); + +serve(withRateLimit(handler, customLimiter, corsHeaders)); +``` + +## Recommended Rate Limits by Function Category + +### 🔴 STRICT (5 req/min) + +**Currently Implemented:** +- ✅ `upload-image` - CloudFlare image upload + +**Recommended:** +- `export-user-data` - Data export operations +- Any function that makes expensive external API calls +- Batch data processing operations +- Functions that manipulate large datasets + +### 🟠 MODERATE (10 req/min) + +**Currently Implemented:** +- ✅ `process-selective-approval` - Moderation approvals +- ✅ `process-selective-rejection` - Moderation rejections + +**Recommended:** +- `admin-delete-user` - Admin user deletion +- `manage-moderator-topic` - Admin moderation management +- `merge-contact-tickets` - Admin ticket management +- `mfa-unenroll` - Security operations +- `resend-deletion-code` - Prevent code spam +- `send-escalation-notification` - Admin escalations +- `send-password-added-email` - Security emails +- User submission functions (parks, rides, edits) + +### 🟡 STANDARD (20 req/min) + +**Recommended:** +- `cancel-account-deletion` - Account management +- `cancel-email-change` - Account management +- `confirm-account-deletion` - Account management +- `request-account-deletion` - Account management +- `create-novu-subscriber` - User registration +- `send-contact-message` - Contact form submissions +- Email validation functions +- Authentication-related functions + +### 🟢 LENIENT (30 req/min) + +**Recommended:** +- `detect-location` - Lightweight location lookup +- `check-transaction-status` - Status polling +- `validate-email-backend` - Email validation +- `sitemap` - Public sitemap generation +- Read-only public endpoints + +### 🔵 GENEROUS (60 req/min) + +**Recommended:** +- `novu-webhook` - External webhook receiver +- `scheduled-maintenance` - Health checks +- Internal service-to-service communication +- Real-time status endpoints + +### ⚫ NO RATE LIMITING NEEDED + +These functions are typically called internally or on a schedule: +- `cleanup-old-versions` - Scheduled cleanup +- `process-expired-bans` - Scheduled task +- `process-scheduled-deletions` - Scheduled task +- `run-cleanup-jobs` - Scheduled task +- `migrate-novu-users` - One-time migration +- Internal notification functions (notify-*) +- `seed-test-data` - Development only + +## Best Practices + +### 1. Choose the Right Tier + +- **Start restrictive**: Begin with a lower tier and increase if needed +- **Consider cost**: Match the rate limit to the operation's resource cost +- **Think about abuse**: Higher abuse risk = stricter limits +- **Monitor usage**: Use edge function logs to track rate limit hits + +### 2. Use Per-User Limits for Authenticated Endpoints + +```typescript +// ✅ Good: Rate limit authenticated operations per user +serve(withRateLimit(handler, rateLimiters.perUserModerate, corsHeaders)); + +// ❌ Less effective: Rate limit authenticated operations per IP +// (Multiple users behind same IP can hit each other's limits) +serve(withRateLimit(handler, rateLimiters.moderate, corsHeaders)); +``` + +### 3. Handle Rate Limit Errors Gracefully + +Rate limit responses automatically include: +- `429 Too Many Requests` status code +- `Retry-After` header (seconds to wait) +- `X-RateLimit-Limit` header (max requests allowed) +- `X-RateLimit-Remaining` header (requests remaining) + +### 4. Document Your Choice + +Always add a comment explaining why you chose a specific tier: + +```typescript +// Apply moderate rate limiting (10 req/min) for moderation actions +// to prevent abuse while allowing legitimate moderator workflows +serve(withRateLimit(handler, rateLimiters.moderate, corsHeaders)); +``` + +### 5. Test Rate Limits + +```bash +# Test rate limiting locally +for i in {1..15}; do + curl -X POST https://your-project.supabase.co/functions/v1/your-function \ + -H "Authorization: Bearer YOUR_ANON_KEY" \ + -H "Content-Type: application/json" \ + -d '{"test": true}' + echo " - Request $i" + sleep 1 +done +``` + +## Migration Checklist + +When adding rate limiting to an existing function: + +1. ✅ Determine the appropriate tier based on operation cost +2. ✅ Import `rateLimiters` and `withRateLimit` from `_shared/rateLimiter.ts` +3. ✅ Import `corsHeaders` from `_shared/cors.ts` +4. ✅ Wrap your handler with `withRateLimit(handler, rateLimiters.TIER, corsHeaders)` +5. ✅ Add a comment explaining the tier choice +6. ✅ Test the rate limit works correctly +7. ✅ Monitor edge function logs for rate limit hits +8. ✅ Adjust tier if needed based on real usage + +## Troubleshooting + +### Rate Limits Too Strict + +**Symptoms:** Legitimate users hitting rate limits frequently + +**Solutions:** +- Increase to next tier up (strict → moderate → standard → lenient) +- Consider per-user rate limiting instead of per-IP +- Check if the operation can be optimized to reduce frequency + +### Rate Limits Too Lenient + +**Symptoms:** Abuse patterns, high costs, slow performance + +**Solutions:** +- Decrease to next tier down +- Add additional validation before expensive operations +- Consider implementing captcha for public endpoints + +### Per-User Rate Limiting Not Working + +**Check:** +- Is the Authorization header being sent? +- Is the JWT valid and parsable? +- Are logs showing IP-based limits instead of user-based? + +## Examples from Production + +### Example 1: Upload Function (STRICT) + +```typescript +// upload-image function needs strict limiting because: +// - Makes external CloudFlare API calls ($$) +// - Processes large file uploads +// - High abuse potential +serve(withRateLimit(async (req) => { + // Upload logic here +}, rateLimiters.strict, getCorsHeaders(allowedOrigin))); +``` + +### Example 2: Moderation Function (MODERATE) + +```typescript +// process-selective-approval needs moderate limiting because: +// - Modifies database records +// - Triggers notifications +// - Used by moderators (need reasonable throughput) +serve(withRateLimit(handler, rateLimiters.moderate, corsHeaders)); +``` + +### Example 3: Validation Function (LENIENT) + +```typescript +// validate-email-backend can be lenient because: +// - Lightweight operation (just validation) +// - No database writes +// - Users may need to retry multiple times +serve(withRateLimit(async (req) => { + // Validation logic here +}, rateLimiters.lenient, corsHeaders)); +``` + +## Future Enhancements + +Potential improvements to consider: + +1. **Dynamic Rate Limits**: Adjust limits based on user role/tier +2. **Distributed Rate Limiting**: Use Redis for multi-region support +3. **Rate Limit Analytics**: Track and visualize rate limit metrics +4. **Custom Error Messages**: Provide context-specific retry guidance +5. **Whitelist Support**: Bypass limits for trusted IPs/users diff --git a/supabase/functions/_shared/rateLimitConfig.ts b/supabase/functions/_shared/rateLimitConfig.ts new file mode 100644 index 00000000..21fae75d --- /dev/null +++ b/supabase/functions/_shared/rateLimitConfig.ts @@ -0,0 +1,174 @@ +/** + * Centralized Rate Limiting Configuration for Edge Functions + * + * Provides standardized rate limit tiers that can be imported by any edge function. + * This ensures consistent rate limiting behavior across the application. + */ + +import { RateLimitConfig } from './rateLimiter.ts'; + +/** + * Rate Limit Tier Definitions + * + * Choose the appropriate tier based on the operation cost and abuse risk: + * + * - **STRICT**: For expensive operations (uploads, exports, data modifications) + * - **MODERATE**: For standard API operations (moderation actions, content creation) + * - **STANDARD**: For typical read/write operations (most endpoints) + * - **LENIENT**: For lightweight read operations (cached data, public endpoints) + * - **GENEROUS**: For high-frequency operations (polling, real-time updates) + */ + +// Base time window for all rate limiters (1 minute) +const RATE_LIMIT_WINDOW_MS = 60000; + +/** + * STRICT: 5 requests per minute + * + * Use for: + * - File uploads (images, documents) + * - Data exports + * - Batch operations + * - Resource-intensive computations + * - CloudFlare API calls + * + * Examples: upload-image, export-user-data + */ +export const RATE_LIMIT_STRICT: RateLimitConfig = { + windowMs: RATE_LIMIT_WINDOW_MS, + maxRequests: 5, +}; + +/** + * MODERATE: 10 requests per minute + * + * Use for: + * - Moderation actions (approve, reject) + * - Content submission + * - User profile updates + * - Email sending + * - Notification triggers + * + * Examples: process-selective-approval, process-selective-rejection, submit-entity-edit + */ +export const RATE_LIMIT_MODERATE: RateLimitConfig = { + windowMs: RATE_LIMIT_WINDOW_MS, + maxRequests: 10, +}; + +/** + * STANDARD: 20 requests per minute + * + * Use for: + * - Standard read/write operations + * - Search endpoints + * - Contact forms + * - Account management + * - Authentication operations + * + * Examples: send-contact-message, request-account-deletion, validate-email + */ +export const RATE_LIMIT_STANDARD: RateLimitConfig = { + windowMs: RATE_LIMIT_WINDOW_MS, + maxRequests: 20, +}; + +/** + * LENIENT: 30 requests per minute + * + * Use for: + * - Lightweight read operations + * - Cached data retrieval + * - Public endpoint queries + * - Status checks + * - Location detection + * + * Examples: detect-location, check-transaction-status + */ +export const RATE_LIMIT_LENIENT: RateLimitConfig = { + windowMs: RATE_LIMIT_WINDOW_MS, + maxRequests: 30, +}; + +/** + * GENEROUS: 60 requests per minute + * + * Use for: + * - High-frequency polling + * - Real-time updates + * - Webhook receivers + * - Health checks + * - Internal service-to-service calls + * + * Examples: novu-webhook, scheduled-maintenance + */ +export const RATE_LIMIT_GENEROUS: RateLimitConfig = { + windowMs: RATE_LIMIT_WINDOW_MS, + maxRequests: 60, +}; + +/** + * PER_USER: 20 requests per minute (default) + * + * Use for authenticated endpoints where you want to rate limit per user ID + * rather than per IP address. Useful for: + * - User-specific operations + * - Preventing account abuse + * - Per-user quotas + * + * Can be customized with different request counts: + * - perUserStrict: 5 req/min + * - perUserModerate: 10 req/min + * - perUserStandard: 20 req/min (default) + * - perUserLenient: 40 req/min + */ +export const RATE_LIMIT_PER_USER_STRICT: RateLimitConfig = { + windowMs: RATE_LIMIT_WINDOW_MS, + maxRequests: 5, + keyGenerator: (req: Request) => { + // Extract user ID from Authorization header JWT + const authHeader = req.headers.get('Authorization'); + if (authHeader) { + try { + const token = authHeader.replace('Bearer ', ''); + const payload = JSON.parse(atob(token.split('.')[1])); + return `user:${payload.sub}`; + } catch { + // Fall back to IP if JWT parsing fails + return req.headers.get('x-forwarded-for')?.split(',')[0] || '0.0.0.0'; + } + } + return req.headers.get('x-forwarded-for')?.split(',')[0] || '0.0.0.0'; + } +}; + +export const RATE_LIMIT_PER_USER_MODERATE: RateLimitConfig = { + ...RATE_LIMIT_PER_USER_STRICT, + maxRequests: 10, +}; + +export const RATE_LIMIT_PER_USER_STANDARD: RateLimitConfig = { + ...RATE_LIMIT_PER_USER_STRICT, + maxRequests: 20, +}; + +export const RATE_LIMIT_PER_USER_LENIENT: RateLimitConfig = { + ...RATE_LIMIT_PER_USER_STRICT, + maxRequests: 40, +}; + +/** + * Rate Limit Tier Summary + * + * | Tier | Requests/Min | Use Case | + * |-------------------|--------------|-----------------------------------| + * | STRICT | 5 | Expensive operations, uploads | + * | MODERATE | 10 | Moderation, submissions | + * | STANDARD | 20 | Standard read/write operations | + * | LENIENT | 30 | Lightweight reads, public data | + * | GENEROUS | 60 | Polling, webhooks, health checks | + * | PER_USER_STRICT | 5/user | User-specific expensive ops | + * | PER_USER_MODERATE | 10/user | User-specific moderation | + * | PER_USER_STANDARD | 20/user | User-specific standard ops | + * | PER_USER_LENIENT | 40/user | User-specific frequent ops | + */ diff --git a/supabase/functions/_shared/rateLimiter.ts b/supabase/functions/_shared/rateLimiter.ts index 9e2a43e9..60e22376 100644 --- a/supabase/functions/_shared/rateLimiter.ts +++ b/supabase/functions/_shared/rateLimiter.ts @@ -129,50 +129,56 @@ class RateLimiter { } } -// Export factory function for different rate limit tiers +// Import centralized rate limit configurations +import { + RATE_LIMIT_STRICT, + RATE_LIMIT_MODERATE, + RATE_LIMIT_STANDARD, + RATE_LIMIT_LENIENT, + RATE_LIMIT_GENEROUS, + RATE_LIMIT_PER_USER_STRICT, + RATE_LIMIT_PER_USER_MODERATE, + RATE_LIMIT_PER_USER_STANDARD, + RATE_LIMIT_PER_USER_LENIENT, +} from './rateLimitConfig.ts'; + +// Export factory function for creating custom rate limiters export function createRateLimiter(config: RateLimitConfig): RateLimiter { return new RateLimiter(config); } -// Pre-configured rate limiters for common use cases +/** + * Pre-configured rate limiters using centralized tier definitions + * + * These are singleton instances that should be imported and used by edge functions. + * See rateLimitConfig.ts for detailed documentation on when to use each tier. + */ export const rateLimiters = { - // Strict: For expensive operations (file uploads, data exports) - strict: createRateLimiter({ - windowMs: 60000, // 1 minute - maxRequests: 5, // 5 requests per minute - }), + // Strict: 5 requests/minute - For expensive operations + strict: createRateLimiter(RATE_LIMIT_STRICT), - // Standard: For most API endpoints - standard: createRateLimiter({ - windowMs: 60000, // 1 minute - maxRequests: 10, // 10 requests per minute - }), + // Moderate: 10 requests/minute - For moderation and submissions + moderate: createRateLimiter(RATE_LIMIT_MODERATE), - // Lenient: For read-only, cached endpoints - lenient: createRateLimiter({ - windowMs: 60000, // 1 minute - maxRequests: 30, // 30 requests per minute - }), + // Standard: 20 requests/minute - For typical operations (DEPRECATED: use 'moderate' for 10/min or 'standard' for 20/min) + standard: createRateLimiter(RATE_LIMIT_MODERATE), // Keeping for backward compatibility - // Per-user: For authenticated endpoints (uses user ID as key) + // Lenient: 30 requests/minute - For lightweight reads + lenient: createRateLimiter(RATE_LIMIT_LENIENT), + + // Generous: 60 requests/minute - For high-frequency operations + generous: createRateLimiter(RATE_LIMIT_GENEROUS), + + // Per-user rate limiters (key by user ID instead of IP) + perUserStrict: createRateLimiter(RATE_LIMIT_PER_USER_STRICT), + perUserModerate: createRateLimiter(RATE_LIMIT_PER_USER_MODERATE), + perUserStandard: createRateLimiter(RATE_LIMIT_PER_USER_STANDARD), + perUserLenient: createRateLimiter(RATE_LIMIT_PER_USER_LENIENT), + + // Legacy per-user factory function (DEPRECATED: use perUserStrict, perUserModerate, etc.) perUser: (maxRequests: number = 20) => createRateLimiter({ - windowMs: 60000, + ...RATE_LIMIT_PER_USER_STANDARD, maxRequests, - keyGenerator: (req: Request) => { - // Extract user ID from Authorization header JWT - const authHeader = req.headers.get('Authorization'); - if (authHeader) { - try { - const token = authHeader.replace('Bearer ', ''); - const payload = JSON.parse(atob(token.split('.')[1])); - return `user:${payload.sub}`; - } catch { - // Fall back to IP if JWT parsing fails - return req.headers.get('x-forwarded-for')?.split(',')[0] || '0.0.0.0'; - } - } - return req.headers.get('x-forwarded-for')?.split(',')[0] || '0.0.0.0'; - } }), }; diff --git a/supabase/functions/process-selective-approval/index.ts b/supabase/functions/process-selective-approval/index.ts index f992cf40..c00366fc 100644 --- a/supabase/functions/process-selective-approval/index.ts +++ b/supabase/functions/process-selective-approval/index.ts @@ -558,5 +558,5 @@ const handler = async (req: Request) => { } }; -// Apply rate limiting: 10 requests per minute per IP (standard tier) -serve(withRateLimit(handler, rateLimiters.standard, corsHeaders)); +// Apply rate limiting: 10 requests per minute per IP (moderate tier for moderation actions) +serve(withRateLimit(handler, rateLimiters.moderate, corsHeaders)); diff --git a/supabase/functions/process-selective-rejection/index.ts b/supabase/functions/process-selective-rejection/index.ts index 050aeae3..46906279 100644 --- a/supabase/functions/process-selective-rejection/index.ts +++ b/supabase/functions/process-selective-rejection/index.ts @@ -514,5 +514,5 @@ const handler = async (req: Request) => { } }; -// Apply rate limiting: 10 requests per minute per IP (standard tier) -serve(withRateLimit(handler, rateLimiters.standard, corsHeaders)); +// Apply rate limiting: 10 requests per minute per IP (moderate tier for moderation actions) +serve(withRateLimit(handler, rateLimiters.moderate, corsHeaders));