Remove multiple components related to multi-factor authentication setup
Removed personal account related multi-factor authentication setup modal and otp-input. Adjusted dependencies, exports, and imports to reflect the deletion. Various adjustments in other areas of the codebase were made to account for these deletions, including moving necessary components and adding the 'input-otp' library in the package.json under 'ui' directory.
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -9,10 +11,11 @@ import { If } from '@kit/ui/if';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { AccountDangerZone } from './account-danger-zone';
|
||||
import { UpdateEmailFormContainer } from './email/update-email-form-container';
|
||||
import { MultiFactorAuthFactorsList } from './mfa/multi-factor-auth-list';
|
||||
import { UpdatePasswordFormContainer } from './password/update-password-container';
|
||||
import { UpdateAccountDetailsFormContainer } from './update-account-details-form-container';
|
||||
import { UpdateAccountImageContainer } from './update-account-image-container';
|
||||
import { UpdateEmailFormContainer } from './update-email-form-container';
|
||||
import { UpdatePasswordFormContainer } from './update-password-container';
|
||||
|
||||
export function PersonalAccountSettingsContainer(
|
||||
props: React.PropsWithChildren<{
|
||||
@@ -91,6 +94,22 @@ export function PersonalAccountSettingsContainer(
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<Trans i18nKey={'account:multiFactorAuth'} />
|
||||
</CardTitle>
|
||||
|
||||
<CardDescription>
|
||||
<Trans i18nKey={'account:multiFactorAuthDescription'} />
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<MultiFactorAuthFactorsList />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<If condition={props.features.enableAccountDeletion}>
|
||||
<Card className={'border-destructive border-2'}>
|
||||
<CardHeader>
|
||||
|
||||
@@ -22,7 +22,7 @@ import { If } from '@kit/ui/if';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { UpdateEmailSchema } from '../../schema/update-email.schema';
|
||||
import { UpdateEmailSchema } from '../../../schema/update-email.schema';
|
||||
|
||||
function createEmailResolver(currentEmail: string, errorMessage: string) {
|
||||
return zodResolver(
|
||||
@@ -0,0 +1,279 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import type { Factor } from '@supabase/gotrue-js';
|
||||
|
||||
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { X } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import useFetchAuthFactors from '@kit/supabase/hooks/use-fetch-mfa-factors';
|
||||
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 {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@kit/ui/alert-dialog';
|
||||
import { Badge } from '@kit/ui/badge';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { If } from '@kit/ui/if';
|
||||
import Spinner from '@kit/ui/spinner';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@kit/ui/table';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@kit/ui/tooltip';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { MultiFactorAuthSetupDialog } from './multi-factor-auth-setup-dialog';
|
||||
|
||||
const MAX_FACTOR_COUNT = 10;
|
||||
|
||||
export function MultiFactorAuthFactorsList() {
|
||||
const { data: factors, isLoading, isError } = useFetchAuthFactors();
|
||||
const [unEnrolling, setUnenrolling] = useState<string>();
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className={'flex items-center space-x-4'}>
|
||||
<Spinner />
|
||||
|
||||
<div>
|
||||
<Trans i18nKey={'account:loadingFactors'} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<div>
|
||||
<Alert variant={'destructive'}>
|
||||
<ExclamationTriangleIcon className={'h-4'} />
|
||||
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'account:factorsListError'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans i18nKey={'account:factorsListErrorDescription'} />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const allFactors = factors?.all ?? [];
|
||||
|
||||
if (!allFactors.length) {
|
||||
return (
|
||||
<div className={'flex flex-col space-y-4'}>
|
||||
<Alert variant={'info'}>
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'account:multiFactorAuthHeading'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans i18nKey={'account:multiFactorAuthDescription'} />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div>
|
||||
<MultiFactorAuthSetupDialog />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const canAddNewFactors = allFactors.length < MAX_FACTOR_COUNT;
|
||||
|
||||
return (
|
||||
<div className={'flex flex-col space-y-4'}>
|
||||
<FactorsTable factors={allFactors} setUnenrolling={setUnenrolling} />
|
||||
|
||||
<If condition={canAddNewFactors}>
|
||||
<div>
|
||||
<MultiFactorAuthSetupDialog />
|
||||
</div>
|
||||
</If>
|
||||
|
||||
<If condition={unEnrolling}>
|
||||
{(factorId) => (
|
||||
<ConfirmUnenrollFactorModal
|
||||
factorId={factorId}
|
||||
setIsModalOpen={() => setUnenrolling(undefined)}
|
||||
/>
|
||||
)}
|
||||
</If>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ConfirmUnenrollFactorModal(
|
||||
props: React.PropsWithChildren<{
|
||||
factorId: string;
|
||||
setIsModalOpen: (isOpen: boolean) => void;
|
||||
}>,
|
||||
) {
|
||||
const { t } = useTranslation();
|
||||
const unEnroll = useUnenrollFactor();
|
||||
|
||||
const onUnenrollRequested = useCallback(
|
||||
(factorId: string) => {
|
||||
if (unEnroll.isPending) return;
|
||||
|
||||
const promise = unEnroll.mutateAsync(factorId).then(() => {
|
||||
props.setIsModalOpen(false);
|
||||
});
|
||||
|
||||
toast.promise(promise, {
|
||||
loading: t(`account:unenrollingFactor`),
|
||||
success: t(`account:unenrollFactorSuccess`),
|
||||
error: t(`account:unenrollFactorError`),
|
||||
});
|
||||
},
|
||||
[props, t, unEnroll],
|
||||
);
|
||||
|
||||
return (
|
||||
<AlertDialog open={!!props.factorId} onOpenChange={props.setIsModalOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
<Trans i18nKey={'account:unenrollFactorModalHeading'} />
|
||||
</AlertDialogTitle>
|
||||
|
||||
<AlertDialogDescription>
|
||||
<Trans i18nKey={'account:unenrollFactorModalDescription'} />
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
||||
<AlertDialogAction
|
||||
className={'w-full'}
|
||||
type={'button'}
|
||||
disabled={unEnroll.isPending}
|
||||
onClick={() => onUnenrollRequested(props.factorId)}
|
||||
>
|
||||
<Trans i18nKey={'account:unenrollFactorModalButtonLabel'} />
|
||||
</AlertDialogAction>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
}
|
||||
|
||||
function FactorsTable({
|
||||
setUnenrolling,
|
||||
factors,
|
||||
}: React.PropsWithChildren<{
|
||||
setUnenrolling: (factorId: string) => void;
|
||||
factors: Factor[];
|
||||
}>) {
|
||||
return (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>
|
||||
<Trans i18nKey={'account:factorName'} />
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
<Trans i18nKey={'account:factorType'} />
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
<Trans i18nKey={'account:factorStatus'} />
|
||||
</TableHead>
|
||||
|
||||
<TableHead />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
{factors.map((factor) => (
|
||||
<TableRow key={factor.id}>
|
||||
<TableCell>
|
||||
<span className={'block truncate'}>{factor.friendly_name}</span>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<Badge variant={'info'} className={'inline-flex uppercase'}>
|
||||
{factor.factor_type}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
|
||||
<td>
|
||||
<Badge
|
||||
variant={'info'}
|
||||
className={'inline-flex capitalize'}
|
||||
color={factor.status === 'verified' ? 'success' : 'normal'}
|
||||
>
|
||||
{factor.status}
|
||||
</Badge>
|
||||
</td>
|
||||
|
||||
<td className={'flex justify-end'}>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant={'ghost'}
|
||||
size={'icon'}
|
||||
onClick={() => setUnenrolling(factor.id)}
|
||||
>
|
||||
<X className={'h-4'} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent>
|
||||
<Trans i18nKey={'account:unenrollTooltip'} />
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</td>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
function useUnenrollFactor() {
|
||||
const queryClient = useQueryClient();
|
||||
const client = useSupabase();
|
||||
const mutationKey = useFactorsMutationKey();
|
||||
|
||||
const mutationFn = async (factorId: string) => {
|
||||
const { data, error } = await client.auth.mfa.unenroll({
|
||||
factorId,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
return useMutation({
|
||||
mutationFn,
|
||||
mutationKey,
|
||||
onSuccess: async () => {
|
||||
return queryClient.refetchQueries({
|
||||
queryKey: mutationKey,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,450 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import Image from 'next/image';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useForm } 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,
|
||||
} 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() {
|
||||
const { t } = useTranslation();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const onEnrollSuccess = useCallback(() => {
|
||||
setIsOpen(false);
|
||||
|
||||
return toast.success(t(`multiFactorSetupSuccess`));
|
||||
}, [t]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={() => setIsOpen(true)}>
|
||||
<Trans i18nKey={'account:setupMfaButtonLabel'} />
|
||||
</Button>
|
||||
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogContent
|
||||
onInteractOutside={(e) => e.preventDefault()}
|
||||
onEscapeKeyDown={(e) => e.preventDefault()}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans i18nKey={'account:setupMfaButtonLabel'} />
|
||||
</DialogTitle>
|
||||
|
||||
<DialogDescription>
|
||||
<Trans i18nKey={'account:multiFactorAuthDescription'} />
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div>
|
||||
<MultiFactorAuthSetupForm
|
||||
onCancel={() => setIsOpen(false)}
|
||||
onEnrolled={onEnrollSuccess}
|
||||
/>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function MultiFactorAuthSetupForm({
|
||||
onEnrolled,
|
||||
onCancel,
|
||||
}: React.PropsWithChildren<{
|
||||
onCancel: () => void;
|
||||
onEnrolled: () => void;
|
||||
}>) {
|
||||
const verifyCodeMutation = useVerifyCodeMutation();
|
||||
|
||||
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 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 <ErrorAlert />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'flex flex-col space-y-4'}>
|
||||
<div className={'flex justify-center'}>
|
||||
<FactorQrCode
|
||||
onCancel={onCancel}
|
||||
onSetFactorId={(factorId) =>
|
||||
verificationCodeForm.setValue('factorId', factorId)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<If condition={verificationCodeForm.watch('factorId')}>
|
||||
<Form {...verificationCodeForm}>
|
||||
<form
|
||||
onSubmit={verificationCodeForm.handleSubmit(onSubmit)}
|
||||
className={'w-full'}
|
||||
>
|
||||
<div className={'flex flex-col space-y-4'}>
|
||||
<FormField
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem
|
||||
className={
|
||||
'mx-auto flex flex-col items-center justify-center'
|
||||
}
|
||||
>
|
||||
<FormControl>
|
||||
<InputOTP {...field} maxLength={6} minLength={6}>
|
||||
<InputOTPGroup>
|
||||
<InputOTPSlot index={0} />
|
||||
<InputOTPSlot index={1} />
|
||||
<InputOTPSlot index={2} />
|
||||
</InputOTPGroup>
|
||||
<InputOTPSeparator />
|
||||
<InputOTPGroup>
|
||||
<InputOTPSlot index={3} />
|
||||
<InputOTPSlot index={4} />
|
||||
<InputOTPSlot index={5} />
|
||||
</InputOTPGroup>
|
||||
</InputOTP>
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
<Trans
|
||||
i18nKey={'account:verifyActivationCodeDescription'}
|
||||
/>
|
||||
</FormDescription>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
name={'verificationCode'}
|
||||
/>
|
||||
|
||||
<Button
|
||||
disabled={!verificationCodeForm.formState.isValid}
|
||||
type={'submit'}
|
||||
>
|
||||
{state.loading ? (
|
||||
<Trans i18nKey={'account:verifyingCode'} />
|
||||
) : (
|
||||
<Trans i18nKey={'account:enableMfaFactor'} />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Button type={'button'} variant={'ghost'} onClick={onCancel}>
|
||||
<Trans i18nKey={'common:cancel'} />
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</If>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FactorQrCode({
|
||||
onSetFactorId,
|
||||
onCancel,
|
||||
}: React.PropsWithChildren<{
|
||||
onCancel: () => void;
|
||||
onSetFactorId: (factorId: string) => void;
|
||||
}>) {
|
||||
const enrollFactorMutation = useEnrollFactor();
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(
|
||||
z.object({
|
||||
factorName: z.string().min(1),
|
||||
qrCode: z.string().min(1),
|
||||
}),
|
||||
),
|
||||
defaultValues: {
|
||||
factorName: '',
|
||||
qrCode: '',
|
||||
},
|
||||
});
|
||||
|
||||
const factorName = form.watch('factorName');
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className={'flex w-full flex-col space-y-2'}>
|
||||
<Alert variant={'destructive'}>
|
||||
<ExclamationTriangleIcon className={'h-4'} />
|
||||
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'account:qrCodeErrorHeading'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans i18nKey={'account:qrCodeErrorDescription'} />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!factorName) {
|
||||
return (
|
||||
<FactorNameForm
|
||||
onCancel={onCancel}
|
||||
onSetFactorName={async (name) => {
|
||||
const data = await enrollFactorMutation.mutateAsync(name);
|
||||
|
||||
if (!data) {
|
||||
return setError(true);
|
||||
}
|
||||
|
||||
form.setValue('factorName', name);
|
||||
form.setValue('qrCode', data.totp.qr_code);
|
||||
|
||||
// dispatch event to set factor ID
|
||||
onSetFactorId(data.id);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'flex flex-col space-y-4'}>
|
||||
<p>
|
||||
<span className={'text-muted-foreground text-sm'}>
|
||||
<Trans i18nKey={'account:multiFactorModalHeading'} />
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<div className={'flex justify-center'}>
|
||||
<QrImage src={form.getValues('qrCode')} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<Form {...form}>
|
||||
<form
|
||||
className={'w-full'}
|
||||
onSubmit={form.handleSubmit((data) => {
|
||||
props.onSetFactorName(data.name);
|
||||
})}
|
||||
>
|
||||
<div className={'flex flex-col space-y-4'}>
|
||||
<FormField
|
||||
name={'name'}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'account:factorNameLabel'} />
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input autoComplete={'off'} required {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
<Trans i18nKey={'account:factorNameHint'} />
|
||||
</FormDescription>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button type={'submit'}>
|
||||
<Trans i18nKey={'account:factorNameSubmitLabel'} />
|
||||
</Button>
|
||||
|
||||
<Button type={'button'} variant={'ghost'} onClick={props.onCancel}>
|
||||
<Trans i18nKey={'common:cancel'} />
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
function QrImage({ src }: { src: string }) {
|
||||
return <Image alt={'QR Code'} src={src} width={160} height={160} />;
|
||||
}
|
||||
|
||||
function useEnrollFactor() {
|
||||
const client = useSupabase();
|
||||
const mutationKey = useFactorsMutationKey();
|
||||
|
||||
const mutationFn = async (factorName: string) => {
|
||||
const { data, error } = await client.auth.mfa.enroll({
|
||||
friendlyName: factorName,
|
||||
factorType: 'totp',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
return useMutation({
|
||||
mutationFn,
|
||||
mutationKey,
|
||||
});
|
||||
}
|
||||
|
||||
function useVerifyCodeMutation() {
|
||||
const mutationKey = useFactorsMutationKey();
|
||||
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 (
|
||||
<Alert variant={'destructive'}>
|
||||
<ExclamationTriangleIcon className={'h-4'} />
|
||||
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'account:multiFactorSetupErrorHeading'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans i18nKey={'account:multiFactorSetupErrorDescription'} />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
@@ -1,344 +0,0 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import Image from 'next/image';
|
||||
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
|
||||
import { useFactorsMutationKey } from '@kit/supabase/hooks/use-user-factors-mutation-key';
|
||||
import { Alert } from '@kit/ui/alert';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@kit/ui/dialog';
|
||||
import { If } from '@kit/ui/if';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { Label } from '@kit/ui/label';
|
||||
import { OtpInput } from '@kit/ui/otp-input';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
function MultiFactorAuthSetupModal(
|
||||
props: React.PropsWithChildren<{
|
||||
isOpen: boolean;
|
||||
setIsOpen: (isOpen: boolean) => void;
|
||||
}>,
|
||||
) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onEnrollSuccess = useCallback(() => {
|
||||
props.setIsOpen(false);
|
||||
|
||||
return toast.success(t(`multiFactorSetupSuccess`));
|
||||
}, [props, t]);
|
||||
|
||||
return (
|
||||
<Dialog open={props.isOpen} onOpenChange={props.setIsOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans i18nKey={'account:setupMfaButtonLabel'} />
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<MultiFactorAuthSetupForm
|
||||
onCancel={() => props.setIsOpen(false)}
|
||||
onEnrolled={onEnrollSuccess}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function MultiFactorAuthSetupForm({
|
||||
onEnrolled,
|
||||
onCancel,
|
||||
}: React.PropsWithChildren<{
|
||||
onCancel: () => void;
|
||||
onEnrolled: () => void;
|
||||
}>) {
|
||||
const verifyCodeMutation = useVerifyCodeMutation();
|
||||
const [factorId, setFactorId] = useState<string | undefined>();
|
||||
const [verificationCode, setVerificationCode] = useState('');
|
||||
|
||||
const [state, setState] = useState({
|
||||
loading: false,
|
||||
error: '',
|
||||
});
|
||||
|
||||
const onSubmit = useCallback(async () => {
|
||||
setState({
|
||||
loading: true,
|
||||
error: '',
|
||||
});
|
||||
|
||||
if (!factorId || !verificationCode) {
|
||||
return setState({
|
||||
loading: false,
|
||||
error: 'No factor ID or verification code found',
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await verifyCodeMutation.mutateAsync({
|
||||
factorId,
|
||||
code: verificationCode,
|
||||
});
|
||||
|
||||
setState({
|
||||
loading: false,
|
||||
error: '',
|
||||
});
|
||||
|
||||
onEnrolled();
|
||||
} catch (error) {
|
||||
const message = (error as Error).message || `Unknown error`;
|
||||
|
||||
setState({
|
||||
loading: false,
|
||||
error: message,
|
||||
});
|
||||
}
|
||||
}, [onEnrolled, verifyCodeMutation, factorId, verificationCode]);
|
||||
|
||||
if (state.error) {
|
||||
return (
|
||||
<div className={'flex flex-col space-y-4'}>
|
||||
<Alert variant={'destructive'}>
|
||||
<Trans i18nKey={'account:multiFactorSetupError'} />
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'flex flex-col space-y-4'}>
|
||||
<div className={'flex justify-center'}>
|
||||
<FactorQrCode onCancel={onCancel} onSetFactorId={setFactorId} />
|
||||
</div>
|
||||
|
||||
<If condition={factorId}>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
|
||||
return onSubmit();
|
||||
}}
|
||||
className={'w-full'}
|
||||
>
|
||||
<div className={'flex flex-col space-y-4'}>
|
||||
<Label>
|
||||
<Trans i18nKey={'account:verificationCode'} />
|
||||
|
||||
<OtpInput
|
||||
onInvalid={() => setVerificationCode('')}
|
||||
onValid={setVerificationCode}
|
||||
/>
|
||||
|
||||
<span>
|
||||
<Trans i18nKey={'account:verifyActivationCodeDescription'} />
|
||||
</span>
|
||||
</Label>
|
||||
|
||||
<div className={'flex justify-end space-x-2'}>
|
||||
<Button disabled={!verificationCode} type={'submit'}>
|
||||
{state.loading ? (
|
||||
<Trans i18nKey={'account:verifyingCode'} />
|
||||
) : (
|
||||
<Trans i18nKey={'account:enableMfaFactor'} />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</If>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FactorQrCode({
|
||||
onSetFactorId,
|
||||
onCancel,
|
||||
}: React.PropsWithChildren<{
|
||||
onCancel: () => void;
|
||||
onSetFactorId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
}>) {
|
||||
const enrollFactorMutation = useEnrollFactor();
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
const [factor, setFactor] = useState({
|
||||
name: '',
|
||||
qrCode: '',
|
||||
});
|
||||
|
||||
const factorName = factor.name;
|
||||
|
||||
useEffect(() => {
|
||||
if (!factorName) {
|
||||
return;
|
||||
}
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
const data = await enrollFactorMutation.mutateAsync(factorName);
|
||||
|
||||
if (!data) {
|
||||
return setError(true);
|
||||
}
|
||||
|
||||
// set image
|
||||
setFactor((factor) => {
|
||||
return {
|
||||
...factor,
|
||||
qrCode: data.totp.qr_code,
|
||||
};
|
||||
});
|
||||
|
||||
// dispatch event to set factor ID
|
||||
onSetFactorId(data.id);
|
||||
} catch (e) {
|
||||
setError(true);
|
||||
}
|
||||
})();
|
||||
}, [onSetFactorId, factorName, enrollFactorMutation]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className={'flex w-full flex-col space-y-2'}>
|
||||
<Alert variant={'destructive'}>
|
||||
<Trans i18nKey={'account:qrCodeError'} />
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!factorName) {
|
||||
return (
|
||||
<FactorNameForm
|
||||
onCancel={onCancel}
|
||||
onSetFactorName={(name) => {
|
||||
setFactor((factor) => ({ ...factor, name }));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'flex flex-col space-y-4'}>
|
||||
<p>
|
||||
<span className={'text-base'}>
|
||||
<Trans i18nKey={'account:multiFactorModalHeading'} />
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<div className={'flex justify-center'}>
|
||||
<QrImage src={factor.qrCode} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FactorNameForm(
|
||||
props: React.PropsWithChildren<{
|
||||
onSetFactorName: (name: string) => void;
|
||||
onCancel: () => void;
|
||||
}>,
|
||||
) {
|
||||
const inputName = 'factorName';
|
||||
|
||||
return (
|
||||
<form
|
||||
className={'w-full'}
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const data = new FormData(event.currentTarget);
|
||||
const name = data.get(inputName) as string;
|
||||
|
||||
props.onSetFactorName(name);
|
||||
}}
|
||||
>
|
||||
<div className={'flex flex-col space-y-4'}>
|
||||
<Label>
|
||||
<Trans i18nKey={'account:factorNameLabel'} />
|
||||
|
||||
<Input autoComplete={'off'} required name={inputName} />
|
||||
|
||||
<span>
|
||||
<Trans i18nKey={'account:factorNameHint'} />
|
||||
</span>
|
||||
</Label>
|
||||
|
||||
<div className={'flex justify-end space-x-2'}>
|
||||
<Button type={'submit'}>
|
||||
<Trans i18nKey={'account:factorNameSubmitLabel'} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
function QrImage({ src }: { src: string }) {
|
||||
return <Image alt={'QR Code'} src={src} width={160} height={160} />;
|
||||
}
|
||||
|
||||
export default MultiFactorAuthSetupModal;
|
||||
|
||||
function useEnrollFactor() {
|
||||
const client = useSupabase();
|
||||
const mutationKey = useFactorsMutationKey();
|
||||
|
||||
const mutationFn = async (factorName: string) => {
|
||||
const { data, error } = await client.auth.mfa.enroll({
|
||||
friendlyName: factorName,
|
||||
factorType: 'totp',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
return useMutation({
|
||||
mutationFn,
|
||||
mutationKey,
|
||||
});
|
||||
}
|
||||
|
||||
function useVerifyCodeMutation() {
|
||||
const mutationKey = useFactorsMutationKey();
|
||||
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 });
|
||||
}
|
||||
@@ -25,7 +25,7 @@ import { Input } from '@kit/ui/input';
|
||||
import { Label } from '@kit/ui/label';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { PasswordUpdateSchema } from '../../schema/update-password.schema';
|
||||
import { PasswordUpdateSchema } from '../../../schema/update-password.schema';
|
||||
|
||||
export const UpdatePasswordForm = ({
|
||||
user,
|
||||
Reference in New Issue
Block a user