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

View File

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

View File

@@ -76,6 +76,7 @@
"passwordsDoNotMatch": "The passwords do not match", "passwordsDoNotMatch": "The passwords do not match",
"minPasswordNumbers": "Password must contain at least one number", "minPasswordNumbers": "Password must contain at least one number",
"minPasswordSpecialChars": "Password must contain at least one special character", "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) => { (factorId: string) => {
if (unEnroll.isPending) return; if (unEnroll.isPending) return;
const promise = unEnroll.mutateAsync(factorId).then(() => { const promise = unEnroll.mutateAsync(factorId).then((response) => {
props.setIsModalOpen(false); props.setIsModalOpen(false);
if (!response.success) {
const errorCode = response.data;
throw t(`auth:errors.${errorCode}`, {
defaultValue: t(`account:unenrollFactorError`)
});
}
}); });
toast.promise(promise, { toast.promise(promise, {
loading: t(`account:unenrollingFactor`), loading: t(`account:unenrollingFactor`),
success: t(`account:unenrollFactorSuccess`), success: t(`account:unenrollFactorSuccess`),
error: t(`account:unenrollFactorError`), error: (error: string) => {
return error;
},
duration: Infinity
}); });
}, },
[props, t, unEnroll], [props, t, unEnroll],
@@ -279,16 +290,22 @@ function useUnenrollFactor(userId: string) {
}); });
if (error) { if (error) {
throw error; return {
success: false as const,
data: error.code as string,
}
} }
return data; return {
success: true as const,
data,
}
}; };
return useMutation({ return useMutation({
mutationFn, mutationFn,
mutationKey, mutationKey,
onSuccess: async () => { onSuccess: () => {
return queryClient.refetchQueries({ return queryClient.refetchQueries({
queryKey: mutationKey, queryKey: mutationKey,
}); });

View File

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