Files
gpt-engineer-app[bot] 8ee548fd27 Migrate Phase 1 user-facing functions
Refactor export-user-data, notify-user-submission-status, and resend-deletion-code to use createEdgeFunction wrapper. Remove manual CORS, auth, rate limiting boilerplate; adopt standardized EdgeFunctionContext (supabase, user, span, requestId), and integrate built-in tracing, rate limiting, and logging through the wrapper. Update handlers to rely on wrapper context and ensure consistent error handling and observability.
2025-11-11 20:35:45 +00:00

115 lines
3.8 KiB
TypeScript

import { serve } from 'https://deno.land/std@0.190.0/http/server.ts';
import { createEdgeFunction, type EdgeFunctionContext } from '../_shared/edgeFunctionWrapper.ts';
import { corsHeaders } from '../_shared/cors.ts';
import { addSpanEvent } from '../_shared/logger.ts';
const handler = async (req: Request, { supabase, user, span, requestId }: EdgeFunctionContext) => {
addSpanEvent(span, 'resending_deletion_code', { userId: user.id });
// Find pending deletion request
const { data: deletionRequest, error: requestError } = await supabase
.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 - ~20 minutes between resends)
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) {
addSpanEvent(span, 'resend_rate_limited', {
hoursSinceLastSend,
minRequired: 0.33
});
throw new Error('Please wait at least 20 minutes between resend requests');
}
// Generate new confirmation code
const { data: codeData, error: codeError } = await supabase
.rpc('generate_deletion_confirmation_code');
if (codeError) {
throw codeError;
}
const confirmationCode = codeData as string;
// Update deletion request with new code
const { error: updateError } = await supabase
.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);
addSpanEvent(span, 'new_code_generated', {
scheduledDeletionDate: scheduledDate.toISOString()
});
// 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>
`,
}),
});
addSpanEvent(span, 'email_sent', { email: user.email });
} catch (emailError) {
addSpanEvent(span, 'email_send_failed', {
error: emailError instanceof Error ? emailError.message : String(emailError)
});
}
}
addSpanEvent(span, 'resend_completed', { userId: user.id });
return {
success: true,
message: 'New confirmation code sent successfully',
};
};
serve(createEdgeFunction({
name: 'resend-deletion-code',
requireAuth: true,
corsHeaders,
enableTracing: true,
logRequests: true,
rateLimitTier: 'moderate', // 10 requests per minute
}, handler));