Files
thrilltrack-explorer/supabase/functions/validate-email-backend/index.ts
gpt-engineer-app[bot] 6da29e95a4 Add rate limiting to high-risk
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.
2025-11-10 21:39:37 +00:00

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));