'use client'; import { useCallback, useState } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; import { ExclamationTriangleIcon } from '@radix-ui/react-icons'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { ArrowLeftIcon } from 'lucide-react'; import { useForm, useWatch } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { toast } from 'sonner'; import { z } from 'zod'; import { useSupabase } from '@kit/supabase/hooks/use-supabase'; import { useFactorsMutationKey } from '@kit/supabase/hooks/use-user-factors-mutation-key'; import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert'; import { Button } from '@kit/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from '@kit/ui/dialog'; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from '@kit/ui/form'; import { If } from '@kit/ui/if'; import { Input } from '@kit/ui/input'; import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot, } from '@kit/ui/input-otp'; import { Trans } from '@kit/ui/trans'; import { refreshAuthSession } from '../../../server/personal-accounts-server-actions'; export function MultiFactorAuthSetupDialog(props: { userId: string }) { const { t } = useTranslation(); const [isOpen, setIsOpen] = useState(false); const onEnrollSuccess = useCallback(() => { setIsOpen(false); return toast.success(t(`account:multiFactorSetupSuccess`)); }, [t]); return ( e.preventDefault()} onEscapeKeyDown={(e) => e.preventDefault()} >
setIsOpen(false)} onEnrolled={onEnrollSuccess} />
); } function MultiFactorAuthSetupForm({ onEnrolled, onCancel, userId, }: React.PropsWithChildren<{ userId: string; onCancel: () => void; onEnrolled: () => void; }>) { const verifyCodeMutation = useVerifyCodeMutation(userId); const verificationCodeForm = useForm({ resolver: zodResolver( z.object({ factorId: z.string().min(1), verificationCode: z.string().min(6).max(6), }), ), defaultValues: { factorId: '', verificationCode: '', }, }); const [state, setState] = useState({ loading: false, error: '', }); const factorId = useWatch({ name: 'factorId', control: verificationCodeForm.control, }); const onSubmit = useCallback( async ({ verificationCode, factorId, }: { verificationCode: string; factorId: string; }) => { setState({ loading: true, error: '', }); try { await verifyCodeMutation.mutateAsync({ factorId, code: verificationCode, }); await refreshAuthSession(); setState({ loading: false, error: '', }); onEnrolled(); } catch (error) { const message = (error as Error).message || `Unknown error`; setState({ loading: false, error: message, }); } }, [onEnrolled, verifyCodeMutation], ); if (state.error) { return ; } return (
verificationCodeForm.setValue('factorId', factorId) } />
{ return ( ); }} name={'verificationCode'} />
); } function FactorQrCode({ onSetFactorId, onCancel, userId, }: React.PropsWithChildren<{ userId: string; onCancel: () => void; onSetFactorId: (factorId: string) => void; }>) { const enrollFactorMutation = useEnrollFactor(userId); const { t } = useTranslation(); const [error, setError] = useState(''); const form = useForm({ resolver: zodResolver( z.object({ factorName: z.string().min(1), qrCode: z.string().min(1), }), ), defaultValues: { factorName: '', qrCode: '', }, }); const factorName = useWatch({ name: 'factorName', control: form.control }); if (error) { return (
); } if (!factorName) { return ( { const response = await enrollFactorMutation.mutateAsync(name); if (!response.success) { return setError(response.data as string); } const data = response.data; if (data.type === 'totp') { form.setValue('factorName', name); form.setValue('qrCode', data.totp.qr_code); } // dispatch event to set factor ID onSetFactorId(data.id); }} /> ); } return (

); } function FactorNameForm( props: React.PropsWithChildren<{ onSetFactorName: (name: string) => void; onCancel: () => void; }>, ) { const form = useForm({ resolver: zodResolver( z.object({ name: z.string().min(1), }), ), defaultValues: { name: '', }, }); return (
{ props.onSetFactorName(data.name); })} >
{ return ( ); }} />
); } function QrImage({ src }: { src: string }) { return {'QR; } function useEnrollFactor(userId: string) { const client = useSupabase(); const queryClient = useQueryClient(); const mutationKey = useFactorsMutationKey(userId); const mutationFn = async (factorName: string) => { const response = await client.auth.mfa.enroll({ friendlyName: factorName, factorType: 'totp', }); if (response.error) { return { success: false as const, data: response.error.code, }; } return { success: true as const, data: response.data, }; }; return useMutation({ mutationFn, mutationKey, onSuccess() { return queryClient.refetchQueries({ queryKey: mutationKey, }); }, }); } function useVerifyCodeMutation(userId: string) { const mutationKey = useFactorsMutationKey(userId); const client = useSupabase(); const mutationFn = async (params: { factorId: string; code: string }) => { const challenge = await client.auth.mfa.challenge({ factorId: params.factorId, }); if (challenge.error) { throw challenge.error; } const challengeId = challenge.data.id; const verify = await client.auth.mfa.verify({ factorId: params.factorId, code: params.code, challengeId, }); if (verify.error) { throw verify.error; } return verify; }; return useMutation({ mutationKey, mutationFn }); } function ErrorAlert() { return ( ); }