mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 11:31:11 -05:00
Fix account deletion flow
This commit is contained in:
@@ -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 }
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
`,
|
`,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -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>
|
||||||
`,
|
`,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
`,
|
`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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));
|
||||||
Reference in New Issue
Block a user