MFA: display meaningful errors

This commit is contained in:
gbuomprisco
2024-10-12 04:35:03 +02:00
parent 1ee6d8c669
commit f2b74a9c7e
5 changed files with 60 additions and 25 deletions

View File

@@ -26,7 +26,7 @@ export default async function RootLayout({
{children}
</RootProviders>
<Toaster richColors={false} />
<Toaster richColors={true} theme={theme} position="top-center" />
</body>
</html>
);
@@ -52,7 +52,7 @@ function getClassName(theme?: string) {
}
function getTheme() {
return cookies().get('theme')?.value;
return cookies().get('theme')?.value as 'light' | 'dark' | 'system';
}
export const generateMetadata = generateRootMetadata;

View File

@@ -39,7 +39,7 @@ const authConfig = AuthConfigSchema.parse({
// in your production project
providers: {
password: process.env.NEXT_PUBLIC_AUTH_PASSWORD === 'true',
magicLink: process.env.NEXT_PUBLIC_AUTH_MAGIC_LINK === 'true',
magicLink: true,
oAuth: ['google'],
},
} satisfies z.infer<typeof AuthConfigSchema>);

View File

@@ -76,6 +76,7 @@
"passwordsDoNotMatch": "The passwords do not match",
"minPasswordNumbers": "Password must contain at least one number",
"minPasswordSpecialChars": "Password must contain at least one special character",
"uppercasePassword": "Password must contain at least one uppercase letter"
"uppercasePassword": "Password must contain at least one uppercase letter",
"insufficient_aal": "Please sign-in with your current multi-factor authentication to perform this action"
}
}

View File

@@ -150,14 +150,25 @@ function ConfirmUnenrollFactorModal(
(factorId: string) => {
if (unEnroll.isPending) return;
const promise = unEnroll.mutateAsync(factorId).then(() => {
const promise = unEnroll.mutateAsync(factorId).then((response) => {
props.setIsModalOpen(false);
if (!response.success) {
const errorCode = response.data;
throw t(`auth:errors.${errorCode}`, {
defaultValue: t(`account:unenrollFactorError`)
});
}
});
toast.promise(promise, {
loading: t(`account:unenrollingFactor`),
success: t(`account:unenrollFactorSuccess`),
error: t(`account:unenrollFactorError`),
error: (error: string) => {
return error;
},
duration: Infinity
});
},
[props, t, unEnroll],
@@ -279,16 +290,22 @@ function useUnenrollFactor(userId: string) {
});
if (error) {
throw error;
return {
success: false as const,
data: error.code as string,
}
}
return data;
return {
success: true as const,
data,
}
};
return useMutation({
mutationFn,
mutationKey,
onSuccess: async () => {
onSuccess: () => {
return queryClient.refetchQueries({
queryKey: mutationKey,
});

View File

@@ -6,7 +6,7 @@ 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 { useMutation, useQueryClient } from '@tanstack/react-query';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';
@@ -43,6 +43,7 @@ import {
import { Trans } from '@kit/ui/trans';
import { refreshAuthSession } from '../../../server/personal-accounts-server-actions';
import {ArrowLeftIcon} from "lucide-react";
export function MultiFactorAuthSetupDialog(props: { userId: string }) {
const { t } = useTranslation();
@@ -252,7 +253,8 @@ function FactorQrCode({
onSetFactorId: (factorId: string) => void;
}>) {
const enrollFactorMutation = useEnrollFactor(userId);
const [error, setError] = useState(false);
const { t } = useTranslation();
const [error, setError] = useState<string>('');
const form = useForm({
resolver: zodResolver(
@@ -280,9 +282,16 @@ function FactorQrCode({
</AlertTitle>
<AlertDescription>
<Trans i18nKey={'account:qrCodeErrorDescription'} />
<Trans i18nKey={`auth:errors.${error}`} defaults={t('account:qrCodeErrorDescription')} />
</AlertDescription>
</Alert>
<div>
<Button variant={'outline'} onClick={onCancel}>
<ArrowLeftIcon className={'h-4'} />
<Trans i18nKey={`common:retry`} />
</Button>
</div>
</div>
);
}
@@ -292,18 +301,14 @@ function FactorQrCode({
<FactorNameForm
onCancel={onCancel}
onSetFactorName={async (name) => {
const data = await enrollFactorMutation
.mutateAsync(name)
.catch((error) => {
console.error(error);
const response = await enrollFactorMutation.mutateAsync(name);
return;
});
if (data === undefined) {
return setError(true);
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);
@@ -401,24 +406,36 @@ function QrImage({ src }: { src: string }) {
function useEnrollFactor(userId: string) {
const client = useSupabase();
const queryClient = useQueryClient();
const mutationKey = useFactorsMutationKey(userId);
const mutationFn = async (factorName: string) => {
const { data, error } = await client.auth.mfa.enroll({
const response = await client.auth.mfa.enroll({
friendlyName: factorName,
factorType: 'totp',
});
if (error) {
throw error;
if (response.error) {
return {
success: false as const,
data: response.error.code,
}
}
return data;
return {
success: true as const,
data: response.data,
}
};
return useMutation({
mutationFn,
mutationKey,
onSuccess() {
return queryClient.refetchQueries({
queryKey: mutationKey,
});
},
});
}