| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201 |
- import HCaptcha from '@hcaptcha/react-hcaptcha'
- import { zodResolver } from '@hookform/resolvers/zod'
- import { useRouter } from 'next/router'
- import { useRef, useState } from 'react'
- import { SubmitHandler, useForm } from 'react-hook-form'
- import { toast } from 'sonner'
- import { Button, Form, FormControl, FormField, Input } from 'ui'
- import { Admonition } from 'ui-patterns'
- import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
- import * as z from 'zod'
- import { useResetPasswordMutation } from '@/data/misc/reset-password-mutation'
- import { BASE_PATH } from '@/lib/constants'
- import { auth } from '@/lib/gotrue'
- const forgotPasswordSchema = z.object({
- email: z.string().min(1, 'Please provide an email address').email('Must be a valid email'),
- })
- const codeSchema = z.object({
- code: z.string().regex(/^\d{6}$/, 'Code must be 6 digits'),
- })
- type ForgotPasswordFormData = z.infer<typeof forgotPasswordSchema>
- type CodeFormData = z.infer<typeof codeSchema>
- export const ForgotPasswordWizard = () => {
- const [email, setEmail] = useState('')
- if (email) {
- return <ConfirmResetCodeForm email={email} />
- }
- return <ForgotPasswordForm onSuccess={(email) => setEmail(email)} />
- }
- const ConfirmResetCodeForm = ({ email }: { email: string }) => {
- const router = useRouter()
- const [isLoading, setIsLoading] = useState(false)
- const codeForm = useForm<CodeFormData>({
- resolver: zodResolver(codeSchema as any),
- defaultValues: { code: '' },
- })
- const onCodeEntered: SubmitHandler<CodeFormData> = async (data) => {
- setIsLoading(true)
- const {
- data: { user },
- error,
- } = await auth.verifyOtp({ email, token: data.code, type: 'recovery' })
- // This fixes a race condition where the user is redirected to the reset password page without the session being set
- // which causes the user to be redirected to /sign-in page even though he's signed in
- await new Promise((resolve) => setTimeout(resolve, 1000))
- if (error) {
- setIsLoading(false)
- toast.error(`Failed to verify code: ${error.message}`)
- } else {
- if (user?.factors?.length) {
- await router.push({
- pathname: '/forgot-password-mfa',
- query: router.query,
- })
- } else {
- await router.push({
- pathname: '/reset-password',
- query: router.query,
- })
- }
- }
- }
- return (
- <Form {...codeForm}>
- <form
- id="code-input-form"
- className="flex flex-col pt-4 space-y-4"
- onSubmit={codeForm.handleSubmit(onCodeEntered)}
- >
- <Admonition
- type="default"
- title="Check your email for a reset code"
- description="You'll receive an email if an account associated with the email address exists"
- />
- <FormField
- control={codeForm.control}
- name="code"
- render={({ field }) => (
- <FormItemLayout label="Code">
- <FormControl>
- <Input {...field} placeholder="123456" autoComplete="off" disabled={isLoading} />
- </FormControl>
- </FormItemLayout>
- )}
- />
- <div className="border-t border-overlay-border" />
- <Button block form="code-input-form" htmlType="submit" size="medium" loading={isLoading}>
- Confirm reset code
- </Button>
- </form>
- </Form>
- )
- }
- const ForgotPasswordForm = ({ onSuccess }: { onSuccess: (email: string) => void }) => {
- const captchaRef = useRef<HCaptcha>(null)
- const [captchaToken, setCaptchaToken] = useState<string | null>(null)
- const forgotPasswordForm = useForm<ForgotPasswordFormData>({
- resolver: zodResolver(forgotPasswordSchema as any),
- defaultValues: { email: '' },
- })
- const { mutate: resetPassword, isPending } = useResetPasswordMutation({
- onSuccess: () => {
- onSuccess(forgotPasswordForm.getValues('email'))
- },
- onError: (error) => {
- setCaptchaToken(null)
- captchaRef.current?.resetCaptcha()
- toast.error(`Failed to send reset email: ${error.message}`)
- },
- })
- const onForgotPassword: SubmitHandler<ForgotPasswordFormData> = async (data) => {
- let token = captchaToken
- if (!token) {
- const captchaResponse = await captchaRef.current?.execute({ async: true })
- token = captchaResponse?.response ?? null
- }
- resetPassword({
- email: data.email,
- hcaptchaToken: token,
- redirectTo: `${
- process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview'
- ? location.origin
- : process.env.NEXT_PUBLIC_SITE_URL
- }${BASE_PATH}/reset-password`,
- })
- }
- return (
- <Form {...forgotPasswordForm}>
- <form
- id="forgot-password-form"
- className="flex flex-col pt-4 space-y-4"
- onSubmit={forgotPasswordForm.handleSubmit(onForgotPassword)}
- >
- <FormField
- control={forgotPasswordForm.control}
- name="email"
- render={({ field }) => (
- <FormItemLayout label="Email">
- <FormControl>
- <Input
- {...field}
- type="email"
- placeholder="you@example.com"
- disabled={isPending}
- autoComplete="email"
- />
- </FormControl>
- </FormItemLayout>
- )}
- />
- <div className="self-center">
- <HCaptcha
- ref={captchaRef}
- sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY!}
- size="invisible"
- onVerify={(token) => {
- setCaptchaToken(token)
- }}
- onExpire={() => {
- setCaptchaToken(null)
- }}
- />
- </div>
- <div className="border-t border-overlay-border" />
- <Button
- block
- form="forgot-password-form"
- htmlType="submit"
- size="medium"
- disabled={isPending}
- loading={isPending}
- >
- Send reset code
- </Button>
- </form>
- </Form>
- )
- }
|