diff --git a/src/components/admin/AdminUserDeletionDialog.tsx b/src/components/admin/AdminUserDeletionDialog.tsx new file mode 100644 index 00000000..446bbe17 --- /dev/null +++ b/src/components/admin/AdminUserDeletionDialog.tsx @@ -0,0 +1,368 @@ +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'; + +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('warning'); + const [confirmationText, setConfirmationText] = useState(''); + const [acknowledged, setAcknowledged] = useState(false); + const [error, setError] = useState(null); + const [factorId, setFactorId] = useState(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) { + console.error('Error deleting user:', err); + setError(err instanceof Error ? err.message : 'Failed to delete user'); + setStep('final_confirm'); + } + }; + + const isDeleteEnabled = confirmationText === 'DELETE' && acknowledged; + + return ( + + step === 'deleting' && e.preventDefault()} + > + {/* Step 1: Warning */} + {step === 'warning' && ( + <> + +
+ + Delete User Account +
+ + You are about to permanently delete this user's account + +
+ +
+ {/* User details */} +
+
+
+ Username: + {targetUser.username} +
+
+ Email: + {targetUser.email} +
+ {targetUser.displayName && ( +
+ Display Name: + {targetUser.displayName} +
+ )} +
+ Roles: + {targetUser.roles.join(', ') || 'None'} +
+
+
+ + {/* Critical warning */} + + + + This action is IMMEDIATE and PERMANENT. It cannot be undone. + + + + {/* What will be deleted */} +
+

Will be deleted:

+
    +
  • User profile and personal information
  • +
  • All reviews and ratings
  • +
  • Account preferences and settings
  • +
  • Authentication credentials
  • +
+
+ + {/* What will be preserved */} +
+

Will be preserved (as anonymous):

+
    +
  • Content submissions (parks, rides, etc.)
  • +
  • Uploaded photos
  • +
+
+ + {error && ( + + {error} + + )} + +
+ + +
+
+ + )} + + {/* Step 2: AAL2 Verification */} + {step === 'aal2_verification' && factorId && ( + <> + +
+ + MFA Verification Required +
+ + This is a critical action that requires additional verification + +
+ + {error && ( + + {error} + + )} + + { + setStep('warning'); + setError(null); + }} + /> + + )} + + {/* Step 3: Final Confirmation */} + {step === 'final_confirm' && ( + <> + +
+ + Final Confirmation +
+ + Type DELETE to confirm permanent deletion + +
+ +
+ + + +
Last chance to cancel!
+
+ Deleting {targetUser.username} will immediately and permanently remove their account. +
+
+
+ +
+ + setConfirmationText(e.target.value)} + placeholder="Type DELETE" + className="font-mono" + autoComplete="off" + /> +
+ +
+ setAcknowledged(checked as boolean)} + /> + +
+ + {error && ( + + {error} + + )} + +
+ + +
+
+ + )} + + {/* Step 4: Deleting */} + {step === 'deleting' && ( +
+
+
+
+
+

Deleting User...

+

+ This may take a moment. Please do not close this dialog. +

+
+
+ )} + + {/* Step 5: Complete */} + {step === 'complete' && ( +
+
+ +
+
+

User Deleted Successfully

+

+ {targetUser.username} has been permanently removed. +

+
+
+ )} +
+
+ ); +} diff --git a/src/components/moderation/ProfileManager.tsx b/src/components/moderation/ProfileManager.tsx index 641dd301..b4e89acc 100644 --- a/src/components/moderation/ProfileManager.tsx +++ b/src/components/moderation/ProfileManager.tsx @@ -1,8 +1,10 @@ import { useState, useEffect } from 'react'; -import { Search, Ban, Shield, UserCheck, UserX, AlertTriangle } from 'lucide-react'; +import { Search, Ban, Shield, UserCheck, UserX, AlertTriangle, Trash2 } from 'lucide-react'; import { supabase } from '@/integrations/supabase/client'; import { useAuth } from '@/hooks/useAuth'; import { useUserRole, UserRole } from '@/hooks/useUserRole'; +import { useSuperuserGuard } from '@/hooks/useSuperuserGuard'; +import { AdminUserDeletionDialog } from '@/components/admin/AdminUserDeletionDialog'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; @@ -34,6 +36,8 @@ export function ProfileManager() { const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'banned'>('all'); const [roleFilter, setRoleFilter] = useState<'all' | UserRole>('all'); const [actionLoading, setActionLoading] = useState(null); + const [deletionTarget, setDeletionTarget] = useState(null); + const superuserGuard = useSuperuserGuard(); useEffect(() => { if (!roleLoading && permissions?.can_view_all_profiles) { @@ -193,6 +197,20 @@ export function ProfileManager() { } }; + // Check if current superuser can delete a specific user + const canDeleteUser = (targetProfile: UserProfile) => { + if (!superuserGuard.isSuperuser) return false; + if (!superuserGuard.canPerformAction) return false; + + // Cannot delete other superusers + if (targetProfile.roles.includes('superuser')) return false; + + // Cannot delete self + if (targetProfile.user_id === user?.id) return false; + + return true; + }; + const canManageUser = (targetProfile: UserProfile) => { if (!permissions) return false; @@ -336,10 +354,10 @@ export function ProfileManager() { - {canManageUser(profile) && ( + {(canManageUser(profile) || canDeleteUser(profile)) && (
{/* Ban/Unban Button */} - {permissions.can_ban_any_user && ( + {canManageUser(profile) && permissions.can_ban_any_user && ( + )} + {/* Role Management */} - {(permissions.can_manage_moderator_roles || permissions.can_manage_admin_roles) && ( + {canManageUser(profile) && (permissions.can_manage_moderator_roles || permissions.can_manage_admin_roles) && (