'use client'; import { useEffect } from 'react'; import { useRouter } from 'next/navigation'; import { zodResolver } from '@hookform/resolvers/zod'; import { ExclamationTriangleIcon } from '@radix-ui/react-icons'; import { useMutation } from '@tanstack/react-query'; import { useForm, useWatch } from 'react-hook-form'; import { 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'; import pathsConfig from '~/config/paths.config'; export function MultiFactorChallengeContainer({ userId, }: React.PropsWithChildren<{ userId: string; }>) { const router = useRouter(); const verifyMFAChallenge = useVerifyMFAChallenge({ onSuccess: () => { router.replace(pathsConfig.app.home); }, }); const verificationCodeForm = useForm({ resolver: zodResolver( z.object({ factorId: z.string().min(1), verificationCode: z.string().min(6).max(6), }), ), defaultValues: { factorId: '', verificationCode: '', }, }); const factorId = useWatch({ name: 'factorId', control: verificationCodeForm.control, }); if (!factorId) { return ( { verificationCodeForm.setValue('factorId', factorId); }} /> ); } return (
{ await verifyMFAChallenge.mutateAsync({ factorId, verificationCode: data.verificationCode, }); })} >
{ 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) { console.warn( { error: response.error.message }, 'Failed to verify MFA challenge', ); throw response.error; } return response.data; }; return useMutation({ mutationKey, mutationFn, onSuccess }); } function FactorsListContainer({ onSelect, userId, }: React.PropsWithChildren<{ userId: string; onSelect: (factor: string) => void; }>) { const signOut = useSignOut(); const { data: factors, isLoading, error } = useFetchAuthFactors(userId); const isSuccess = factors && !isLoading && !error; useEffect(() => { // If there is an error, sign out if (error) { void signOut.mutateAsync(); } }, [error, signOut]); useEffect(() => { // If there is only one factor, select it automatically if (isSuccess && factors.totp.length === 1) { const factorId = factors.totp[0]?.id; if (factorId) { onSelect(factorId); } } }); if (isLoading) { return (
); } if (error) { return (
); } const verifiedFactors = factors?.totp ?? []; return (
{verifiedFactors.map((factor) => (
))}
); }