mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 06:51:12 -05:00
Compare commits
4 Commits
dc12ccbc0d
...
6da29e95a4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6da29e95a4 | ||
|
|
ed6ddbd04b | ||
|
|
bf3da6414a | ||
|
|
7cbd09b2ad |
277
supabase/functions/_shared/RATE_LIMITING_GUIDE.md
Normal file
277
supabase/functions/_shared/RATE_LIMITING_GUIDE.md
Normal file
@@ -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<Response> => {
|
||||
// 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
|
||||
119
supabase/functions/_shared/cors.ts
Normal file
119
supabase/functions/_shared/cors.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Centralized CORS configuration for all edge functions
|
||||
* Provides consistent header handling across the application
|
||||
*/
|
||||
|
||||
// Standard headers that should be allowed across all functions
|
||||
const STANDARD_HEADERS = [
|
||||
'authorization',
|
||||
'x-client-info',
|
||||
'apikey',
|
||||
'content-type',
|
||||
];
|
||||
|
||||
// Tracing headers for distributed tracing and request tracking
|
||||
const TRACING_HEADERS = [
|
||||
'traceparent',
|
||||
'x-request-id',
|
||||
];
|
||||
|
||||
// All headers combined
|
||||
const ALL_HEADERS = [...STANDARD_HEADERS, ...TRACING_HEADERS];
|
||||
|
||||
/**
|
||||
* Basic CORS headers - allows all origins
|
||||
* Use for most edge functions that need public access
|
||||
*/
|
||||
export const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': STANDARD_HEADERS.join(', '),
|
||||
};
|
||||
|
||||
/**
|
||||
* Extended CORS headers - includes tracing headers
|
||||
* Use for functions that participate in distributed tracing
|
||||
*/
|
||||
export const corsHeadersWithTracing = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': ALL_HEADERS.join(', '),
|
||||
};
|
||||
|
||||
/**
|
||||
* CORS headers with methods - for functions with multiple HTTP verbs
|
||||
*/
|
||||
export const corsHeadersWithMethods = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': ALL_HEADERS.join(', '),
|
||||
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
|
||||
};
|
||||
|
||||
/**
|
||||
* CORS headers with credentials - for authenticated requests requiring cookies
|
||||
*/
|
||||
export const corsHeadersWithCredentials = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': ALL_HEADERS.join(', '),
|
||||
'Access-Control-Allow-Credentials': 'true',
|
||||
};
|
||||
|
||||
/**
|
||||
* Environment-aware CORS configuration
|
||||
* Validates origin against allowlist (production) or localhost (development)
|
||||
*/
|
||||
export const getAllowedOrigin = (requestOrigin: string | null): string | null => {
|
||||
// If no origin header, it's not a CORS request (same-origin or server-to-server)
|
||||
if (!requestOrigin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const environment = Deno.env.get('ENVIRONMENT') || 'development';
|
||||
|
||||
// Production allowlist - configure via ALLOWED_ORIGINS environment variable
|
||||
const allowedOriginsEnv = Deno.env.get('ALLOWED_ORIGINS') || '';
|
||||
const allowedOrigins = allowedOriginsEnv.split(',').filter(origin => origin.trim());
|
||||
|
||||
// In development, only allow localhost and Replit domains
|
||||
if (environment === 'development') {
|
||||
if (
|
||||
requestOrigin.includes('localhost') ||
|
||||
requestOrigin.includes('127.0.0.1') ||
|
||||
requestOrigin.includes('.repl.co') ||
|
||||
requestOrigin.includes('.replit.dev')
|
||||
) {
|
||||
return requestOrigin;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// In production, only allow specific domains from environment variable
|
||||
if (allowedOrigins.includes(requestOrigin)) {
|
||||
return requestOrigin;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get CORS headers with validated origin
|
||||
* Use for functions requiring strict origin validation (e.g., upload-image)
|
||||
*/
|
||||
export const getCorsHeaders = (allowedOrigin: string | null): Record<string, string> => {
|
||||
if (!allowedOrigin) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
'Access-Control-Allow-Origin': allowedOrigin,
|
||||
'Access-Control-Allow-Headers': ALL_HEADERS.join(', '),
|
||||
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
|
||||
'Access-Control-Allow-Credentials': 'true',
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle OPTIONS preflight request
|
||||
* Returns a Response with appropriate CORS headers
|
||||
*/
|
||||
export const handleCorsPreFlight = (corsHeaders: Record<string, string>): Response => {
|
||||
return new Response(null, { headers: corsHeaders });
|
||||
};
|
||||
174
supabase/functions/_shared/rateLimitConfig.ts
Normal file
174
supabase/functions/_shared/rateLimitConfig.ts
Normal file
@@ -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 |
|
||||
*/
|
||||
@@ -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';
|
||||
}
|
||||
}),
|
||||
};
|
||||
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.57.4';
|
||||
import { corsHeaders } from '../_shared/cors.ts';
|
||||
import { rateLimiters, withRateLimit } from '../_shared/rateLimiter.ts';
|
||||
import { edgeLogger, startRequest, endRequest } from '../_shared/logger.ts';
|
||||
import { formatEdgeError } from '../_shared/errorFormatter.ts';
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
};
|
||||
|
||||
interface DeleteUserRequest {
|
||||
targetUserId: string;
|
||||
}
|
||||
@@ -17,7 +14,9 @@ interface DeleteUserResponse {
|
||||
errorCode?: 'aal2_required' | 'permission_denied' | 'invalid_request' | 'deletion_failed';
|
||||
}
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
// Apply moderate rate limiting (10 req/min) for admin user deletion
|
||||
// Prevents abuse of this sensitive administrative operation
|
||||
Deno.serve(withRateLimit(async (req) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response(null, { headers: corsHeaders });
|
||||
}
|
||||
@@ -560,4 +559,4 @@ Deno.serve(async (req) => {
|
||||
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
});
|
||||
}, rateLimiters.moderate, corsHeaders));
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
|
||||
import { corsHeaders } from '../_shared/cors.ts';
|
||||
import { edgeLogger, startRequest, endRequest } from '../_shared/logger.ts';
|
||||
import { formatEdgeError } from '../_shared/errorFormatter.ts';
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
};
|
||||
|
||||
serve(async (req) => {
|
||||
const tracking = startRequest();
|
||||
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.57.4';
|
||||
import { corsHeaders } from '../_shared/cors.ts';
|
||||
import { edgeLogger, startRequest, endRequest } from '../_shared/logger.ts';
|
||||
import { formatEdgeError } from '../_shared/errorFormatter.ts';
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
};
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
const tracking = startRequest();
|
||||
|
||||
|
||||
@@ -8,13 +8,9 @@
|
||||
*/
|
||||
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.57.4';
|
||||
import { corsHeaders } from '../_shared/cors.ts';
|
||||
import { edgeLogger, startRequest, endRequest } from '../_shared/logger.ts';
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
};
|
||||
|
||||
interface StatusRequest {
|
||||
idempotencyKey: string;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
|
||||
import { corsHeaders } from '../_shared/cors.ts';
|
||||
import { edgeLogger } from '../_shared/logger.ts';
|
||||
import { formatEdgeError } from '../_shared/errorFormatter.ts';
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
};
|
||||
|
||||
interface CleanupStats {
|
||||
item_edit_history_deleted: number;
|
||||
orphaned_records_deleted: number;
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
|
||||
import { corsHeaders } from '../_shared/cors.ts';
|
||||
import { edgeLogger, startRequest, endRequest } from '../_shared/logger.ts';
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
};
|
||||
|
||||
serve(async (req) => {
|
||||
const tracking = startRequest();
|
||||
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
|
||||
import { Novu } from "npm:@novu/api@1.6.0";
|
||||
import { corsHeaders } from '../_shared/cors.ts';
|
||||
import { edgeLogger } from '../_shared/logger.ts';
|
||||
import { formatEdgeError } from '../_shared/errorFormatter.ts';
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
};
|
||||
|
||||
// Simple request tracking
|
||||
const startRequest = () => ({ requestId: crypto.randomUUID(), start: Date.now() });
|
||||
const endRequest = (tracking: { start: number }) => Date.now() - tracking.start;
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
|
||||
import { corsHeadersWithTracing as corsHeaders } from '../_shared/cors.ts';
|
||||
import { edgeLogger, startRequest, endRequest } from "../_shared/logger.ts";
|
||||
import { formatEdgeError } from "../_shared/errorFormatter.ts";
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type, x-request-id',
|
||||
};
|
||||
|
||||
interface IPLocationResponse {
|
||||
country: string;
|
||||
countryCode: string;
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.57.4';
|
||||
import { corsHeaders } from '../_shared/cors.ts';
|
||||
import { rateLimiters, withRateLimit } from '../_shared/rateLimiter.ts';
|
||||
import { sanitizeError } from '../_shared/errorSanitizer.ts';
|
||||
import { edgeLogger, startRequest, endRequest } from '../_shared/logger.ts';
|
||||
import { formatEdgeError } from '../_shared/errorFormatter.ts';
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
};
|
||||
|
||||
interface ExportOptions {
|
||||
include_reviews: boolean;
|
||||
include_lists: boolean;
|
||||
@@ -17,7 +14,9 @@ interface ExportOptions {
|
||||
format: 'json';
|
||||
}
|
||||
|
||||
serve(async (req) => {
|
||||
// Apply strict rate limiting (5 req/min) for expensive data export operations
|
||||
// This prevents abuse and manages server load from large data exports
|
||||
serve(withRateLimit(async (req) => {
|
||||
const tracking = startRequest();
|
||||
|
||||
// Handle CORS preflight requests
|
||||
@@ -368,4 +367,4 @@ serve(async (req) => {
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}, rateLimiters.strict, corsHeaders));
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
|
||||
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4";
|
||||
import { Novu } from "npm:@novu/api@1.6.0";
|
||||
import { corsHeadersWithTracing as corsHeaders } from '../_shared/cors.ts';
|
||||
import { edgeLogger, startRequest, endRequest } from "../_shared/logger.ts";
|
||||
import { withEdgeRetry } from '../_shared/retryHelper.ts';
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type, x-request-id',
|
||||
};
|
||||
|
||||
const TOPICS = {
|
||||
MODERATION_SUBMISSIONS: 'moderation-submissions',
|
||||
MODERATION_REPORTS: 'moderation-reports',
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.57.4';
|
||||
import { corsHeaders } from '../_shared/cors.ts';
|
||||
import { edgeLogger, startRequest, endRequest } from '../_shared/logger.ts';
|
||||
import { createErrorResponse, sanitizeError } from '../_shared/errorSanitizer.ts';
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
};
|
||||
|
||||
interface MergeTicketsRequest {
|
||||
primaryTicketId: string;
|
||||
mergeTicketIds: string[];
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.57.4';
|
||||
import { corsHeaders } from '../_shared/cors.ts';
|
||||
import { edgeLogger, startRequest, endRequest } from '../_shared/logger.ts';
|
||||
import { formatEdgeError } from '../_shared/errorFormatter.ts';
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
};
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
const tracking = startRequest();
|
||||
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
|
||||
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4";
|
||||
import { Novu } from "npm:@novu/api@1.6.0";
|
||||
import { corsHeadersWithTracing as corsHeaders } from '../_shared/cors.ts';
|
||||
import { edgeLogger, startRequest, endRequest } from "../_shared/logger.ts";
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type, x-request-id',
|
||||
};
|
||||
|
||||
serve(async (req) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response(null, { headers: corsHeaders });
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
|
||||
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4";
|
||||
import { corsHeadersWithTracing as corsHeaders } from '../_shared/cors.ts';
|
||||
import { edgeLogger, startRequest, endRequest } from "../_shared/logger.ts";
|
||||
import { withEdgeRetry } from '../_shared/retryHelper.ts';
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type, x-request-id',
|
||||
};
|
||||
|
||||
interface NotificationPayload {
|
||||
reportId: string;
|
||||
reportType: string;
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
|
||||
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4";
|
||||
import { corsHeaders } from '../_shared/cors.ts';
|
||||
import { edgeLogger, startRequest, endRequest } from '../_shared/logger.ts';
|
||||
import { withEdgeRetry } from '../_shared/retryHelper.ts';
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
};
|
||||
|
||||
interface NotificationPayload {
|
||||
submission_id: string;
|
||||
submission_type: string;
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
|
||||
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4";
|
||||
import { corsHeadersWithTracing as corsHeaders } from '../_shared/cors.ts';
|
||||
import { edgeLogger, startRequest, endRequest } from "../_shared/logger.ts";
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type, x-request-id',
|
||||
};
|
||||
|
||||
interface AnnouncementPayload {
|
||||
title: string;
|
||||
message: string;
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
|
||||
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4";
|
||||
import { corsHeadersWithTracing as corsHeaders } from '../_shared/cors.ts';
|
||||
import { edgeLogger, startRequest, endRequest } from "../_shared/logger.ts";
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type, x-request-id',
|
||||
};
|
||||
|
||||
interface RequestBody {
|
||||
submission_id: string;
|
||||
user_id: string;
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
|
||||
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4";
|
||||
import { corsHeaders } from '../_shared/cors.ts';
|
||||
import { edgeLogger } from '../_shared/logger.ts';
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
};
|
||||
|
||||
// Simple request tracking
|
||||
const startRequest = () => ({ requestId: crypto.randomUUID(), start: Date.now() });
|
||||
const endRequest = (tracking: { start: number }) => Date.now() - tracking.start;
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.57.4';
|
||||
import { corsHeaders } from '../_shared/cors.ts';
|
||||
import { edgeLogger } from '../_shared/logger.ts';
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
};
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
// Handle CORS preflight
|
||||
if (req.method === 'OPTIONS') {
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
|
||||
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4";
|
||||
import { corsHeaders } from '../_shared/cors.ts';
|
||||
import { edgeLogger, startRequest, endRequest } from '../_shared/logger.ts';
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
};
|
||||
|
||||
const CLOUDFLARE_ACCOUNT_ID = Deno.env.get('CLOUDFLARE_ACCOUNT_ID');
|
||||
const CLOUDFLARE_API_TOKEN = Deno.env.get('CLOUDFLARE_IMAGES_API_TOKEN');
|
||||
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
|
||||
import { corsHeadersWithTracing as corsHeaders } from '../_shared/cors.ts';
|
||||
import { edgeLogger, startRequest, endRequest } from "../_shared/logger.ts";
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type, x-request-id',
|
||||
};
|
||||
|
||||
serve(async (req) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response(null, { headers: corsHeaders });
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
export const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.57.4';
|
||||
import { corsHeaders } from './cors.ts';
|
||||
import { corsHeadersWithTracing as corsHeaders } from '../_shared/cors.ts';
|
||||
import { rateLimiters, withRateLimit } from '../_shared/rateLimiter.ts';
|
||||
import {
|
||||
edgeLogger,
|
||||
@@ -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));
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
export const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.57.4';
|
||||
import { corsHeaders } from './cors.ts';
|
||||
import { corsHeadersWithTracing as corsHeaders } from '../_shared/cors.ts';
|
||||
import { rateLimiters, withRateLimit } from '../_shared/rateLimiter.ts';
|
||||
import {
|
||||
edgeLogger,
|
||||
@@ -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));
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
|
||||
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4";
|
||||
import { corsHeaders } from '../_shared/cors.ts';
|
||||
import { edgeLogger, startRequest, endRequest } from "../_shared/logger.ts";
|
||||
import { createErrorResponse } from "../_shared/errorSanitizer.ts";
|
||||
import { formatEdgeError } from "../_shared/errorFormatter.ts";
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
};
|
||||
|
||||
interface InboundEmailPayload {
|
||||
from: string;
|
||||
to: string;
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
|
||||
import { Novu } from "npm:@novu/api@1.6.0";
|
||||
import { corsHeaders } from '../_shared/cors.ts';
|
||||
import { edgeLogger, startRequest, endRequest } from "../_shared/logger.ts";
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type, x-request-id',
|
||||
};
|
||||
|
||||
serve(async (req) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response(null, { headers: corsHeaders });
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
|
||||
import { corsHeaders } from '../_shared/cors.ts';
|
||||
import { rateLimiters, withRateLimit } from '../_shared/rateLimiter.ts';
|
||||
import { edgeLogger, startRequest, endRequest } from '../_shared/logger.ts';
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
};
|
||||
|
||||
serve(async (req) => {
|
||||
// Apply standard rate limiting (20 req/min) for account deletion requests
|
||||
// Balances user needs with protection against automated abuse
|
||||
serve(withRateLimit(async (req) => {
|
||||
const tracking = startRequest();
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
@@ -222,4 +221,4 @@ serve(async (req) => {
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}, rateLimiters.standard, corsHeaders));
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
|
||||
import { corsHeaders } from '../_shared/cors.ts';
|
||||
import { rateLimiters, withRateLimit } from '../_shared/rateLimiter.ts';
|
||||
import { edgeLogger, startRequest, endRequest } from '../_shared/logger.ts';
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
};
|
||||
|
||||
serve(async (req) => {
|
||||
// Apply moderate rate limiting (10 req/min) to prevent deletion code spam
|
||||
// Protects against abuse while allowing legitimate resend requests
|
||||
serve(withRateLimit(async (req) => {
|
||||
const tracking = startRequest();
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
@@ -181,4 +180,4 @@ serve(async (req) => {
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}, rateLimiters.moderate, corsHeaders));
|
||||
|
||||
@@ -11,13 +11,9 @@
|
||||
*/
|
||||
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.57.4';
|
||||
import { corsHeaders } from '../_shared/cors.ts';
|
||||
import { edgeLogger } from '../_shared/logger.ts';
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
};
|
||||
|
||||
interface CleanupResult {
|
||||
idempotency_keys?: {
|
||||
deleted: number;
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.57.4';
|
||||
import { corsHeaders } from '../_shared/cors.ts';
|
||||
import { edgeLogger } from '../_shared/logger.ts';
|
||||
import { formatEdgeError } from '../_shared/errorFormatter.ts';
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
};
|
||||
|
||||
serve(async (req: Request) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response(null, { headers: corsHeaders });
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.57.4';
|
||||
import { corsHeaders } from '../_shared/cors.ts';
|
||||
import { edgeLogger, startRequest, endRequest } from '../_shared/logger.ts';
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
};
|
||||
|
||||
interface SeedOptions {
|
||||
preset: 'small' | 'medium' | 'large' | 'stress';
|
||||
entityTypes: string[];
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
|
||||
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4";
|
||||
import { corsHeaders } from '../_shared/cors.ts';
|
||||
import { edgeLogger, startRequest, endRequest } from "../_shared/logger.ts";
|
||||
import { createErrorResponse } from "../_shared/errorSanitizer.ts";
|
||||
import { formatEdgeError } from "../_shared/errorFormatter.ts";
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
};
|
||||
|
||||
interface AdminReplyRequest {
|
||||
submissionId: string;
|
||||
replyBody: string;
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
|
||||
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4";
|
||||
import { corsHeaders } from '../_shared/cors.ts';
|
||||
import { rateLimiters, withRateLimit } from '../_shared/rateLimiter.ts';
|
||||
import { edgeLogger } from "../_shared/logger.ts";
|
||||
import { createErrorResponse } from "../_shared/errorSanitizer.ts";
|
||||
import { formatEdgeError } from "../_shared/errorFormatter.ts";
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
};
|
||||
|
||||
interface ContactSubmission {
|
||||
name: string;
|
||||
email: string;
|
||||
@@ -342,4 +339,6 @@ The ThrillWiki Team`,
|
||||
}
|
||||
};
|
||||
|
||||
serve(handler);
|
||||
// Apply standard rate limiting (20 req/min) for contact form submissions
|
||||
// Balances legitimate user needs with spam prevention
|
||||
serve(withRateLimit(handler, rateLimiters.standard, corsHeaders));
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
|
||||
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4";
|
||||
import { corsHeaders } from '../_shared/cors.ts';
|
||||
import { edgeLogger, startRequest, endRequest } from '../_shared/logger.ts';
|
||||
import { withEdgeRetry } from '../_shared/retryHelper.ts';
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
};
|
||||
|
||||
interface EscalationRequest {
|
||||
submissionId: string;
|
||||
escalationReason: string;
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
|
||||
import { corsHeaders } from '../_shared/cors.ts';
|
||||
import { edgeLogger, startRequest, endRequest } from '../_shared/logger.ts';
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
};
|
||||
|
||||
interface EmailRequest {
|
||||
email: string;
|
||||
displayName?: string;
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
|
||||
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4";
|
||||
import { Novu } from "npm:@novu/api@1.6.0";
|
||||
import { corsHeaders } from '../_shared/cors.ts';
|
||||
import { edgeLogger, startRequest, endRequest } from '../_shared/logger.ts';
|
||||
import { withEdgeRetry } from '../_shared/retryHelper.ts';
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
};
|
||||
|
||||
const TOPICS = {
|
||||
MODERATION_SUBMISSIONS: 'moderation-submissions',
|
||||
MODERATION_REPORTS: 'moderation-reports',
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
|
||||
import { Novu } from "npm:@novu/api@1.6.0";
|
||||
import { corsHeaders } from '../_shared/cors.ts';
|
||||
import { edgeLogger, startRequest, endRequest } from "../_shared/logger.ts";
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type, x-request-id',
|
||||
};
|
||||
|
||||
serve(async (req) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response(null, { headers: corsHeaders });
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
|
||||
import { Novu } from "npm:@novu/api@1.6.0";
|
||||
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4";
|
||||
import { corsHeaders } from '../_shared/cors.ts';
|
||||
import { edgeLogger, startRequest, endRequest } from "../_shared/logger.ts";
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
};
|
||||
|
||||
serve(async (req) => {
|
||||
const tracking = startRequest('update-novu-preferences');
|
||||
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
|
||||
import { Novu } from "npm:@novu/api@1.6.0";
|
||||
import { corsHeaders } from '../_shared/cors.ts';
|
||||
import { edgeLogger, startRequest, endRequest } from "../_shared/logger.ts";
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type, x-request-id',
|
||||
};
|
||||
|
||||
serve(async (req) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response(null, { headers: corsHeaders });
|
||||
|
||||
@@ -1,62 +1,10 @@
|
||||
import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
|
||||
import { getAllowedOrigin, getCorsHeaders } from '../_shared/cors.ts'
|
||||
import { edgeLogger, startRequest, endRequest } from '../_shared/logger.ts'
|
||||
import { rateLimiters, withRateLimit } from '../_shared/rateLimiter.ts'
|
||||
import { formatEdgeError } from '../_shared/errorFormatter.ts'
|
||||
|
||||
// Environment-aware CORS configuration
|
||||
const getAllowedOrigin = (requestOrigin: string | null): string | null => {
|
||||
// If no origin header, it's not a CORS request (same-origin or server-to-server)
|
||||
if (!requestOrigin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const environment = Deno.env.get('ENVIRONMENT') || 'development';
|
||||
|
||||
// Production allowlist - configure via ALLOWED_ORIGINS environment variable
|
||||
// Format: comma-separated list of origins, e.g., "https://example.com,https://www.example.com"
|
||||
const allowedOriginsEnv = Deno.env.get('ALLOWED_ORIGINS') || '';
|
||||
const allowedOrigins = allowedOriginsEnv.split(',').filter(origin => origin.trim());
|
||||
|
||||
// In development, only allow localhost and Replit domains - nothing else
|
||||
if (environment === 'development') {
|
||||
if (
|
||||
requestOrigin.includes('localhost') ||
|
||||
requestOrigin.includes('127.0.0.1') ||
|
||||
requestOrigin.includes('.repl.co') ||
|
||||
requestOrigin.includes('.replit.dev')
|
||||
) {
|
||||
return requestOrigin;
|
||||
}
|
||||
// Origin not allowed in development - log and deny
|
||||
edgeLogger.warn('CORS origin not allowed in development mode', { origin: requestOrigin });
|
||||
return null;
|
||||
}
|
||||
|
||||
// In production, only allow specific domains from environment variable
|
||||
if (allowedOrigins.includes(requestOrigin)) {
|
||||
return requestOrigin;
|
||||
}
|
||||
|
||||
// Origin not allowed in production - log and deny
|
||||
edgeLogger.warn('CORS origin not allowed in production mode', { origin: requestOrigin });
|
||||
return null;
|
||||
};
|
||||
|
||||
const getCorsHeaders = (allowedOrigin: string | null): Record<string, string> => {
|
||||
// If no allowed origin, return empty headers (no CORS access)
|
||||
if (!allowedOrigin) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
'Access-Control-Allow-Origin': allowedOrigin,
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
|
||||
'Access-Control-Allow-Credentials': 'true',
|
||||
};
|
||||
};
|
||||
|
||||
// Helper to create authenticated Supabase client
|
||||
const createAuthenticatedSupabaseClient = (authHeader: string) => {
|
||||
const supabaseUrl = Deno.env.get('SUPABASE_URL')
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.39.3';
|
||||
import { corsHeaders } from '../_shared/cors.ts';
|
||||
import { rateLimiters, withRateLimit } from '../_shared/rateLimiter.ts';
|
||||
import { edgeLogger } from "../_shared/logger.ts";
|
||||
import { formatEdgeError } from "../_shared/errorFormatter.ts";
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
};
|
||||
|
||||
// Simple request tracking
|
||||
const startRequest = () => ({ requestId: crypto.randomUUID(), start: Date.now() });
|
||||
const endRequest = (tracking: { start: number }) => Date.now() - tracking.start;
|
||||
@@ -55,7 +52,9 @@ function validateEmailFormat(email: string): EmailValidationResult {
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
serve(async (req) => {
|
||||
// Apply lenient rate limiting (30 req/min) for email validation
|
||||
// Users may need to validate multiple times during signup/profile update
|
||||
serve(withRateLimit(async (req) => {
|
||||
const tracking = startRequest();
|
||||
|
||||
// Handle CORS preflight requests
|
||||
@@ -119,4 +118,4 @@ serve(async (req) => {
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}, rateLimiters.lenient, corsHeaders));
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
|
||||
import { corsHeaders } from '../_shared/cors.ts';
|
||||
import { startRequest, endRequest, edgeLogger } from "../_shared/logger.ts";
|
||||
import { formatEdgeError } from "../_shared/errorFormatter.ts";
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type, x-request-id',
|
||||
};
|
||||
|
||||
// Comprehensive list of disposable email domains
|
||||
const DISPOSABLE_DOMAINS = new Set([
|
||||
// Popular disposable email services
|
||||
|
||||
Reference in New Issue
Block a user