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:
Giancarlo Buomprisco
2024-12-12 12:26:50 +01:00
committed by GitHub
parent ae9c33aea4
commit 97d2cf9f85
11 changed files with 268 additions and 71 deletions

View File

@@ -15,7 +15,8 @@
"./shared": "./src/shared.ts",
"./mfa": "./src/mfa.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": {
"@hookform/resolvers": "^3.9.1",

View File

@@ -1,60 +1,105 @@
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
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 { Alert, AlertDescription } from '@kit/ui/alert';
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
import { Button } from '@kit/ui/button';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
} from '@kit/ui/form';
import { Input } from '@kit/ui/input';
import { Label } from '@kit/ui/label';
import { Trans } from '@kit/ui/trans';
function ResendAuthLinkForm() {
import { useCaptchaToken } from '../captcha/client';
export function ResendAuthLinkForm(props: {
redirectPath?: string;
}) {
const resendLink = useResendLink();
const form = useForm({
resolver: zodResolver(z.object({ email: z.string().email() })),
defaultValues: {
email: '',
},
});
if (resendLink.data && !resendLink.isPending) {
return (
<Alert variant={'success'}>
<AlertTitle>
<Trans i18nKey={'auth:resendLinkSuccess'} />
</AlertTitle>
<AlertDescription>
<Trans i18nKey={'auth:resendLinkSuccess'} defaults={'Success!'} />
<Trans
i18nKey={'auth:resendLinkSuccessDescription'}
defaults={'Success!'}
/>
</AlertDescription>
</Alert>
);
}
return (
<form
className={'flex flex-col space-y-2'}
onSubmit={(data) => {
data.preventDefault();
<Form {...form}>
<form
className={'flex flex-col space-y-2'}
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);
}}
>
<Label>
<Trans i18nKey={'common:emailAddress'} />
<Input name={'email'} required placeholder={''} />
</Label>
<Button disabled={resendLink.isPending}>
<Trans i18nKey={'auth:resendLink'} defaults={'Resend Link'} />
</Button>
</form>
<Button disabled={resendLink.isPending}>
<Trans i18nKey={'auth:resendLink'} defaults={'Resend Link'} />
</Button>
</form>
</Form>
);
}
export default ResendAuthLinkForm;
function useResendLink() {
const supabase = useSupabase();
const { captchaToken } = useCaptchaToken();
const mutationKey = ['resend-link'];
const mutationFn = async (email: string) => {
const mutationFn = async (props: {
email: string;
redirectPath?: string;
}) => {
const response = await supabase.auth.resend({
email,
email: props.email,
type: 'signup',
options: {
emailRedirectTo: props.redirectPath,
captchaToken,
},
});
if (response.error) {
@@ -65,7 +110,6 @@ function useResendLink() {
};
return useMutation({
mutationKey,
mutationFn,
});
}

View File

@@ -2,6 +2,8 @@
import { useState, useTransition } from 'react';
import { isRedirectError } from 'next/dist/client/components/redirect-error';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
@@ -76,10 +78,16 @@ function CreateOrganizationAccountForm(props: { onClose: () => void }) {
data-test={'create-team-form'}
onSubmit={form.handleSubmit((data) => {
startTransition(async () => {
const { error } = await createTeamAccountAction(data);
try {
const { error } = await createTeamAccountAction(data);
if (error) {
setError(true);
if (error) {
setError(true);
}
} catch (error) {
if (!isRedirectError(error)) {
setError(true);
}
}
});
})}

View File

@@ -101,6 +101,30 @@ class AccountInvitationsService {
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
* @description Sends invitations to join a team.
@@ -123,6 +147,24 @@ class AccountInvitationsService {
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
.from('accounts')
.select('name')