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