mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 08:31:12 -05:00
373 lines
13 KiB
TypeScript
373 lines
13 KiB
TypeScript
import { useState } from 'react';
|
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Checkbox } from '@/components/ui/checkbox';
|
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
|
import { AlertTriangle, Trash2, Shield, CheckCircle2 } from 'lucide-react';
|
|
import { supabase } from '@/integrations/supabase/client';
|
|
import { useAuth } from '@/hooks/useAuth';
|
|
import { MFAChallenge } from '@/components/auth/MFAChallenge';
|
|
import { toast } from '@/hooks/use-toast';
|
|
import type { UserRole } from '@/hooks/useUserRole';
|
|
import { handleError } from '@/lib/errorHandler';
|
|
|
|
interface AdminUserDeletionDialogProps {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
targetUser: {
|
|
userId: string;
|
|
username: string;
|
|
email: string;
|
|
displayName?: string;
|
|
roles: UserRole[];
|
|
};
|
|
onDeletionComplete: () => void;
|
|
}
|
|
|
|
type DeletionStep = 'warning' | 'aal2_verification' | 'final_confirm' | 'deleting' | 'complete';
|
|
|
|
export function AdminUserDeletionDialog({
|
|
open,
|
|
onOpenChange,
|
|
targetUser,
|
|
onDeletionComplete
|
|
}: AdminUserDeletionDialogProps) {
|
|
const { session } = useAuth();
|
|
const [step, setStep] = useState<DeletionStep>('warning');
|
|
const [confirmationText, setConfirmationText] = useState('');
|
|
const [acknowledged, setAcknowledged] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [factorId, setFactorId] = useState<string | null>(null);
|
|
|
|
// Reset state when dialog opens/closes
|
|
const handleOpenChange = (isOpen: boolean) => {
|
|
if (!isOpen) {
|
|
setStep('warning');
|
|
setConfirmationText('');
|
|
setAcknowledged(false);
|
|
setError(null);
|
|
setFactorId(null);
|
|
}
|
|
onOpenChange(isOpen);
|
|
};
|
|
|
|
// Step 1: Show warning and proceed
|
|
const handleContinueFromWarning = async () => {
|
|
setError(null);
|
|
|
|
// Check if user needs AAL2 verification
|
|
const { data: factorsData } = await supabase.auth.mfa.listFactors();
|
|
const hasMFAEnrolled = factorsData?.totp?.some(f => f.status === 'verified') || false;
|
|
|
|
if (hasMFAEnrolled) {
|
|
// Check current AAL from JWT
|
|
if (session) {
|
|
const jwt = session.access_token;
|
|
const payload = JSON.parse(atob(jwt.split('.')[1]));
|
|
const currentAal = payload.aal || 'aal1';
|
|
|
|
if (currentAal !== 'aal2') {
|
|
// Need to verify MFA
|
|
const verifiedFactor = factorsData?.totp?.find(f => f.status === 'verified');
|
|
if (verifiedFactor) {
|
|
setFactorId(verifiedFactor.id);
|
|
setStep('aal2_verification');
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If no MFA or already at AAL2, go directly to final confirmation
|
|
setStep('final_confirm');
|
|
};
|
|
|
|
// Step 2: Handle successful AAL2 verification
|
|
const handleAAL2Success = () => {
|
|
setStep('final_confirm');
|
|
};
|
|
|
|
// Step 3: Perform deletion
|
|
const handleDelete = async () => {
|
|
setError(null);
|
|
setStep('deleting');
|
|
|
|
try {
|
|
const { data, error: functionError } = await supabase.functions.invoke('admin-delete-user', {
|
|
body: { targetUserId: targetUser.userId }
|
|
});
|
|
|
|
if (functionError) {
|
|
throw functionError;
|
|
}
|
|
|
|
if (!data.success) {
|
|
if (data.errorCode === 'aal2_required') {
|
|
// Session degraded during deletion, restart AAL2 flow
|
|
setError('Your session requires re-verification. Please verify again.');
|
|
const { data: factorsData } = await supabase.auth.mfa.listFactors();
|
|
const verifiedFactor = factorsData?.totp?.find(f => f.status === 'verified');
|
|
if (verifiedFactor) {
|
|
setFactorId(verifiedFactor.id);
|
|
setStep('aal2_verification');
|
|
} else {
|
|
setStep('warning');
|
|
}
|
|
return;
|
|
}
|
|
throw new Error(data.error || 'Failed to delete user');
|
|
}
|
|
|
|
// Success
|
|
setStep('complete');
|
|
|
|
setTimeout(() => {
|
|
toast({
|
|
title: 'User Deleted',
|
|
description: `${targetUser.username} has been permanently deleted.`,
|
|
});
|
|
onDeletionComplete();
|
|
handleOpenChange(false);
|
|
}, 2000);
|
|
|
|
} catch (err) {
|
|
handleError(err, {
|
|
action: 'Delete User',
|
|
metadata: { targetUserId: targetUser.userId }
|
|
});
|
|
setError(err instanceof Error ? err.message : 'Failed to delete user');
|
|
setStep('final_confirm');
|
|
}
|
|
};
|
|
|
|
const isDeleteEnabled = confirmationText === 'DELETE' && acknowledged;
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
|
<DialogContent
|
|
className="sm:max-w-lg"
|
|
onInteractOutside={(e) => step === 'deleting' && e.preventDefault()}
|
|
>
|
|
{/* Step 1: Warning */}
|
|
{step === 'warning' && (
|
|
<>
|
|
<DialogHeader>
|
|
<div className="flex items-center gap-2 justify-center mb-2">
|
|
<AlertTriangle className="h-6 w-6 text-destructive" />
|
|
<DialogTitle className="text-destructive">Delete User Account</DialogTitle>
|
|
</div>
|
|
<DialogDescription className="text-center">
|
|
You are about to permanently delete this user's account
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4">
|
|
{/* User details */}
|
|
<div className="p-4 border rounded-lg bg-muted/50">
|
|
<div className="space-y-2">
|
|
<div>
|
|
<span className="text-sm font-medium">Username:</span>
|
|
<span className="ml-2 text-sm">{targetUser.username}</span>
|
|
</div>
|
|
<div>
|
|
<span className="text-sm font-medium">Email:</span>
|
|
<span className="ml-2 text-sm">{targetUser.email}</span>
|
|
</div>
|
|
{targetUser.displayName && (
|
|
<div>
|
|
<span className="text-sm font-medium">Display Name:</span>
|
|
<span className="ml-2 text-sm">{targetUser.displayName}</span>
|
|
</div>
|
|
)}
|
|
<div>
|
|
<span className="text-sm font-medium">Roles:</span>
|
|
<span className="ml-2 text-sm">{targetUser.roles.join(', ') || 'None'}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Critical warning */}
|
|
<Alert variant="destructive">
|
|
<AlertTriangle className="h-4 w-4" />
|
|
<AlertDescription className="font-semibold">
|
|
This action is IMMEDIATE and PERMANENT. It cannot be undone.
|
|
</AlertDescription>
|
|
</Alert>
|
|
|
|
{/* What will be deleted */}
|
|
<div>
|
|
<h4 className="font-semibold text-sm mb-2 text-destructive">Will be deleted:</h4>
|
|
<ul className="text-sm space-y-1 list-disc list-inside text-muted-foreground">
|
|
<li>User profile and personal information</li>
|
|
<li>All reviews and ratings</li>
|
|
<li>Account preferences and settings</li>
|
|
<li>Authentication credentials</li>
|
|
</ul>
|
|
</div>
|
|
|
|
{/* What will be preserved */}
|
|
<div>
|
|
<h4 className="font-semibold text-sm mb-2">Will be preserved (as anonymous):</h4>
|
|
<ul className="text-sm space-y-1 list-disc list-inside text-muted-foreground">
|
|
<li>Content submissions (parks, rides, etc.)</li>
|
|
<li>Uploaded photos</li>
|
|
</ul>
|
|
</div>
|
|
|
|
{error && (
|
|
<Alert variant="destructive">
|
|
<AlertDescription>{error}</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
<div className="flex gap-2 justify-end pt-2">
|
|
<Button variant="outline" onClick={() => handleOpenChange(false)}>
|
|
Cancel
|
|
</Button>
|
|
<Button variant="destructive" onClick={handleContinueFromWarning}>
|
|
Continue
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* Step 2: AAL2 Verification */}
|
|
{step === 'aal2_verification' && factorId && (
|
|
<>
|
|
<DialogHeader>
|
|
<div className="flex items-center gap-2 justify-center mb-2">
|
|
<Shield className="h-6 w-6 text-primary" />
|
|
<DialogTitle>Multi-Factor Authentication Verification Required</DialogTitle>
|
|
</div>
|
|
<DialogDescription className="text-center">
|
|
This is a critical action that requires additional verification
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
{error && (
|
|
<Alert variant="destructive">
|
|
<AlertDescription>{error}</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
<MFAChallenge
|
|
factorId={factorId}
|
|
onSuccess={handleAAL2Success}
|
|
onCancel={() => {
|
|
setStep('warning');
|
|
setError(null);
|
|
}}
|
|
/>
|
|
</>
|
|
)}
|
|
|
|
{/* Step 3: Final Confirmation */}
|
|
{step === 'final_confirm' && (
|
|
<>
|
|
<DialogHeader>
|
|
<div className="flex items-center gap-2 justify-center mb-2">
|
|
<Trash2 className="h-6 w-6 text-destructive" />
|
|
<DialogTitle className="text-destructive">Final Confirmation</DialogTitle>
|
|
</div>
|
|
<DialogDescription className="text-center">
|
|
Type DELETE to confirm permanent deletion
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4">
|
|
<Alert variant="destructive">
|
|
<AlertTriangle className="h-4 w-4" />
|
|
<AlertDescription>
|
|
<div className="font-semibold mb-1">Last chance to cancel!</div>
|
|
<div className="text-sm">
|
|
Deleting {targetUser.username} will immediately and permanently remove their account.
|
|
</div>
|
|
</AlertDescription>
|
|
</Alert>
|
|
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium">
|
|
Type <span className="font-mono font-bold text-destructive">DELETE</span> to confirm:
|
|
</label>
|
|
<Input
|
|
value={confirmationText}
|
|
onChange={(e) => setConfirmationText(e.target.value)}
|
|
placeholder="Type DELETE"
|
|
className="font-mono"
|
|
autoComplete="off"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-start space-x-2">
|
|
<Checkbox
|
|
id="acknowledge"
|
|
checked={acknowledged}
|
|
onCheckedChange={(checked) => setAcknowledged(checked as boolean)}
|
|
/>
|
|
<label
|
|
htmlFor="acknowledge"
|
|
className="text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
>
|
|
I understand this action cannot be undone and will permanently delete this user's account
|
|
</label>
|
|
</div>
|
|
|
|
{error && (
|
|
<Alert variant="destructive">
|
|
<AlertDescription>{error}</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
<div className="flex gap-2 justify-end pt-2">
|
|
<Button variant="outline" onClick={() => handleOpenChange(false)}>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
variant="destructive"
|
|
onClick={handleDelete}
|
|
disabled={!isDeleteEnabled}
|
|
>
|
|
<Trash2 className="w-4 h-4 mr-2" />
|
|
Delete User Permanently
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* Step 4: Deleting */}
|
|
{step === 'deleting' && (
|
|
<div className="py-8 text-center space-y-4">
|
|
<div className="flex justify-center">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
|
|
</div>
|
|
<div>
|
|
<h3 className="font-semibold mb-1">Deleting User...</h3>
|
|
<p className="text-sm text-muted-foreground">
|
|
This may take a moment. Please do not close this dialog.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Step 5: Complete */}
|
|
{step === 'complete' && (
|
|
<div className="py-8 text-center space-y-4">
|
|
<div className="flex justify-center">
|
|
<CheckCircle2 className="h-12 w-12 text-green-500" />
|
|
</div>
|
|
<div>
|
|
<h3 className="font-semibold mb-1">User Deleted Successfully</h3>
|
|
<p className="text-sm text-muted-foreground">
|
|
{targetUser.username} has been permanently removed.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|