MFA: display meaningful errors
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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>);
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user