mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 15:31:13 -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.
184 lines
5.7 KiB
TypeScript
184 lines
5.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';
|
|
import { corsHeaders } from '../_shared/cors.ts';
|
|
import { rateLimiters, withRateLimit } from '../_shared/rateLimiter.ts';
|
|
import { edgeLogger, startRequest, endRequest } from '../_shared/logger.ts';
|
|
|
|
// 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') {
|
|
return new Response(null, {
|
|
headers: {
|
|
...corsHeaders,
|
|
'X-Request-ID': tracking.requestId
|
|
}
|
|
});
|
|
}
|
|
|
|
try {
|
|
const supabaseClient = createClient(
|
|
Deno.env.get('SUPABASE_URL') ?? '',
|
|
Deno.env.get('SUPABASE_ANON_KEY') ?? '',
|
|
{
|
|
global: {
|
|
headers: { Authorization: req.headers.get('Authorization')! },
|
|
},
|
|
}
|
|
);
|
|
|
|
// Get authenticated user
|
|
const {
|
|
data: { user },
|
|
error: userError,
|
|
} = await supabaseClient.auth.getUser();
|
|
|
|
if (userError || !user) {
|
|
const duration = endRequest(tracking);
|
|
edgeLogger.error('Authentication failed', {
|
|
action: 'resend_deletion_code',
|
|
requestId: tracking.requestId,
|
|
duration
|
|
});
|
|
throw new Error('Unauthorized');
|
|
}
|
|
|
|
edgeLogger.info('Resending deletion code for user', {
|
|
action: 'resend_deletion_code',
|
|
requestId: tracking.requestId,
|
|
userId: user.id
|
|
});
|
|
|
|
// Find pending deletion request
|
|
const { data: deletionRequest, error: requestError } = await supabaseClient
|
|
.from('account_deletion_requests')
|
|
.select('*')
|
|
.eq('user_id', user.id)
|
|
.eq('status', 'pending')
|
|
.maybeSingle();
|
|
|
|
if (requestError || !deletionRequest) {
|
|
throw new Error('No pending deletion request found');
|
|
}
|
|
|
|
// Check rate limiting (max 3 resends per hour)
|
|
const lastSent = new Date(deletionRequest.confirmation_code_sent_at);
|
|
const now = new Date();
|
|
const hoursSinceLastSend = (now.getTime() - lastSent.getTime()) / (1000 * 60 * 60);
|
|
|
|
if (hoursSinceLastSend < 0.33) { // ~20 minutes between resends
|
|
throw new Error('Please wait at least 20 minutes between resend requests');
|
|
}
|
|
|
|
// Generate new confirmation code
|
|
const { data: codeData, error: codeError } = await supabaseClient
|
|
.rpc('generate_deletion_confirmation_code');
|
|
|
|
if (codeError) {
|
|
throw codeError;
|
|
}
|
|
|
|
const confirmationCode = codeData as string;
|
|
|
|
// Update deletion request with new code
|
|
const { error: updateError } = await supabaseClient
|
|
.from('account_deletion_requests')
|
|
.update({
|
|
confirmation_code: confirmationCode,
|
|
confirmation_code_sent_at: now.toISOString(),
|
|
})
|
|
.eq('id', deletionRequest.id);
|
|
|
|
if (updateError) {
|
|
throw updateError;
|
|
}
|
|
|
|
const scheduledDate = new Date(deletionRequest.scheduled_deletion_at);
|
|
|
|
// Send email with new code
|
|
const forwardEmailKey = Deno.env.get('FORWARDEMAIL_API_KEY');
|
|
const fromEmail = Deno.env.get('FROM_EMAIL_ADDRESS') || 'noreply@thrillwiki.com';
|
|
|
|
if (forwardEmailKey) {
|
|
try {
|
|
await fetch('https://api.forwardemail.net/v1/emails', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Basic ${btoa(forwardEmailKey + ':')}`,
|
|
},
|
|
body: JSON.stringify({
|
|
from: fromEmail,
|
|
to: user.email,
|
|
subject: 'Account Deletion - New Confirmation Code',
|
|
html: `
|
|
<h2>New Confirmation Code</h2>
|
|
<p>You requested a new confirmation code for your account deletion.</p>
|
|
<p>Your account will be permanently deleted on <strong>${scheduledDate.toLocaleDateString()}</strong>.</p>
|
|
|
|
<h3>CONFIRMATION CODE: <strong>${confirmationCode}</strong></h3>
|
|
<p>To confirm deletion after the waiting period, you'll need to enter this 6-digit code.</p>
|
|
|
|
<p><strong>Need to cancel?</strong> Log in and visit your account settings to reactivate your account.</p>
|
|
`,
|
|
}),
|
|
});
|
|
edgeLogger.info('New confirmation code email sent', { requestId: tracking.requestId });
|
|
} catch (emailError) {
|
|
edgeLogger.error('Failed to send email', {
|
|
requestId: tracking.requestId,
|
|
error: emailError.message
|
|
});
|
|
}
|
|
}
|
|
|
|
const duration = endRequest(tracking);
|
|
edgeLogger.info('New confirmation code sent successfully', {
|
|
action: 'resend_deletion_code',
|
|
requestId: tracking.requestId,
|
|
userId: user.id,
|
|
duration
|
|
});
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
success: true,
|
|
message: 'New confirmation code sent successfully',
|
|
requestId: tracking.requestId,
|
|
}),
|
|
{
|
|
status: 200,
|
|
headers: {
|
|
...corsHeaders,
|
|
'Content-Type': 'application/json',
|
|
'X-Request-ID': tracking.requestId
|
|
},
|
|
}
|
|
);
|
|
} catch (error) {
|
|
const duration = endRequest(tracking);
|
|
edgeLogger.error('Error resending code', {
|
|
action: 'resend_deletion_code',
|
|
requestId: tracking.requestId,
|
|
duration,
|
|
error: error.message
|
|
});
|
|
return new Response(
|
|
JSON.stringify({
|
|
error: error.message,
|
|
requestId: tracking.requestId
|
|
}),
|
|
{
|
|
status: 400,
|
|
headers: {
|
|
...corsHeaders,
|
|
'Content-Type': 'application/json',
|
|
'X-Request-ID': tracking.requestId
|
|
},
|
|
}
|
|
);
|
|
}
|
|
}, rateLimiters.moderate, corsHeaders));
|