Fix account deletion flow

This commit is contained in:
gpt-engineer-app[bot]
2025-10-29 22:46:49 +00:00
parent 2918f9d280
commit a2cb037410
8 changed files with 143 additions and 28 deletions

View File

@@ -48,7 +48,7 @@ export function AccountDeletionDialog({ open, onOpenChange, userEmail, onDeletio
{ action: 'Request account deletion' } { action: 'Request account deletion' }
); );
sessionStorage.setItem('mfa_step_up_required', 'true'); sessionStorage.setItem('mfa_step_up_required', 'true');
sessionStorage.setItem('mfa_intended_path', '/settings?tab=privacy'); sessionStorage.setItem('mfa_intended_path', '/settings');
window.location.href = '/auth'; window.location.href = '/auth';
return; return;
} }
@@ -66,6 +66,10 @@ export function AccountDeletionDialog({ open, onOpenChange, userEmail, onDeletio
if (error) throw error; if (error) throw error;
// Clear MFA session storage
sessionStorage.removeItem('mfa_step_up_required');
sessionStorage.removeItem('mfa_intended_path');
dispatch({ dispatch({
type: 'REQUEST_DELETION', type: 'REQUEST_DELETION',
payload: { scheduledDate: data.scheduled_deletion_at } payload: { scheduledDate: data.scheduled_deletion_at }

View File

@@ -84,7 +84,7 @@ export function AccountProfileTab() {
isValid: usernameValidation.isAvailable !== false isValid: usernameValidation.isAvailable !== false
}); });
// Check for existing deletion request on mount // Check for existing deletion request on mount (both pending and confirmed)
useEffect(() => { useEffect(() => {
const checkDeletionRequest = async () => { const checkDeletionRequest = async () => {
if (!user?.id) return; if (!user?.id) return;
@@ -93,7 +93,7 @@ export function AccountProfileTab() {
.from('account_deletion_requests') .from('account_deletion_requests')
.select('*') .select('*')
.eq('user_id', user.id) .eq('user_id', user.id)
.eq('status', 'pending') .in('status', ['pending', 'confirmed'])
.maybeSingle(); .maybeSingle();
if (!error && data) { if (!error && data) {
@@ -210,12 +210,12 @@ export function AccountProfileTab() {
}; };
const handleDeletionRequested = async () => { const handleDeletionRequested = async () => {
// Refresh deletion request data // Refresh deletion request data (check for both pending and confirmed)
const { data, error } = await supabase const { data, error } = await supabase
.from('account_deletion_requests') .from('account_deletion_requests')
.select('*') .select('*')
.eq('user_id', user!.id) .eq('user_id', user!.id)
.eq('status', 'pending') .in('status', ['pending', 'confirmed'])
.maybeSingle(); .maybeSingle();
if (!error && data) { if (!error && data) {
@@ -235,6 +235,7 @@ export function AccountProfileTab() {
{deletionRequest && ( {deletionRequest && (
<DeletionStatusBanner <DeletionStatusBanner
scheduledDate={deletionRequest.scheduled_deletion_at} scheduledDate={deletionRequest.scheduled_deletion_at}
status={deletionRequest.status as 'pending' | 'confirmed'}
onCancelled={handleDeletionCancelled} onCancelled={handleDeletionCancelled}
/> />
)} )}

View File

@@ -9,10 +9,11 @@ import { getErrorMessage } from '@/lib/errorHandler';
interface DeletionStatusBannerProps { interface DeletionStatusBannerProps {
scheduledDate: string; scheduledDate: string;
status: 'pending' | 'confirmed';
onCancelled: () => void; onCancelled: () => void;
} }
export function DeletionStatusBanner({ scheduledDate, onCancelled }: DeletionStatusBannerProps) { export function DeletionStatusBanner({ scheduledDate, status, onCancelled }: DeletionStatusBannerProps) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { toast } = useToast(); const { toast } = useToast();
@@ -64,10 +65,26 @@ export function DeletionStatusBanner({ scheduledDate, onCancelled }: DeletionSta
return ( return (
<Alert variant="destructive" className="mb-6"> <Alert variant="destructive" className="mb-6">
<AlertTriangle className="w-4 h-4" /> <AlertTriangle className="w-4 h-4" />
<AlertTitle>Account Deactivated - Deletion Scheduled</AlertTitle> <AlertTitle>
{status === 'pending' ? 'Deletion Requested' : 'Account Deactivated - Deletion Scheduled'}
</AlertTitle>
<AlertDescription className="space-y-2"> <AlertDescription className="space-y-2">
{status === 'pending' ? (
<>
<p> <p>
Your account is <strong>deactivated</strong> and scheduled for permanent deletion on <strong>{formattedDate}</strong>. You have requested account deletion. Please check your email for a confirmation code.
You must enter the code within 24 hours to proceed.
</p>
<p className="text-sm text-muted-foreground">
After confirming with the code, your account will be deactivated and scheduled for deletion on{' '}
<strong>{formattedDate}</strong>.
</p>
</>
) : (
<>
<p>
Your account is <strong>deactivated</strong> and scheduled for permanent deletion on{' '}
<strong>{formattedDate}</strong>.
</p> </p>
<p className="text-sm"> <p className="text-sm">
{daysRemaining > 0 ? ( {daysRemaining > 0 ? (
@@ -75,9 +92,11 @@ export function DeletionStatusBanner({ scheduledDate, onCancelled }: DeletionSta
<strong>{daysRemaining}</strong> day{daysRemaining !== 1 ? 's' : ''} remaining to cancel. <strong>{daysRemaining}</strong> day{daysRemaining !== 1 ? 's' : ''} remaining to cancel.
</> </>
) : ( ) : (
'You can now confirm deletion with your confirmation code.' 'Your account will be deleted within 24 hours.'
)} )}
</p> </p>
</>
)}
<div className="flex gap-2 mt-3"> <div className="flex gap-2 mt-3">
<Button <Button
variant="outline" variant="outline"

View File

@@ -3976,6 +3976,10 @@ export type Database = {
Args: { item_id: string } Args: { item_id: string }
Returns: boolean Returns: boolean
} }
can_manage_deletion: {
Args: { _deletion_id: string; _user_id: string }
Returns: boolean
}
can_manage_user: { can_manage_user: {
Args: { _manager_id: string; _target_user_id: string } Args: { _manager_id: string; _target_user_id: string }
Returns: boolean Returns: boolean

View File

@@ -39,16 +39,27 @@ serve(async (req) => {
edgeLogger.info('Cancelling deletion request', { action: 'cancel_deletion', userId: user.id, requestId: tracking.requestId }); edgeLogger.info('Cancelling deletion request', { action: 'cancel_deletion', userId: user.id, requestId: tracking.requestId });
// Find pending deletion request // Find pending or confirmed deletion request
const { data: deletionRequest, error: requestError } = await supabaseClient const { data: deletionRequest, error: requestError } = await supabaseClient
.from('account_deletion_requests') .from('account_deletion_requests')
.select('*') .select('*')
.eq('user_id', user.id) .eq('user_id', user.id)
.eq('status', 'pending') .in('status', ['pending', 'confirmed'])
.maybeSingle(); .maybeSingle();
if (requestError || !deletionRequest) { if (requestError || !deletionRequest) {
throw new Error('No pending deletion request found'); throw new Error('No active deletion request found');
}
// Validate that deletion hasn't already been processed
if (deletionRequest.status === 'completed') {
throw new Error('This deletion request has already been completed');
}
// Validate scheduled deletion hasn't passed
const scheduledDate = new Date(deletionRequest.scheduled_deletion_at);
if (scheduledDate < new Date()) {
throw new Error('Cannot cancel - deletion has already been processed');
} }
// Cancel deletion request // Cancel deletion request
@@ -99,6 +110,7 @@ serve(async (req) => {
<h2>Account Deletion Cancelled</h2> <h2>Account Deletion Cancelled</h2>
<p>Your account deletion request has been cancelled on ${new Date().toLocaleDateString()}.</p> <p>Your account deletion request has been cancelled on ${new Date().toLocaleDateString()}.</p>
<p>Your account has been reactivated and you can continue using all features.</p> <p>Your account has been reactivated and you can continue using all features.</p>
<p>If you did not cancel this request, please contact support immediately.</p>
<p>Welcome back!</p> <p>Welcome back!</p>
`, `,
}), }),

View File

@@ -70,6 +70,18 @@ serve(async (req) => {
throw new Error('No pending deletion request found'); throw new Error('No pending deletion request found');
} }
// Check if there's already a confirmed request
const { data: confirmedRequest } = await supabaseClient
.from('account_deletion_requests')
.select('*')
.eq('user_id', user.id)
.eq('status', 'confirmed')
.maybeSingle();
if (confirmedRequest) {
throw new Error('You already have a confirmed deletion request. Your account is scheduled for deletion.');
}
// Verify confirmation code // Verify confirmation code
if (deletionRequest.confirmation_code !== confirmation_code) { if (deletionRequest.confirmation_code !== confirmation_code) {
throw new Error('Invalid confirmation code'); throw new Error('Invalid confirmation code');
@@ -141,7 +153,10 @@ serve(async (req) => {
<li>✓ To cancel, log in and visit your account settings</li> <li>✓ To cancel, log in and visit your account settings</li>
</ul> </ul>
<p>If you take no action, your account will be automatically deleted after the 14-day waiting period.</p> <h3>Changed Your Mind?</h3>
<p>You can cancel at any time during the 14-day waiting period by logging in and clicking "Cancel Deletion" in your account settings.</p>
<p><strong>If you take no action</strong>, your account will be automatically deleted after the 14-day waiting period.</p>
`, `,
}), }),
}); });

View File

@@ -52,23 +52,31 @@ serve(async (req) => {
userId: user.id userId: user.id
}); });
// Check for existing pending deletion request // Check for existing active deletion request (pending or confirmed)
const { data: existingRequest } = await supabaseClient const { data: existingRequest } = await supabaseClient
.from('account_deletion_requests') .from('account_deletion_requests')
.select('*') .select('*')
.eq('user_id', user.id) .eq('user_id', user.id)
.eq('status', 'pending') .in('status', ['pending', 'confirmed'])
.maybeSingle(); .maybeSingle();
if (existingRequest) { if (existingRequest) {
const errorMsg = existingRequest.status === 'confirmed'
? 'Your account is already scheduled for deletion. You can cancel it from your account settings.'
: 'You already have a pending deletion request. Check your email for the confirmation code.';
return new Response( return new Response(
JSON.stringify({ JSON.stringify({
error: 'You already have a pending deletion request', error: errorMsg,
request: existingRequest, request: existingRequest,
}), }),
{ {
status: 400, status: 400,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }, headers: {
...corsHeaders,
'Content-Type': 'application/json',
'X-Request-ID': tracking.requestId
},
} }
); );
} }
@@ -131,7 +139,9 @@ serve(async (req) => {
<h3>CONFIRMATION CODE: <strong>${confirmationCode}</strong></h3> <h3>CONFIRMATION CODE: <strong>${confirmationCode}</strong></h3>
<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>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> You can cancel at any time during the 14-day period after confirming.</p> <p><strong>Need to cancel?</strong> You can cancel at any time - before OR after confirming - during the 14-day period.</p>
<p><strong>Changed your mind?</strong> Simply log in to your account settings and click "Cancel Deletion".</p>
`, `,
}; };

View File

@@ -0,0 +1,50 @@
-- Fix account deletion flow: Critical schema improvements
-- 1. Drop old unique constraint and create new one that covers both pending and confirmed
DROP INDEX IF EXISTS unique_active_deletion_per_user;
CREATE UNIQUE INDEX unique_active_deletion_per_user
ON account_deletion_requests(user_id)
WHERE status IN ('pending', 'confirmed');
-- 2. Add indexes for efficient deletion processing
CREATE INDEX IF NOT EXISTS idx_account_deletions_confirmed
ON account_deletion_requests(scheduled_deletion_at)
WHERE status = 'confirmed';
CREATE INDEX IF NOT EXISTS idx_profiles_pending_deletion
ON profiles(user_id, deactivated)
WHERE deactivated = true;
-- 3. Create security definer function for deletion checks (prevents infinite recursion in RLS)
CREATE OR REPLACE FUNCTION can_manage_deletion(
_user_id uuid,
_deletion_id uuid
)
RETURNS boolean
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = public
AS $$
SELECT EXISTS (
SELECT 1 FROM account_deletion_requests
WHERE id = _deletion_id
AND user_id = _user_id
AND status IN ('pending', 'confirmed')
)
$$;
-- 4. Update RLS policies to use security definer function
DROP POLICY IF EXISTS "Users can update their own deletion requests" ON account_deletion_requests;
DROP POLICY IF EXISTS "Users can view their own deletion requests" ON account_deletion_requests;
CREATE POLICY "Users can view their own deletion requests"
ON account_deletion_requests FOR SELECT
TO authenticated
USING (user_id = auth.uid());
CREATE POLICY "Users can update their own deletion requests"
ON account_deletion_requests FOR UPDATE
TO authenticated
USING (can_manage_deletion(auth.uid(), id))
WITH CHECK (can_manage_deletion(auth.uid(), id));