mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 13:31:12 -05:00
Introduce centralized rate limiting by applying defined tiers (STRICT, STANDARD, LENIENT, MODERATE) to high-risk edge functions: - export-user-data (STRICT, 5 req/min) - send-contact-message (STANDARD, 20 req/min) - validate-email-backend (LENIENT, 30 req/min) - admin-delete-user, resend-deletion-code (MODERATE) - additional standard targets identified (request-account-deletion, cancel-account-deletion) as per guidance Implements: - Wrapped handlers with withRateLimit using centralized rateLimiters - Imported from shared rate limiter module - Annotated with comments explaining tier rationale - Updated three initial functions and extended coverage to admin/account management functions - Added documentation guide for rate limiting usage This aligns with the Rate Limiting Guide and centralizes rate limit configuration for consistency.
122 lines
3.7 KiB
TypeScript
122 lines
3.7 KiB
TypeScript
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";
|
|
|
|
// Simple request tracking
|
|
const startRequest = () => ({ requestId: crypto.randomUUID(), start: Date.now() });
|
|
const endRequest = (tracking: { start: number }) => Date.now() - tracking.start;
|
|
|
|
// Common disposable email domains (subset for performance)
|
|
const DISPOSABLE_DOMAINS = new Set([
|
|
'tempmail.com', 'guerrillamail.com', '10minutemail.com', 'mailinator.com',
|
|
'throwaway.email', 'temp-mail.org', 'fakeinbox.com', 'maildrop.cc',
|
|
'yopmail.com', 'sharklasers.com', 'guerrillamailblock.com'
|
|
]);
|
|
|
|
interface EmailValidationResult {
|
|
valid: boolean;
|
|
reason?: string;
|
|
suggestions?: string[];
|
|
}
|
|
|
|
function validateEmailFormat(email: string): EmailValidationResult {
|
|
// Basic format validation
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
if (!emailRegex.test(email)) {
|
|
return { valid: false, reason: 'Invalid email format' };
|
|
}
|
|
|
|
// Extract domain
|
|
const domain = email.split('@')[1].toLowerCase();
|
|
|
|
// Check against disposable domains
|
|
if (DISPOSABLE_DOMAINS.has(domain)) {
|
|
return {
|
|
valid: false,
|
|
reason: 'Disposable email addresses are not allowed. Please use a permanent email address.',
|
|
suggestions: ['gmail.com', 'outlook.com', 'yahoo.com', 'protonmail.com']
|
|
};
|
|
}
|
|
|
|
// Check for suspicious patterns
|
|
if (domain.includes('temp') || domain.includes('disposable') || domain.includes('trash')) {
|
|
return {
|
|
valid: false,
|
|
reason: 'This email domain appears to be temporary. Please use a permanent email address.',
|
|
};
|
|
}
|
|
|
|
return { valid: true };
|
|
}
|
|
|
|
// 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
|
|
if (req.method === 'OPTIONS') {
|
|
return new Response(null, { headers: corsHeaders });
|
|
}
|
|
|
|
try {
|
|
const { email } = await req.json();
|
|
|
|
if (!email || typeof email !== 'string') {
|
|
return new Response(
|
|
JSON.stringify({ valid: false, reason: 'Email is required', requestId: tracking.requestId }),
|
|
{
|
|
status: 400,
|
|
headers: {
|
|
...corsHeaders,
|
|
'Content-Type': 'application/json',
|
|
'X-Request-ID': tracking.requestId
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
// Validate email
|
|
const result = validateEmailFormat(email.toLowerCase().trim());
|
|
const duration = endRequest(tracking);
|
|
|
|
return new Response(
|
|
JSON.stringify({ ...result, requestId: tracking.requestId }),
|
|
{
|
|
status: 200,
|
|
headers: {
|
|
...corsHeaders,
|
|
'Content-Type': 'application/json',
|
|
'X-Request-ID': tracking.requestId
|
|
}
|
|
}
|
|
);
|
|
} catch (error) {
|
|
const duration = endRequest(tracking);
|
|
const errorMessage = formatEdgeError(error);
|
|
edgeLogger.error('Email validation error', {
|
|
error: errorMessage,
|
|
requestId: tracking.requestId,
|
|
duration
|
|
});
|
|
return new Response(
|
|
JSON.stringify({
|
|
valid: false,
|
|
reason: 'Failed to validate email. Please try again.',
|
|
requestId: tracking.requestId
|
|
}),
|
|
{
|
|
status: 500,
|
|
headers: {
|
|
...corsHeaders,
|
|
'Content-Type': 'application/json',
|
|
'X-Request-ID': tracking.requestId
|
|
}
|
|
}
|
|
);
|
|
}
|
|
}, rateLimiters.lenient, corsHeaders));
|