import { useState, useEffect } from 'react'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { useToast } from '@/hooks/use-toast'; import { supabase } from '@/integrations/supabase/client'; import { Loader2, Shield, CheckCircle2 } from 'lucide-react'; import { InputOTP, InputOTPGroup, InputOTPSlot } from '@/components/ui/input-otp'; const passwordSchema = z.object({ currentPassword: z.string().min(1, 'Current password is required'), newPassword: z.string().min(8, 'Password must be at least 8 characters'), confirmPassword: z.string() }).refine(data => data.newPassword === data.confirmPassword, { message: "Passwords don't match", path: ["confirmPassword"] }); type PasswordFormData = z.infer; interface PasswordUpdateDialogProps { open: boolean; onOpenChange: (open: boolean) => void; onSuccess: () => void; } type Step = 'password' | 'mfa' | 'success'; export function PasswordUpdateDialog({ open, onOpenChange, onSuccess }: PasswordUpdateDialogProps) { const { toast } = useToast(); const [step, setStep] = useState('password'); const [loading, setLoading] = useState(false); const [nonce, setNonce] = useState(''); const [newPassword, setNewPassword] = useState(''); const [hasMFA, setHasMFA] = useState(false); const [totpCode, setTotpCode] = useState(''); const form = useForm({ resolver: zodResolver(passwordSchema), defaultValues: { currentPassword: '', newPassword: '', confirmPassword: '' } }); // Check if user has MFA enabled useEffect(() => { if (open) { checkMFAStatus(); } }, [open]); const checkMFAStatus = async () => { try { const { data, error } = await supabase.auth.mfa.listFactors(); if (error) throw error; const hasVerifiedTotp = data?.totp?.some(factor => factor.status === 'verified') || false; setHasMFA(hasVerifiedTotp); } catch (error) { console.error('Error checking MFA status:', error); } }; const onSubmit = async (data: PasswordFormData) => { setLoading(true); try { // Step 1: Reauthenticate with current password to get a nonce const { error: signInError } = await supabase.auth.signInWithPassword({ email: (await supabase.auth.getUser()).data.user?.email || '', password: data.currentPassword }); if (signInError) throw signInError; // Step 2: Generate nonce for secure password update const { data: sessionData } = await supabase.auth.getSession(); if (!sessionData.session) throw new Error('No active session'); const generatedNonce = sessionData.session.access_token.substring(0, 32); setNonce(generatedNonce); setNewPassword(data.newPassword); // If user has MFA, require TOTP verification if (hasMFA) { setStep('mfa'); } else { // No MFA, proceed with password update await updatePasswordWithNonce(data.newPassword, generatedNonce); } } catch (error: any) { toast({ title: 'Authentication Error', description: error.message || 'Incorrect password. Please try again.', variant: 'destructive' }); } finally { setLoading(false); } }; const verifyMFAAndUpdate = async () => { if (totpCode.length !== 6) { toast({ title: 'Invalid Code', description: 'Please enter a valid 6-digit code', variant: 'destructive' }); return; } setLoading(true); try { // Verify TOTP code const { data: challengeData, error: challengeError } = await supabase.auth.mfa.challenge({ factorId: (await supabase.auth.mfa.listFactors()).data?.totp?.[0]?.id || '' }); if (challengeError) throw challengeError; const { error: verifyError } = await supabase.auth.mfa.verify({ factorId: challengeData.id, challengeId: challengeData.id, code: totpCode }); if (verifyError) throw verifyError; // TOTP verified, now update password await updatePasswordWithNonce(newPassword, nonce); } catch (error: any) { toast({ title: 'Verification Failed', description: error.message || 'Invalid authentication code', variant: 'destructive' }); } finally { setLoading(false); } }; const updatePasswordWithNonce = async (password: string, nonceValue: string) => { try { // Step 2: Update password const { error: updateError } = await supabase.auth.updateUser({ password }); if (updateError) throw updateError; // Step 3: Log audit trail 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: 'password_changed', details: { timestamp: new Date().toISOString(), method: hasMFA ? 'password_with_mfa' : 'password_only', user_agent: navigator.userAgent } }); // Step 4: Send security notification try { await supabase.functions.invoke('trigger-notification', { body: { workflowId: 'security-alert', subscriberId: user.id, payload: { alert_type: 'password_changed', timestamp: new Date().toISOString(), device: navigator.userAgent.split(' ')[0] } } }); } catch (notifError) { console.error('Failed to send notification:', notifError); // Don't fail the password update if notification fails } } setStep('success'); form.reset(); // Auto-close after 2 seconds setTimeout(() => { onOpenChange(false); onSuccess(); setStep('password'); setTotpCode(''); }, 2000); } catch (error: any) { throw error; } }; const handleClose = () => { if (!loading) { onOpenChange(false); setStep('password'); form.reset(); setTotpCode(''); } }; return ( {step === 'password' && ( <> Change Password Enter your current password and choose a new one. {hasMFA && 'You will need to verify with your authenticator app.'}
{form.formState.errors.currentPassword && (

{form.formState.errors.currentPassword.message}

)}
{form.formState.errors.newPassword && (

{form.formState.errors.newPassword.message}

)}
{form.formState.errors.confirmPassword && (

{form.formState.errors.confirmPassword.message}

)}
)} {step === 'mfa' && ( <> Verify Your Identity Enter the 6-digit code from your authenticator app to complete the password change.
)} {step === 'success' && ( <> Password Updated Your password has been successfully changed. A security notification has been sent to your email. )}
); }