mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-24 03:51:12 -05:00
feat: Implement secure MFA removal
This commit is contained in:
245
src/components/auth/MFARemovalDialog.tsx
Normal file
245
src/components/auth/MFARemovalDialog.tsx
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { supabase } from '@/integrations/supabase/client';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from '@/components/ui/alert-dialog';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
|
import { Shield, AlertTriangle } from 'lucide-react';
|
||||||
|
|
||||||
|
interface MFARemovalDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
factorId: string;
|
||||||
|
onSuccess: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MFARemovalDialog({ open, onOpenChange, factorId, onSuccess }: MFARemovalDialogProps) {
|
||||||
|
const [step, setStep] = useState<'password' | 'totp' | 'confirm'>('password');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [totpCode, setTotpCode] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setStep('password');
|
||||||
|
setPassword('');
|
||||||
|
setTotpCode('');
|
||||||
|
setLoading(false);
|
||||||
|
onOpenChange(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePasswordVerification = async () => {
|
||||||
|
if (!password.trim()) {
|
||||||
|
toast.error('Please enter your password');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
// Get current user email
|
||||||
|
const { data: { user } } = await supabase.auth.getUser();
|
||||||
|
if (!user?.email) throw new Error('User email not found');
|
||||||
|
|
||||||
|
// Re-authenticate with password
|
||||||
|
const { error } = await supabase.auth.signInWithPassword({
|
||||||
|
email: user.email,
|
||||||
|
password: password
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
toast.success('Password verified');
|
||||||
|
setStep('totp');
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Password verification failed:', error);
|
||||||
|
toast.error('Invalid password. Please try again.');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTOTPVerification = async () => {
|
||||||
|
if (!totpCode.trim() || totpCode.length !== 6) {
|
||||||
|
toast.error('Please enter a valid 6-digit code');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
// Create challenge
|
||||||
|
const { data: challengeData, error: challengeError } = await supabase.auth.mfa.challenge({
|
||||||
|
factorId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (challengeError) throw challengeError;
|
||||||
|
|
||||||
|
// Verify TOTP code
|
||||||
|
const { error: verifyError } = await supabase.auth.mfa.verify({
|
||||||
|
factorId,
|
||||||
|
challengeId: challengeData.id,
|
||||||
|
code: totpCode.trim()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (verifyError) throw verifyError;
|
||||||
|
|
||||||
|
toast.success('TOTP code verified');
|
||||||
|
setStep('confirm');
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('TOTP verification failed:', error);
|
||||||
|
toast.error('Invalid code. Please try again.');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMFARemoval = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
// Unenroll the factor
|
||||||
|
const { error } = await supabase.auth.mfa.unenroll({ factorId });
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
// Log the action
|
||||||
|
const { data: { user } } = await supabase.auth.getUser();
|
||||||
|
if (user) {
|
||||||
|
await supabase.from('admin_audit_log').insert({
|
||||||
|
admin_user_id: user.id,
|
||||||
|
target_user_id: user.id,
|
||||||
|
action: 'mfa_disabled',
|
||||||
|
details: {
|
||||||
|
factor_id: factorId,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
user_agent: navigator.userAgent
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trigger email notification
|
||||||
|
await supabase.functions.invoke('trigger-notification', {
|
||||||
|
body: {
|
||||||
|
userId: user.id,
|
||||||
|
workflowId: 'security-alert',
|
||||||
|
payload: {
|
||||||
|
action: 'MFA Disabled',
|
||||||
|
message: 'Two-factor authentication has been disabled on your account.',
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).catch(err => console.error('Notification error:', err));
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success('Two-factor authentication has been disabled');
|
||||||
|
handleClose();
|
||||||
|
onSuccess();
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('MFA removal failed:', error);
|
||||||
|
toast.error('Failed to disable MFA. Please try again.');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle className="flex items-center gap-2">
|
||||||
|
<Shield className="h-5 w-5 text-destructive" />
|
||||||
|
Disable Two-Factor Authentication
|
||||||
|
</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription asChild>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertTriangle className="h-4 w-4" />
|
||||||
|
<AlertDescription>
|
||||||
|
Disabling MFA will make your account less secure. You'll need to verify your identity first.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
{step === 'password' && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<p className="text-sm">Step 1 of 3: Enter your password to continue</p>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="password">Password</Label>
|
||||||
|
<Input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
onKeyDown={(e) => e.key === 'Enter' && handlePasswordVerification()}
|
||||||
|
placeholder="Enter your password"
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 'totp' && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<p className="text-sm">Step 2 of 3: Enter your current TOTP code</p>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="totp">Verification Code</Label>
|
||||||
|
<Input
|
||||||
|
id="totp"
|
||||||
|
type="text"
|
||||||
|
value={totpCode}
|
||||||
|
onChange={(e) => setTotpCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
|
||||||
|
onKeyDown={(e) => e.key === 'Enter' && handleTOTPVerification()}
|
||||||
|
placeholder="000000"
|
||||||
|
maxLength={6}
|
||||||
|
disabled={loading}
|
||||||
|
className="text-center text-2xl tracking-widest"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 'confirm' && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<p className="text-sm font-semibold">Step 3 of 3: Final confirmation</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Are you sure you want to disable two-factor authentication? This will:
|
||||||
|
</p>
|
||||||
|
<ul className="text-sm text-muted-foreground list-disc list-inside space-y-1">
|
||||||
|
<li>Remove TOTP protection from your account</li>
|
||||||
|
<li>Send a security notification email</li>
|
||||||
|
<li>Log this action in your security history</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel onClick={handleClose} disabled={loading}>
|
||||||
|
Cancel
|
||||||
|
</AlertDialogCancel>
|
||||||
|
{step === 'password' && (
|
||||||
|
<Button onClick={handlePasswordVerification} disabled={loading}>
|
||||||
|
{loading ? 'Verifying...' : 'Continue'}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{step === 'totp' && (
|
||||||
|
<Button onClick={handleTOTPVerification} disabled={loading}>
|
||||||
|
{loading ? 'Verifying...' : 'Continue'}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{step === 'confirm' && (
|
||||||
|
<AlertDialogAction onClick={handleMFARemoval} disabled={loading} className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
|
||||||
|
{loading ? 'Disabling...' : 'Disable MFA'}
|
||||||
|
</AlertDialogAction>
|
||||||
|
)}
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,11 +5,11 @@ import { Label } from '@/components/ui/label';
|
|||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog';
|
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
import { useAuth } from '@/hooks/useAuth';
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
import { supabase } from '@/integrations/supabase/client';
|
import { supabase } from '@/integrations/supabase/client';
|
||||||
import { Smartphone, Shield, Copy, Eye, EyeOff, Trash2, AlertCircle } from 'lucide-react';
|
import { Smartphone, Shield, Copy, Eye, EyeOff, Trash2 } from 'lucide-react';
|
||||||
|
import { MFARemovalDialog } from './MFARemovalDialog';
|
||||||
|
|
||||||
interface TOTPFactor {
|
interface TOTPFactor {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -30,6 +30,7 @@ export function TOTPSetup() {
|
|||||||
const [factorId, setFactorId] = useState('');
|
const [factorId, setFactorId] = useState('');
|
||||||
const [verificationCode, setVerificationCode] = useState('');
|
const [verificationCode, setVerificationCode] = useState('');
|
||||||
const [showSecret, setShowSecret] = useState(false);
|
const [showSecret, setShowSecret] = useState(false);
|
||||||
|
const [showRemovalDialog, setShowRemovalDialog] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchTOTPFactors();
|
fetchTOTPFactors();
|
||||||
@@ -133,30 +134,8 @@ export function TOTPSetup() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const unenrollFactor = async (factorId: string) => {
|
const handleRemovalSuccess = async () => {
|
||||||
setLoading(true);
|
await fetchTOTPFactors();
|
||||||
try {
|
|
||||||
const { error } = await supabase.auth.mfa.unenroll({
|
|
||||||
factorId
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error) throw error;
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: 'TOTP Disabled',
|
|
||||||
description: 'Two-factor authentication has been disabled for your account.'
|
|
||||||
});
|
|
||||||
|
|
||||||
fetchTOTPFactors();
|
|
||||||
} catch (error: any) {
|
|
||||||
toast({
|
|
||||||
title: 'Error',
|
|
||||||
description: error.message || 'Failed to disable TOTP',
|
|
||||||
variant: 'destructive'
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const copySecret = () => {
|
const copySecret = () => {
|
||||||
@@ -281,36 +260,23 @@ export function TOTPSetup() {
|
|||||||
<Badge variant="secondary" className="bg-green-50 dark:bg-green-950 text-green-700 dark:text-green-300">
|
<Badge variant="secondary" className="bg-green-50 dark:bg-green-950 text-green-700 dark:text-green-300">
|
||||||
Active
|
Active
|
||||||
</Badge>
|
</Badge>
|
||||||
<AlertDialog>
|
<Button
|
||||||
<AlertDialogTrigger asChild>
|
variant="outline"
|
||||||
<Button variant="outline" size="sm">
|
size="sm"
|
||||||
|
onClick={() => setShowRemovalDialog(true)}
|
||||||
|
>
|
||||||
<Trash2 className="w-4 h-4 mr-2" />
|
<Trash2 className="w-4 h-4 mr-2" />
|
||||||
Disable
|
Disable
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogTrigger>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle className="flex items-center gap-2">
|
|
||||||
<AlertCircle className="w-5 h-5 text-destructive" />
|
|
||||||
Disable Two-Factor Authentication
|
|
||||||
</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
Are you sure you want to disable two-factor authentication? This will make your account less secure.
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
||||||
<AlertDialogAction
|
|
||||||
onClick={() => unenrollFactor(activeFactor.id)}
|
|
||||||
className="bg-destructive hover:bg-destructive/90"
|
|
||||||
>
|
|
||||||
Disable TOTP
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<MFARemovalDialog
|
||||||
|
open={showRemovalDialog}
|
||||||
|
onOpenChange={setShowRemovalDialog}
|
||||||
|
factorId={activeFactor.id}
|
||||||
|
onSuccess={handleRemovalSuccess}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center justify-between p-4 border rounded-lg">
|
<div className="flex items-center justify-between p-4 border rounded-lg">
|
||||||
|
|||||||
Reference in New Issue
Block a user