'use client'; import { useCallback, useEffect, useEffectEvent, useState } from 'react'; import { useRouter } from 'next/navigation'; import type { Factor } from '@supabase/supabase-js'; import { zodResolver } from '@hookform/resolvers/zod'; import { useMutation } from '@tanstack/react-query'; import { Fingerprint, KeyRound, TriangleAlert } from 'lucide-react'; import { useForm } from 'react-hook-form'; import * as z from 'zod'; import { useFetchAuthFactors } from '@kit/supabase/hooks/use-fetch-mfa-factors'; import { useSignOut } from '@kit/supabase/hooks/use-sign-out'; import { useSupabase } from '@kit/supabase/hooks/use-supabase'; import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert'; import { Button } from '@kit/ui/button'; import { Form, FormControl, FormDescription, FormField, FormItem, FormMessage, } from '@kit/ui/form'; import { Heading } from '@kit/ui/heading'; import { If } from '@kit/ui/if'; import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot, } from '@kit/ui/input-otp'; import { Spinner } from '@kit/ui/spinner'; import { Trans } from '@kit/ui/trans'; export function MultiFactorChallengeContainer({ paths, userId, }: React.PropsWithChildren<{ userId: string; paths: { redirectPath: string; }; }>) { const router = useRouter(); const [selectedFactor, setSelectedFactor] = useState(null); const onSuccess = useCallback(() => { router.replace(paths.redirectPath); }, [router, paths.redirectPath]); if (!selectedFactor) { return ( ); } if (selectedFactor.factor_type === 'webauthn') { return ( setSelectedFactor(null)} /> ); } return ; } function TotpChallengeView({ factor, onSuccess, }: { factor: Factor; onSuccess: () => void; }) { const verifyMFAChallenge = useVerifyMFAChallenge({ onSuccess }); const verificationCodeForm = useForm({ resolver: zodResolver( z.object({ verificationCode: z.string().min(6).max(6), }), ), defaultValues: { verificationCode: '', }, }); return (
{ await verifyMFAChallenge.mutateAsync({ factorId: factor.id, verificationCode: data.verificationCode, }); })} >
{ return ( ); }} />
); } function WebAuthnChallengeView({ factor, onSuccess, onBack, }: { factor: Factor; onSuccess: () => void; onBack: () => void; }) { const authenticatePasskey = useAuthenticatePasskey({ onSuccess }); const [autoTriggered, setAutoTriggered] = useState(false); // Auto-trigger the passkey challenge on mount useEffect(() => { if (!autoTriggered) { setAutoTriggered(true); authenticatePasskey.mutate(factor.id); } }, [autoTriggered, authenticatePasskey, factor.id]); return (

); } function useVerifyMFAChallenge({ onSuccess }: { onSuccess: () => void }) { const client = useSupabase(); const mutationKey = ['mfa-verify-challenge']; const mutationFn = async (params: { factorId: string; verificationCode: string; }) => { const { factorId, verificationCode: code } = params; const response = await client.auth.mfa.challengeAndVerify({ factorId, code, }); if (response.error) { throw response.error; } return response.data; }; return useMutation({ mutationKey, mutationFn, onSuccess }); } function useAuthenticatePasskey({ onSuccess }: { onSuccess: () => void }) { const client = useSupabase(); const mutationKey = ['mfa-webauthn-authenticate']; const mutationFn = async (factorId: string) => { const { data, error } = await client.auth.mfa.webauthn.authenticate({ factorId, }); if (error) { throw error; } return data; }; return useMutation({ mutationKey, mutationFn, onSuccess }); } function FactorsListContainer({ onSelect, userId, }: React.PropsWithChildren<{ userId: string; onSelect: (factor: Factor) => void; }>) { const signOut = useSignOut(); const { data: factors, isLoading, error } = useFetchAuthFactors(userId); const isSuccess = factors && !isLoading && !error; const signOutFn = useEffectEvent(() => { void signOut.mutateAsync(); }); useEffect(() => { // If there is an error, sign out if (error) { void signOutFn(); } }, [error]); useEffect(() => { if (!isSuccess) return; const allVerified = [ ...(factors.totp ?? []), ...((factors as Record).webauthn ?? []), ]; // If there is only one factor, select it automatically if (allVerified.length === 1 && allVerified[0]) { onSelect(allVerified[0]); } }); if (isLoading) { return (
); } if (error) { return (
); } const totpFactors = factors?.totp ?? []; const webauthnFactors = (factors as Record | undefined)?.webauthn ?? []; return (
{totpFactors.map((factor) => (
))} {webauthnFactors.map((factor) => (
))}
); }