Expired links (#94)
1. Handle expired links on signup 2.Reject invitations when user is already a member 3. Make sure not to display errors due to Next.js redirection during team creation 4. Fix documentation sidebar
This commit is contained in:
committed by
GitHub
parent
ae9c33aea4
commit
97d2cf9f85
@@ -57,11 +57,15 @@ export class InvitationsPageObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
navigateToMembers() {
|
navigateToMembers() {
|
||||||
return this.page
|
return expect(async () => {
|
||||||
.locator('a', {
|
await this.page
|
||||||
hasText: 'Members',
|
.locator('a', {
|
||||||
})
|
hasText: 'Members',
|
||||||
.click();
|
})
|
||||||
|
.click();
|
||||||
|
|
||||||
|
return expect(this.page.url()).toContain('members');
|
||||||
|
}).toPass()
|
||||||
}
|
}
|
||||||
|
|
||||||
async openInviteForm() {
|
async openInviteForm() {
|
||||||
|
|||||||
@@ -60,6 +60,31 @@ test.describe('Invitations', () => {
|
|||||||
'Owner',
|
'Owner',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('user cannot invite a member of the team again', async ({ page }) => {
|
||||||
|
await invitations.navigateToMembers();
|
||||||
|
|
||||||
|
const email = invitations.auth.createRandomEmail();
|
||||||
|
|
||||||
|
const invites = [
|
||||||
|
{
|
||||||
|
email,
|
||||||
|
role: 'member',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await invitations.openInviteForm();
|
||||||
|
await invitations.inviteMembers(invites);
|
||||||
|
|
||||||
|
await expect(invitations.getInvitations()).toHaveCount(1);
|
||||||
|
|
||||||
|
// Try to invite the same member again
|
||||||
|
// This should fail
|
||||||
|
await invitations.openInviteForm();
|
||||||
|
await invitations.inviteMembers(invites);
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
await expect(invitations.getInvitations()).toHaveCount(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('Full Invitation Flow', () => {
|
test.describe('Full Invitation Flow', () => {
|
||||||
|
|||||||
@@ -92,6 +92,10 @@ function Tree({
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pages.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarMenuSub>
|
<SidebarMenuSub>
|
||||||
{pages.map((treeNode, index) => (
|
{pages.map((treeNode, index) => (
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { redirect } from 'next/navigation';
|
|
||||||
|
|
||||||
|
import type { AuthError } from '@supabase/supabase-js';
|
||||||
|
|
||||||
|
import { ResendAuthLinkForm } from '@kit/auth/resend-email-link';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
@@ -11,41 +13,59 @@ import { withI18n } from '~/lib/i18n/with-i18n';
|
|||||||
interface AuthCallbackErrorPageProps {
|
interface AuthCallbackErrorPageProps {
|
||||||
searchParams: Promise<{
|
searchParams: Promise<{
|
||||||
error: string;
|
error: string;
|
||||||
invite_token: string;
|
callback?: string;
|
||||||
|
email?: string;
|
||||||
|
code?: AuthError['code'];
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function AuthCallbackErrorPage(props: AuthCallbackErrorPageProps) {
|
async function AuthCallbackErrorPage(props: AuthCallbackErrorPageProps) {
|
||||||
const { error, invite_token } = await props.searchParams;
|
const { error, callback, code } = await props.searchParams;
|
||||||
const queryParam = invite_token ? `?invite_token=${invite_token}` : '';
|
const signInPath = pathsConfig.auth.signIn;
|
||||||
const signInPath = pathsConfig.auth.signIn + queryParam;
|
const redirectPath = callback ?? pathsConfig.auth.callback;
|
||||||
|
|
||||||
// if there is no error, redirect the user to the sign-in page
|
|
||||||
if (!error) {
|
|
||||||
redirect(signInPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'flex flex-col space-y-4 py-4'}>
|
<div className={'flex flex-col space-y-4 py-4'}>
|
||||||
<div>
|
<Alert variant={'warning'}>
|
||||||
<Alert variant={'destructive'}>
|
<AlertTitle>
|
||||||
<AlertTitle>
|
<Trans i18nKey={'auth:authenticationErrorAlertHeading'} />
|
||||||
<Trans i18nKey={'auth:authenticationErrorAlertHeading'} />
|
</AlertTitle>
|
||||||
</AlertTitle>
|
|
||||||
|
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
<Trans i18nKey={error} />
|
<Trans i18nKey={error ?? 'auth:authenticationErrorAlertBody'} />
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button asChild>
|
<AuthCallbackForm
|
||||||
<Link href={signInPath}>
|
code={code}
|
||||||
<Trans i18nKey={'auth:signIn'} />
|
signInPath={signInPath}
|
||||||
</Link>
|
redirectPath={redirectPath}
|
||||||
</Button>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function AuthCallbackForm(props: {
|
||||||
|
signInPath: string;
|
||||||
|
redirectPath?: string;
|
||||||
|
code?: AuthError['code'];
|
||||||
|
}) {
|
||||||
|
switch (props.code) {
|
||||||
|
case 'otp_expired':
|
||||||
|
return <ResendAuthLinkForm redirectPath={props.redirectPath} />;
|
||||||
|
default:
|
||||||
|
return <SignInButton signInPath={props.signInPath} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function SignInButton(props: { signInPath: string }) {
|
||||||
|
return (
|
||||||
|
<Button className={'w-full'} asChild>
|
||||||
|
<Link href={props.signInPath}>
|
||||||
|
<Trans i18nKey={'auth:signIn'} />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default withI18n(AuthCallbackErrorPage);
|
export default withI18n(AuthCallbackErrorPage);
|
||||||
|
|||||||
@@ -52,7 +52,8 @@
|
|||||||
"emailConfirmationAlertHeading": "We sent you a confirmation email.",
|
"emailConfirmationAlertHeading": "We sent you a confirmation email.",
|
||||||
"emailConfirmationAlertBody": "Welcome! Please check your email and click the link to verify your account.",
|
"emailConfirmationAlertBody": "Welcome! Please check your email and click the link to verify your account.",
|
||||||
"resendLink": "Resend link",
|
"resendLink": "Resend link",
|
||||||
"resendLinkSuccess": "We sent you a new link to your email! Follow the link to sign in.",
|
"resendLinkSuccessDescription": "We sent you a new link to your email! Follow the link to sign in.",
|
||||||
|
"resendLinkSuccess": "Check your email!",
|
||||||
"authenticationErrorAlertHeading": "Authentication Error",
|
"authenticationErrorAlertHeading": "Authentication Error",
|
||||||
"authenticationErrorAlertBody": "Sorry, we could not authenticate you. Please try again.",
|
"authenticationErrorAlertBody": "Sorry, we could not authenticate you. Please try again.",
|
||||||
"sendEmailCode": "Get code to your Email",
|
"sendEmailCode": "Get code to your Email",
|
||||||
@@ -77,6 +78,7 @@
|
|||||||
"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"
|
"insufficient_aal": "Please sign-in with your current multi-factor authentication to perform this action",
|
||||||
|
"otp_expired": "The email link has expired. Please try again."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,6 +146,11 @@ class KeystaticClient implements CmsClient {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the parent is already set, we don't need to do anything
|
||||||
|
if (item.entry.parent !== null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (isIndexFile(item.slug)) {
|
if (isIndexFile(item.slug)) {
|
||||||
item.entry.parent = null;
|
item.entry.parent = null;
|
||||||
results[i] = item;
|
results[i] = item;
|
||||||
@@ -167,6 +172,7 @@ class KeystaticClient implements CmsClient {
|
|||||||
item.entry.parent = findClosestValidParent(pathParts);
|
item.entry.parent = findClosestValidParent(pathParts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
results[i] = item;
|
results[i] = item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,8 @@
|
|||||||
"./shared": "./src/shared.ts",
|
"./shared": "./src/shared.ts",
|
||||||
"./mfa": "./src/mfa.ts",
|
"./mfa": "./src/mfa.ts",
|
||||||
"./captcha/client": "./src/captcha/client/index.ts",
|
"./captcha/client": "./src/captcha/client/index.ts",
|
||||||
"./captcha/server": "./src/captcha/server/index.ts"
|
"./captcha/server": "./src/captcha/server/index.ts",
|
||||||
|
"./resend-email-link": "./src/components/resend-auth-link-form.tsx"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@hookform/resolvers": "^3.9.1",
|
"@hookform/resolvers": "^3.9.1",
|
||||||
|
|||||||
@@ -1,60 +1,105 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useMutation } from '@tanstack/react-query';
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
|
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
|
||||||
import { Alert, AlertDescription } from '@kit/ui/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
} from '@kit/ui/form';
|
||||||
import { Input } from '@kit/ui/input';
|
import { Input } from '@kit/ui/input';
|
||||||
import { Label } from '@kit/ui/label';
|
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
function ResendAuthLinkForm() {
|
import { useCaptchaToken } from '../captcha/client';
|
||||||
|
|
||||||
|
export function ResendAuthLinkForm(props: {
|
||||||
|
redirectPath?: string;
|
||||||
|
}) {
|
||||||
const resendLink = useResendLink();
|
const resendLink = useResendLink();
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
resolver: zodResolver(z.object({ email: z.string().email() })),
|
||||||
|
defaultValues: {
|
||||||
|
email: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (resendLink.data && !resendLink.isPending) {
|
if (resendLink.data && !resendLink.isPending) {
|
||||||
return (
|
return (
|
||||||
<Alert variant={'success'}>
|
<Alert variant={'success'}>
|
||||||
|
<AlertTitle>
|
||||||
|
<Trans i18nKey={'auth:resendLinkSuccess'} />
|
||||||
|
</AlertTitle>
|
||||||
|
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
<Trans i18nKey={'auth:resendLinkSuccess'} defaults={'Success!'} />
|
<Trans
|
||||||
|
i18nKey={'auth:resendLinkSuccessDescription'}
|
||||||
|
defaults={'Success!'}
|
||||||
|
/>
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<Form {...form}>
|
||||||
className={'flex flex-col space-y-2'}
|
<form
|
||||||
onSubmit={(data) => {
|
className={'flex flex-col space-y-2'}
|
||||||
data.preventDefault();
|
onSubmit={form.handleSubmit((data) => {
|
||||||
|
return resendLink.mutate({
|
||||||
|
email: data.email,
|
||||||
|
redirectPath: props.redirectPath,
|
||||||
|
});
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
<Trans i18nKey={'common:emailAddress'} />
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
const email = new FormData(data.currentTarget).get('email') as string;
|
<FormControl>
|
||||||
|
<Input type="email" required {...field} />
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
name={'email'}
|
||||||
|
/>
|
||||||
|
|
||||||
return resendLink.mutateAsync(email);
|
<Button disabled={resendLink.isPending}>
|
||||||
}}
|
<Trans i18nKey={'auth:resendLink'} defaults={'Resend Link'} />
|
||||||
>
|
</Button>
|
||||||
<Label>
|
</form>
|
||||||
<Trans i18nKey={'common:emailAddress'} />
|
</Form>
|
||||||
<Input name={'email'} required placeholder={''} />
|
|
||||||
</Label>
|
|
||||||
|
|
||||||
<Button disabled={resendLink.isPending}>
|
|
||||||
<Trans i18nKey={'auth:resendLink'} defaults={'Resend Link'} />
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ResendAuthLinkForm;
|
|
||||||
|
|
||||||
function useResendLink() {
|
function useResendLink() {
|
||||||
const supabase = useSupabase();
|
const supabase = useSupabase();
|
||||||
|
const { captchaToken } = useCaptchaToken();
|
||||||
|
|
||||||
const mutationKey = ['resend-link'];
|
const mutationFn = async (props: {
|
||||||
const mutationFn = async (email: string) => {
|
email: string;
|
||||||
|
redirectPath?: string;
|
||||||
|
}) => {
|
||||||
const response = await supabase.auth.resend({
|
const response = await supabase.auth.resend({
|
||||||
email,
|
email: props.email,
|
||||||
type: 'signup',
|
type: 'signup',
|
||||||
|
options: {
|
||||||
|
emailRedirectTo: props.redirectPath,
|
||||||
|
captchaToken,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
@@ -65,7 +110,6 @@ function useResendLink() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationKey,
|
|
||||||
mutationFn,
|
mutationFn,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
import { useState, useTransition } from 'react';
|
import { useState, useTransition } from 'react';
|
||||||
|
|
||||||
|
import { isRedirectError } from 'next/dist/client/components/redirect-error';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
@@ -76,10 +78,16 @@ function CreateOrganizationAccountForm(props: { onClose: () => void }) {
|
|||||||
data-test={'create-team-form'}
|
data-test={'create-team-form'}
|
||||||
onSubmit={form.handleSubmit((data) => {
|
onSubmit={form.handleSubmit((data) => {
|
||||||
startTransition(async () => {
|
startTransition(async () => {
|
||||||
const { error } = await createTeamAccountAction(data);
|
try {
|
||||||
|
const { error } = await createTeamAccountAction(data);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
setError(true);
|
setError(true);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (!isRedirectError(error)) {
|
||||||
|
setError(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -101,6 +101,30 @@ class AccountInvitationsService {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async validateInvitation(
|
||||||
|
invitation: z.infer<typeof InviteMembersSchema>['invitations'][number],
|
||||||
|
accountSlug: string,
|
||||||
|
) {
|
||||||
|
const { data: members, error } = await this.client.rpc(
|
||||||
|
'get_account_members',
|
||||||
|
{
|
||||||
|
account_slug: accountSlug,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isUserAlreadyMember = members.find((member) => {
|
||||||
|
return member.email === invitation.email;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isUserAlreadyMember) {
|
||||||
|
throw new Error('User already member of the team');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name sendInvitations
|
* @name sendInvitations
|
||||||
* @description Sends invitations to join a team.
|
* @description Sends invitations to join a team.
|
||||||
@@ -123,6 +147,24 @@ class AccountInvitationsService {
|
|||||||
|
|
||||||
logger.info(ctx, 'Storing invitations...');
|
logger.info(ctx, 'Storing invitations...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.all(
|
||||||
|
invitations.map((invitation) =>
|
||||||
|
this.validateInvitation(invitation, accountSlug),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
{
|
||||||
|
...ctx,
|
||||||
|
error: (error as Error).message,
|
||||||
|
},
|
||||||
|
'Error validating invitations',
|
||||||
|
);
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
const accountResponse = await this.client
|
const accountResponse = await this.client
|
||||||
.from('accounts')
|
.from('accounts')
|
||||||
.select('name')
|
.select('name')
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import 'server-only';
|
import 'server-only';
|
||||||
|
|
||||||
import { type EmailOtpType, SupabaseClient } from '@supabase/supabase-js';
|
|
||||||
|
|
||||||
|
import {AuthError, type EmailOtpType, SupabaseClient} from '@supabase/supabase-js';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name createAuthCallbackService
|
* @name createAuthCallbackService
|
||||||
@@ -105,6 +108,14 @@ class AuthCallbackService {
|
|||||||
if (!error) {
|
if (!error) {
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (error.code) {
|
||||||
|
url.searchParams.set('code', error.code);
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorMessage = getAuthErrorMessage({ error: error.message, code: error.code });
|
||||||
|
|
||||||
|
url.searchParams.set('error', errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
// return the user to an error page with some instructions
|
// return the user to an error page with some instructions
|
||||||
@@ -163,6 +174,7 @@ class AuthCallbackService {
|
|||||||
// if we have an error, we redirect to the error page
|
// if we have an error, we redirect to the error page
|
||||||
if (error) {
|
if (error) {
|
||||||
return onError({
|
return onError({
|
||||||
|
code: error.code,
|
||||||
error: error.message,
|
error: error.message,
|
||||||
path: errorPath,
|
path: errorPath,
|
||||||
});
|
});
|
||||||
@@ -179,6 +191,7 @@ class AuthCallbackService {
|
|||||||
const message = error instanceof Error ? error.message : error;
|
const message = error instanceof Error ? error.message : error;
|
||||||
|
|
||||||
return onError({
|
return onError({
|
||||||
|
code: (error as AuthError)?.code,
|
||||||
error: message as string,
|
error: message as string,
|
||||||
path: errorPath,
|
path: errorPath,
|
||||||
});
|
});
|
||||||
@@ -198,8 +211,16 @@ class AuthCallbackService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onError({ error, path }: { error: string; path: string }) {
|
function onError({
|
||||||
const errorMessage = getAuthErrorMessage(error);
|
error,
|
||||||
|
path,
|
||||||
|
code,
|
||||||
|
}: {
|
||||||
|
error: string;
|
||||||
|
path: string;
|
||||||
|
code?: string;
|
||||||
|
}) {
|
||||||
|
const errorMessage = getAuthErrorMessage({ error, code });
|
||||||
|
|
||||||
console.error(
|
console.error(
|
||||||
{
|
{
|
||||||
@@ -209,7 +230,12 @@ function onError({ error, path }: { error: string; path: string }) {
|
|||||||
`An error occurred while signing user in`,
|
`An error occurred while signing user in`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const nextPath = `${path}?error=${errorMessage}`;
|
const searchParams = new URLSearchParams({
|
||||||
|
error: errorMessage,
|
||||||
|
code: code ?? '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const nextPath = `${path}?${searchParams.toString()}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
nextPath,
|
nextPath,
|
||||||
@@ -227,8 +253,23 @@ function isVerifierError(error: string) {
|
|||||||
return error.includes('both auth code and code verifier should be non-empty');
|
return error.includes('both auth code and code verifier should be non-empty');
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAuthErrorMessage(error: string) {
|
function getAuthErrorMessage(params: {
|
||||||
return isVerifierError(error)
|
error: string;
|
||||||
? `auth:errors.codeVerifierMismatch`
|
code?: string;
|
||||||
: `auth:authenticationErrorAlertBody`;
|
}) {
|
||||||
|
// this error arises when the user tries to sign in with an expired email link
|
||||||
|
if (params.code) {
|
||||||
|
if (params.code === 'otp_expired') {
|
||||||
|
return 'auth:errors.otp_expired';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// this error arises when the user is trying to sign in with a different
|
||||||
|
// browser than the one they used to request the sign in link
|
||||||
|
if (isVerifierError(params.error)) {
|
||||||
|
return 'auth:errors.codeVerifierMismatch';
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback to the default error message
|
||||||
|
return `auth:authenticationErrorAlertBody`;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user