mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 08:11:13 -05:00
Refactor account deletion flow
This commit is contained in:
@@ -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
|
||||
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>
|
||||
<strong>2-Week Waiting Period:</strong> Your account will be deactivated immediately, but you'll have 14 days to cancel before permanent deletion. You'll receive a confirmation code via email.
|
||||
<strong>24-Hour Code + 2-Week Waiting Period:</strong> After confirming with the code (within 24 hours), your account will be deactivated immediately. You'll then have 14 days to cancel before permanent deletion.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
@@ -169,7 +168,7 @@ export function AccountDeletionDialog({ open, onOpenChange, userEmail, onDeletio
|
||||
{step === 'confirm' && (
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>
|
||||
Are you absolutely sure? This will deactivate your account immediately and schedule it for permanent deletion in 2 weeks.
|
||||
Are you absolutely sure? You'll receive a confirmation code via email. After confirming with the code, your account will be deactivated and scheduled for deletion in 2 weeks.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
@@ -179,15 +178,11 @@ export function AccountDeletionDialog({ open, onOpenChange, userEmail, onDeletio
|
||||
<Alert>
|
||||
<Info className="w-4 h-4" />
|
||||
<AlertDescription>
|
||||
Your account has been deactivated and will be deleted on{' '}
|
||||
<strong>{scheduledDate ? new Date(scheduledDate).toLocaleDateString() : '14 days from now'}</strong>.
|
||||
A 6-digit confirmation code has been sent to <strong>{userEmail}</strong>. Enter it below within 24 hours to confirm deletion. Your account will be deactivated and scheduled for deletion on{' '}
|
||||
<strong>{scheduledDate ? new Date(scheduledDate).toLocaleDateString() : '14 days from confirmation'}</strong>.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<p className="text-sm">
|
||||
A 6-digit confirmation code has been sent to <strong>{userEmail}</strong>.
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="confirmationCode">Confirmation Code</Label>
|
||||
@@ -222,12 +217,6 @@ export function AccountDeletionDialog({ open, onOpenChange, userEmail, onDeletio
|
||||
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : 'Resend Code'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Alert>
|
||||
<AlertDescription className="text-xs">
|
||||
Note: You cannot confirm deletion until the 2-week period has passed. You can cancel at any time during this period.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
)}
|
||||
</AlertDialogDescription>
|
||||
@@ -271,7 +260,7 @@ export function AccountDeletionDialog({ open, onOpenChange, userEmail, onDeletio
|
||||
disabled={loading || !codeReceived || confirmationCode.length !== 6}
|
||||
>
|
||||
{loading ? <Loader2 className="w-4 h-4 animate-spin mr-2" /> : null}
|
||||
Confirm Deletion
|
||||
Verify Code & Deactivate Account
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -59,10 +59,10 @@ export function DeletionStatusBanner({ scheduledDate, onCancelled }: DeletionSta
|
||||
return (
|
||||
<Alert variant="destructive" className="mb-6">
|
||||
<AlertTriangle className="w-4 h-4" />
|
||||
<AlertTitle>Account Deletion Scheduled</AlertTitle>
|
||||
<AlertTitle>Account Deactivated - Deletion Scheduled</AlertTitle>
|
||||
<AlertDescription className="space-y-2">
|
||||
<p>
|
||||
Your account is scheduled for permanent deletion on <strong>{formattedDate}</strong>.
|
||||
Your account is <strong>deactivated</strong> and scheduled for permanent deletion on <strong>{formattedDate}</strong>.
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
{daysRemaining > 0 ? (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) => {
|
||||
<p>We received a request to delete your account on ${new Date().toLocaleDateString()}.</p>
|
||||
|
||||
<h3>IMPORTANT INFORMATION:</h3>
|
||||
<p>Your account has been deactivated and will be permanently deleted on <strong>${scheduledDeletionAt.toLocaleDateString()}</strong> (14 days from now).</p>
|
||||
<p>You must enter the confirmation code within 24 hours. Once confirmed, your account will be deactivated and permanently deleted on <strong>${scheduledDeletionAt.toLocaleDateString()}</strong> (14 days from confirmation).</p>
|
||||
|
||||
<h4>What will be DELETED:</h4>
|
||||
<ul>
|
||||
@@ -125,9 +111,9 @@ serve(async (req) => {
|
||||
</ul>
|
||||
|
||||
<h3>CONFIRMATION CODE: <strong>${confirmationCode}</strong></h3>
|
||||
<p>To confirm deletion after the 14-day period, you'll need to enter this 6-digit code.</p>
|
||||
<p><strong>IMPORTANT:</strong> 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.</p>
|
||||
|
||||
<p><strong>Need to cancel?</strong> Log in and visit your account settings to reactivate your account at any time during the next 14 days.</p>
|
||||
<p><strong>Need to cancel?</strong> You can cancel at any time during the 14-day period after confirming.</p>
|
||||
`,
|
||||
};
|
||||
|
||||
|
||||
@@ -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 $$;
|
||||
Reference in New Issue
Block a user