diff --git a/apps/e2e/tests/authentication/auth.po.ts b/apps/e2e/tests/authentication/auth.po.ts index 21c3a6176..6d1f98430 100644 --- a/apps/e2e/tests/authentication/auth.po.ts +++ b/apps/e2e/tests/authentication/auth.po.ts @@ -80,4 +80,10 @@ export class AuthPageObject { await this.visitConfirmEmailLink(email); } + + async updatePassword(password: string) { + await this.page.fill('[name="password"]', password); + await this.page.fill('[name="repeatPassword"]', password); + await this.page.click('[type="submit"]'); + } } diff --git a/apps/e2e/tests/authentication/password-reset.spec.ts b/apps/e2e/tests/authentication/password-reset.spec.ts new file mode 100644 index 000000000..037f4c581 --- /dev/null +++ b/apps/e2e/tests/authentication/password-reset.spec.ts @@ -0,0 +1,48 @@ +import { expect, test } from '@playwright/test'; + +import { AuthPageObject } from './auth.po'; + +const email = 'owner@makerkit.dev'; +const newPassword = (Math.random() * 10000).toString(); + +test.describe('Password Reset Flow', () => { + test.describe.configure({ mode: 'serial' }); + + test('will reset the password and sign in with new one', async ({ page }) => { + const auth = new AuthPageObject(page); + + await page.goto('/auth/password-reset'); + + await page.fill('[name="email"]', email); + await page.click('[type="submit"]'); + + await auth.visitConfirmEmailLink(email); + + await page.waitForURL('/update-password'); + + await auth.updatePassword(newPassword); + + await page + .locator('a', { + hasText: 'Back to Home Page', + }) + .click(); + + await page.waitForURL('/home'); + + await auth.signOut(); + + await page + .locator('a', { + hasText: 'Sign in', + }) + .click(); + + await auth.signIn({ + email, + password: newPassword, + }); + + await page.waitForURL('/home'); + }); +}); diff --git a/apps/web/app/update-password/page.tsx b/apps/web/app/update-password/page.tsx index fea8e33d5..a06007608 100644 --- a/apps/web/app/update-password/page.tsx +++ b/apps/web/app/update-password/page.tsx @@ -15,11 +15,13 @@ export const generateMetadata = async () => { }; }; +const Logo = () => ; + async function UpdatePasswordPage() { await requireUserInServerComponent(); return ( - + ); diff --git a/packages/features/auth/src/components/password-reset-request-container.tsx b/packages/features/auth/src/components/password-reset-request-container.tsx index f3adb6e40..23a5ec47e 100644 --- a/packages/features/auth/src/components/password-reset-request-container.tsx +++ b/packages/features/auth/src/components/password-reset-request-container.tsx @@ -20,6 +20,7 @@ import { If } from '@kit/ui/if'; import { Input } from '@kit/ui/input'; import { Trans } from '@kit/ui/trans'; +import { useCaptchaToken } from '../captcha/client'; import { AuthErrorAlert } from './auth-error-alert'; const PasswordResetSchema = z.object({ @@ -31,6 +32,8 @@ export function PasswordResetRequestContainer(params: { }) { const { t } = useTranslation('auth'); const resetPasswordMutation = useRequestResetPassword(); + const { captchaToken, resetCaptchaToken } = useCaptchaToken(); + const error = resetPasswordMutation.error; const success = resetPasswordMutation.data; @@ -55,11 +58,20 @@ export function PasswordResetRequestContainer(params: {
{ - return resetPasswordMutation.mutateAsync({ - email, - redirectTo: new URL(params.redirectPath, window.location.origin) - .href, - }); + const redirectTo = new URL( + params.redirectPath, + window.location.origin, + ).href; + + return resetPasswordMutation + .mutateAsync({ + email, + redirectTo, + captchaToken, + }) + .catch(() => { + resetCaptchaToken(); + }); })} className={'w-full'} > diff --git a/packages/supabase/src/auth-callback.service.ts b/packages/supabase/src/auth-callback.service.ts index 490542029..c68fdd6e7 100644 --- a/packages/supabase/src/auth-callback.service.ts +++ b/packages/supabase/src/auth-callback.service.ts @@ -48,11 +48,12 @@ class AuthCallbackService { const token_hash = searchParams.get('token_hash'); const type = searchParams.get('type') as EmailOtpType | null; - const callbackParam = searchParams.get('callback'); + const callbackParam = searchParams.get('next') ?? searchParams.get('callback'); let nextPath: string | null = null; const callbackUrl = callbackParam ? new URL(callbackParam) : null; + // if we have a callback url, we check if it has a next path if (callbackUrl) { // if we have a callback url, we check if it has a next path const callbackNextPath = callbackUrl.searchParams.get('next'); diff --git a/packages/supabase/src/hooks/use-request-reset-password.ts b/packages/supabase/src/hooks/use-request-reset-password.ts index a4a4b2fa8..113782d92 100644 --- a/packages/supabase/src/hooks/use-request-reset-password.ts +++ b/packages/supabase/src/hooks/use-request-reset-password.ts @@ -2,9 +2,10 @@ import { useMutation } from '@tanstack/react-query'; import { useSupabase } from './use-supabase'; -interface Params { +interface RequestPasswordResetMutationParams { email: string; redirectTo: string; + captchaToken?: string; } /** @@ -18,11 +19,12 @@ export function useRequestResetPassword() { const client = useSupabase(); const mutationKey = ['auth', 'reset-password']; - const mutationFn = async (params: Params) => { + const mutationFn = async (params: RequestPasswordResetMutationParams) => { const { error, data } = await client.auth.resetPasswordForEmail( params.email, { redirectTo: params.redirectTo, + captchaToken: params.captchaToken, }, );