From 391e6a07fd7b9e0938ef30f0a4571632f95738a8 Mon Sep 17 00:00:00 2001
From: "gpt-engineer-app[bot]"
<159125892+gpt-engineer-app[bot]@users.noreply.github.com>
Date: Sun, 12 Oct 2025 14:31:26 +0000
Subject: [PATCH] Refactor account deletion flow
---
.../settings/AccountDeletionDialog.tsx | 29 ++--
.../settings/DeletionStatusBanner.tsx | 4 +-
.../confirm-account-deletion/index.ts | 130 +++++-------------
.../process-scheduled-deletions/index.ts | 4 +-
.../request-account-deletion/index.ts | 20 +--
...9_a4478743-0039-4d33-9b11-30fbb8e5b528.sql | 11 ++
6 files changed, 59 insertions(+), 139 deletions(-)
create mode 100644 supabase/migrations/20251012142929_a4478743-0039-4d33-9b11-30fbb8e5b528.sql
diff --git a/src/components/settings/AccountDeletionDialog.tsx b/src/components/settings/AccountDeletionDialog.tsx
index 504b2c58..45cc344e 100644
--- a/src/components/settings/AccountDeletionDialog.tsx
+++ b/src/components/settings/AccountDeletionDialog.tsx
@@ -71,13 +71,12 @@ export function AccountDeletionDialog({ open, onOpenChange, userEmail, onDeletio
if (error) throw error;
toast({
- title: 'Account Deleted',
- description: 'Your account has been permanently deleted.',
+ title: 'Deletion Confirmed',
+ description: 'Your account has been deactivated and scheduled for permanent deletion.',
});
- // Sign out and redirect
- await supabase.auth.signOut();
- window.location.href = '/';
+ // Refresh the page to show the deletion banner
+ window.location.reload();
} catch (error: any) {
toast({
variant: 'destructive',
@@ -159,7 +158,7 @@ export function AccountDeletionDialog({ open, onOpenChange, userEmail, onDeletio
- A 6-digit confirmation code has been sent to {userEmail}. -
-- Your account is scheduled for permanent deletion on {formattedDate}. + Your account is deactivated and scheduled for permanent deletion on {formattedDate}.
{daysRemaining > 0 ? ( diff --git a/supabase/functions/confirm-account-deletion/index.ts b/supabase/functions/confirm-account-deletion/index.ts index 8f5f39c5..39f45e43 100644 --- a/supabase/functions/confirm-account-deletion/index.ts +++ b/supabase/functions/confirm-account-deletion/index.ts @@ -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: `
Your account has been permanently deleted on ${new Date().toLocaleDateString()}.
-Your profile and reviews have been removed, but your contributions to the database remain preserved.
-Thank you for being part of our community.
+Your deletion request has been confirmed. Your account is now deactivated and will be permanently deleted on ${new Date(deletionRequest.scheduled_deletion_at).toLocaleDateString()}.
+ +If you take no action, your account will be automatically deleted after the 14-day waiting period.
`, }), }); @@ -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, diff --git a/supabase/functions/process-scheduled-deletions/index.ts b/supabase/functions/process-scheduled-deletions/index.ts index be2b5a3a..f8d5f390 100644 --- a/supabase/functions/process-scheduled-deletions/index.ts +++ b/supabase/functions/process-scheduled-deletions/index.ts @@ -20,11 +20,11 @@ serve(async (req) => { console.log('Processing scheduled account deletions...'); - // Find pending deletion requests that are past their scheduled date + // Find confirmed deletion requests that are past their scheduled date const { data: pendingDeletions, error: fetchError } = await supabaseAdmin .from('account_deletion_requests') .select('*') - .eq('status', 'pending') + .eq('status', 'confirmed') .lt('scheduled_deletion_at', new Date().toISOString()); if (fetchError) { diff --git a/supabase/functions/request-account-deletion/index.ts b/supabase/functions/request-account-deletion/index.ts index 52ec11e8..83ddc97b 100644 --- a/supabase/functions/request-account-deletion/index.ts +++ b/supabase/functions/request-account-deletion/index.ts @@ -84,20 +84,6 @@ serve(async (req) => { throw requestError; } - // Deactivate profile - const { error: profileError } = await supabaseClient - .from('profiles') - .update({ - deactivated: true, - deactivated_at: new Date().toISOString(), - deactivation_reason: 'User requested account deletion', - }) - .eq('user_id', user.id); - - if (profileError) { - throw profileError; - } - // Send confirmation email const emailPayload = { to: user.email, @@ -108,7 +94,7 @@ serve(async (req) => {We received a request to delete your account on ${new Date().toLocaleDateString()}.
Your account has been deactivated and will be permanently deleted on ${scheduledDeletionAt.toLocaleDateString()} (14 days from now).
+You must enter the confirmation code within 24 hours. Once confirmed, your account will be deactivated and permanently deleted on ${scheduledDeletionAt.toLocaleDateString()} (14 days from confirmation).
To confirm deletion after the 14-day period, you'll need to enter this 6-digit code.
+IMPORTANT: You have 24 hours to enter this code to confirm the deletion. After entering the code, your account will be deactivated and you'll have 14 days to cancel before permanent deletion.
-Need to cancel? Log in and visit your account settings to reactivate your account at any time during the next 14 days.
+Need to cancel? You can cancel at any time during the 14-day period after confirming.
`, }; diff --git a/supabase/migrations/20251012142929_a4478743-0039-4d33-9b11-30fbb8e5b528.sql b/supabase/migrations/20251012142929_a4478743-0039-4d33-9b11-30fbb8e5b528.sql new file mode 100644 index 00000000..67fa4a41 --- /dev/null +++ b/supabase/migrations/20251012142929_a4478743-0039-4d33-9b11-30fbb8e5b528.sql @@ -0,0 +1,11 @@ +-- Add 'confirmed' status to account_deletion_status enum +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_enum + WHERE enumlabel = 'confirmed' + AND enumtypid = (SELECT oid FROM pg_type WHERE typname = 'account_deletion_status') + ) THEN + ALTER TYPE account_deletion_status ADD VALUE 'confirmed'; + END IF; +END $$; \ No newline at end of file