Refactor account deletion flow

This commit is contained in:
gpt-engineer-app[bot]
2025-10-12 14:31:26 +00:00
parent 8d814d43a1
commit 391e6a07fd
6 changed files with 59 additions and 139 deletions

View File

@@ -57,117 +57,43 @@ serve(async (req) => {
throw new Error('Invalid confirmation code');
}
// Check if 14 days have passed
const scheduledDate = new Date(deletionRequest.scheduled_deletion_at);
// Verify code was entered within 24 hours
const codeSentAt = new Date(deletionRequest.confirmation_code_sent_at);
const now = new Date();
const hoursSinceCodeSent = (now.getTime() - codeSentAt.getTime()) / (1000 * 60 * 60);
if (now < scheduledDate) {
const daysRemaining = Math.ceil((scheduledDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
throw new Error(`You must wait ${daysRemaining} more day(s) before confirming deletion`);
if (hoursSinceCodeSent > 24) {
throw new Error('Confirmation code has expired. Please request a new deletion code.');
}
// Use service role client for admin operations
const supabaseAdmin = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
);
console.log('Deactivating account and confirming deletion request...');
console.log('Starting deletion process...');
// Delete reviews (CASCADE will handle review_photos)
const { error: reviewsError } = await supabaseAdmin
.from('reviews')
.delete()
.eq('user_id', user.id);
if (reviewsError) {
console.error('Error deleting reviews:', reviewsError);
}
// Anonymize submissions and photos
const { error: anonymizeError } = await supabaseAdmin
.rpc('anonymize_user_submissions', { target_user_id: user.id });
if (anonymizeError) {
console.error('Error anonymizing submissions:', anonymizeError);
}
// Delete user roles
const { error: rolesError } = await supabaseAdmin
.from('user_roles')
.delete()
.eq('user_id', user.id);
if (rolesError) {
console.error('Error deleting user roles:', rolesError);
}
// Get profile to check for avatar before deletion
const { data: profile } = await supabaseAdmin
// Deactivate profile
const { error: profileError } = await supabaseClient
.from('profiles')
.select('avatar_image_id')
.eq('user_id', user.id)
.maybeSingle();
// Delete avatar from Cloudflare Images if it exists
if (profile?.avatar_image_id) {
const cloudflareAccountId = Deno.env.get('VITE_CLOUDFLARE_ACCOUNT_ID');
const cloudflareApiToken = Deno.env.get('CLOUDFLARE_API_TOKEN');
if (cloudflareAccountId && cloudflareApiToken) {
try {
console.log(`Deleting avatar image: ${profile.avatar_image_id}`);
const deleteResponse = await fetch(
`https://api.cloudflare.com/client/v4/accounts/${cloudflareAccountId}/images/v1/${profile.avatar_image_id}`,
{
method: 'DELETE',
headers: {
'Authorization': `Bearer ${cloudflareApiToken}`,
},
}
);
if (!deleteResponse.ok) {
console.error('Failed to delete avatar from Cloudflare:', await deleteResponse.text());
} else {
console.log('Avatar deleted from Cloudflare successfully');
}
} catch (avatarError) {
console.error('Error deleting avatar from Cloudflare:', avatarError);
}
}
}
// Delete profile
const { error: profileError } = await supabaseAdmin
.from('profiles')
.delete()
.update({
deactivated: true,
deactivated_at: new Date().toISOString(),
deactivation_reason: 'User confirmed account deletion request',
})
.eq('user_id', user.id);
if (profileError) {
console.error('Error deleting profile:', profileError);
console.error('Error deactivating profile:', profileError);
throw profileError;
}
// Update deletion request status
const { error: updateError } = await supabaseAdmin
// Update deletion request status to 'confirmed'
const { error: updateError } = await supabaseClient
.from('account_deletion_requests')
.update({
status: 'completed',
completed_at: new Date().toISOString(),
status: 'confirmed',
})
.eq('id', deletionRequest.id);
if (updateError) {
console.error('Error updating deletion request:', updateError);
}
// Delete auth user (this should cascade delete the deletion request)
const { error: authError } = await supabaseAdmin.auth.admin.deleteUser(user.id);
if (authError) {
console.error('Error deleting auth user:', authError);
throw authError;
throw updateError;
}
// Send confirmation email
@@ -185,12 +111,19 @@ serve(async (req) => {
body: JSON.stringify({
from: fromEmail,
to: user.email,
subject: 'Account Deletion Confirmed',
subject: 'Account Deletion Confirmed - 14 Days to Cancel',
html: `
<h2>Account Deletion Confirmed</h2>
<p>Your account has been permanently deleted on ${new Date().toLocaleDateString()}.</p>
<p>Your profile and reviews have been removed, but your contributions to the database remain preserved.</p>
<p>Thank you for being part of our community.</p>
<p>Your deletion request has been confirmed. Your account is now <strong>deactivated</strong> and will be permanently deleted on <strong>${new Date(deletionRequest.scheduled_deletion_at).toLocaleDateString()}</strong>.</p>
<h3>What happens now?</h3>
<ul>
<li>✓ Your account is deactivated immediately</li>
<li>✓ You have 14 days to cancel before permanent deletion</li>
<li>✓ To cancel, log in and visit your account settings</li>
</ul>
<p>If you take no action, your account will be automatically deleted after the 14-day waiting period.</p>
`,
}),
});
@@ -200,12 +133,13 @@ serve(async (req) => {
}
}
console.log('Account deletion completed successfully');
console.log('Account deactivated and deletion confirmed');
return new Response(
JSON.stringify({
success: true,
message: 'Account deleted successfully',
message: 'Deletion confirmed. Account deactivated and scheduled for permanent deletion.',
scheduled_deletion_at: deletionRequest.scheduled_deletion_at,
}),
{
status: 200,