DEV Community

Daanish2003
Daanish2003

Posted on

Forgot and Reset Password using better_auth, nextjs and resend

In this blog we will dive into setting up forgot and reset password in a mordern web application. By leveraging a robust stack of tools and frameworks, you'll create a seamless and secure email verification system integrated into your full-stack application.

By the end of this guide, you'll have a functional system that allows user to create new password if they forgot their password

Tech Stack

Here’s what we’ll use:

  • Better_Auth v1: A lightweight, extensible TypeScript authentication library.
  • Next.js: A React framework for building server-rendered applications.
  • Prisma: A modern ORM for efficient database interaction.
  • Shadcn: A utility-first component library for rapid UI development.
  • TailwindCSS: A popular utility-first CSS framework.
  • Resend: A popular service for sending emails

Prerequisites

Before proceeding, ensure you have the following ready:

  1. Node.js (LTS version) installed.
  2. A package manager like npm, yarn, or pnpm (we'll use pnpm in this guide).
  3. A PostgreSQL database instance (local or hosted, such as Supabase or PlanetScale).
    • If you're working locally, Docker is a great way to set this up.
  4. Familiarity with TypeScript, Next.js, and Prisma.

Cloning the Starter Project:

Note: This project is continuation of EmailPassword auth and Email verification. If want to build from stratch please visit EmailPassword blog and Email verification blog or could just clone the branch of the repository

git clone -b feat-email-verification https://github.com/Daanish2003/better_auth_nextjs.git  
Enter fullscreen mode Exit fullscreen mode

Navigate into the folder and install all the dependencies

pnpm install  
Enter fullscreen mode Exit fullscreen mode

Add .env file

# General authentication settings
BETTER_AUTH_SECRET="your-secret-key" # Replace with your own secure key or generate one using "pnpm dlx auth secret"
BETTER_AUTH_URL="http://localhost:3000" # Base URL of your app during local development
NEXT_PUBLIC_APP_URL="http://localhost:3000" # Public-facing base URL of your app
#.env
POSTGRES_PASSWORD="your-password"
POSTGRES_USER="your-username"
DATABASE_URL="postgresql://your-username:your-password@localhost:5432/mydb?schema=public"
RESEND_API_KEY="your-resend-api-key"
Enter fullscreen mode Exit fullscreen mode

Run the docker if you have docker locally

docker compose up -d
Enter fullscreen mode Exit fullscreen mode

Step 1: Update auth.ts file

Open src/lib/auth.ts and configure sendResetPassword

export const auth = betterAuth({
  // other options
  emailAndPassword: {
    enabled: true,
    autoSignIn: true,
    minPasswordLength: 8,
    maxPasswordLength: 20,
    requireEmailVerification: true,
    // it sends the reset password token using resend to your email
    sendResetPassword: async ({ user, url }) => {
      await resend.emails.send({
        from: "Acme <onboarding@resend.dev>",
        to: user.email,
        subject: "Reset your password",
        html: `Click the link to reset your password: ${url}`,
      });
    },
  },
  // other options
})
Enter fullscreen mode Exit fullscreen mode

Step 2: Create Schema for validation for forgot and reset password

Create a file named forgot-password-schema.tsx inside the folder src/helpers/zod

paste the code inside the file

import { z } from "zod";

export const ForgotPasswordSchema = z.object({
    email: z
    .string() // string type
    .email({message: "Invalid type"}) // checks if the input given by the user is email
    .min(1, {message: "Email is required"}), // checks if the email field is empty or not 
})
Enter fullscreen mode Exit fullscreen mode

Then create another file named reset-password-schema.tsx inside the same folder src/helpers/zod

import { z } from "zod";

export const ResetPasswordSchema = z.object({
    password: z
    .string() // check if it is string type
    .min(8, { message: "Password must be at least 8 characters long" }) // checks for character length
    .max(20, { message: "Password must be at most 20 characters long" }),
  confirmPassword: z
    .string()
    .min(8, { message: "Password must be at least 8 characters long" })
    .max(20, { message: "Password must be at most 20 characters long" }),
}).refine((data) => data.password === data.confirmPassword, {
    message: "Passwords do not match",
    path: ["confirmPassword"],

    // checks if the password and confirm password are equal
});
Enter fullscreen mode Exit fullscreen mode

Step 3: Create UI components for forgot and reset password

Create a component named forgot-password.tsx inside the component folder or any other location.

Paste the code inside of it

"use client"
import { useForm } from 'react-hook-form'
import CardWrapper from '../card-wrapper'
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../ui/form'
import { z } from 'zod'
import { zodResolver } from '@hookform/resolvers/zod'
import { Input } from '../ui/input'
import { Button } from '../ui/button'
import FormError from '../form-error'
import { FormSuccess } from '../form-success'
import { useAuthState } from '@/hooks/useAuthState'
import { authClient } from '@/lib/auth-client'
import { ForgotPasswordSchema } from '@/helpers/zod/forgot-password-schema'

const ForgotPassword = () => {
  const { error, success, loading, setError, setSuccess, setLoading, resetState } = useAuthState()

  const form = useForm<z.infer<typeof ForgotPasswordSchema>>({
    resolver: zodResolver(ForgotPasswordSchema),
    defaultValues: {
      email: '',
    }
  })

  const onSubmit = async (values: z.infer<typeof ForgotPasswordSchema>) => {
    try {
        // Call the authClient's forgetPassword method, passing the email and a redirect URL.
        await authClient.forgetPassword({
        email: values.email, // Email to which the reset password link should be sent.
        redirectTo: "/reset-password" // URL to redirect the user after resetting the password.
      }, {
        // Lifecycle hooks to handle different stages of the request.
        onResponse: () => {
          setLoading(false)
        },
        onRequest: () => {
          resetState()
          setLoading(true)
        },
        onSuccess: () => {
          setSuccess("Reset password link has been sent")
        },
        onError: (ctx) => {
          setError(ctx.error.message);
        },
      });

    } catch (error) { // catch the error
      console.log(error)
      setError("Something went wrong")
    }
  }

  return (
    <CardWrapper
      cardTitle='Forgot Password'
      cardDescription='Enter your email to send link to reset password'
      cardFooterDescription="Remember your password?"
      cardFooterLink='/signin'
      cardFooterLinkTitle='Signin'
    >
      <Form {...form}>
        <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
          <FormField
            control={form.control}
            name="email"
            render={({ field }) => (
              <FormItem>
                <FormLabel>Email</FormLabel>
                <FormControl>
                  <Input
                    disabled={loading}
                    type="email"
                    placeholder='example@gmail.com'
                    {...field}
                  />
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />
          <FormError message={error} />
          <FormSuccess message={success} />
          <Button disabled={loading} type="submit" className='w-full'>Submit</Button>
        </form>
      </Form>

    </CardWrapper>
  )
}

export default ForgotPassword
Enter fullscreen mode Exit fullscreen mode

Then Create an another component named reset-password.tsx inside the component folder or any other location.

Paste the code inside of it

"use client"
import React from 'react'
import { Button } from '../ui/button'
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../ui/form'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
import { zodResolver } from '@hookform/resolvers/zod'
import { Input } from '../ui/input'
import { useRouter } from 'next/navigation'
import CardWrapper from '../card-wrapper'
import FormError from '../form-error'
import { FormSuccess } from '../form-success'
import { ResetPasswordSchema } from '@/helpers/zod/reset-password-schema'
import { authClient } from '@/lib/auth-client'
import { useAuthState } from '@/hooks/useAuthState'

const ResetPassword = () => {
    const router = useRouter()
    const { error, success, loading, setError, setLoading, setSuccess, resetState } = useAuthState()

    const form = useForm<z.infer<typeof ResetPasswordSchema>>({
        resolver: zodResolver(ResetPasswordSchema),
        defaultValues: {
            password: '',
            confirmPassword: ''
        }
    })

    const onSubmit = async (values: z.infer<typeof ResetPasswordSchema>) => {
        try {
            // Call the authClient's reset password method, passing the email and a redirect URL.
            await authClient.resetPassword({
                newPassword: values.password, // new password given by user
            }, {
                onResponse: () => {
                    setLoading(false)
                },
                onRequest: () => {
                    resetState()
                    setLoading(true)
                },
                onSuccess: () => {
                    setSuccess("New password has been created")
                    router.replace('/signin')
                },
                onError: (ctx) => {
                    setError(ctx.error.message);
                },
            });
        } catch (error) { // catches the error
            console.log(error)
            setError("Something went wrong")
        }

    }

    return (
        <CardWrapper
            cardTitle='Reset Password'
            cardDescription='create a new password'
            cardFooterLink='/signin'
            cardFooterDescription='Remember your password?'
            cardFooterLinkTitle='Signin'
        >
            <Form {...form}>
                <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
                    <FormField
                        control={form.control}
                        name="password"
                        render={({ field }) => (
                            <FormItem>
                                <FormLabel>Password</FormLabel>
                                <FormControl>
                                    <Input
                                        disabled={loading}
                                        type="password"
                                        placeholder='************'
                                        {...field} />
                                </FormControl>
                                <FormMessage />
                            </FormItem>
                        )}
                    />
                    <FormField
                        control={form.control}
                        name="confirmPassword"
                        render={({ field }) => (
                            <FormItem>
                                <FormLabel>Confirm Password</FormLabel>
                                <FormControl>
                                    <Input
                                        disabled={loading}
                                        type="password"
                                        placeholder='*************'
                                        {...field}
                                    />
                                </FormControl>
                                <FormMessage />
                            </FormItem>
                        )}
                    />
                    <FormError message={error} />
                    <FormSuccess message={success} />
                    <Button type="submit" className="w-full" disabled={loading}>Submit</Button>
                </form>
            </Form>
        </CardWrapper>
    )
}

export default ResetPassword
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • UI and Form Utilities:
    • Components like CardWrapper, Form, Input, and Button are used to construct the form UI.
    • FormError and FormSuccess display error or success messages.
  • Form Handling:
    • useForm from react-hook-form handles form state and validation.
    • zodResolver integrates Zod schema validation with React Hook Form.
  • Authentication Utilities:
    • useAuthState manages the error, success, and loading states for the authentication process.
    • authClient facilitates server-side password reset requests.
  • Schema:
    • ResetPasswordSchema and ForgotPasswordSchemadefines the expected shape of the form data (email or password) and validation rules using Zod.

Step 4: Create a page.tsxfile for both forgot-password and reset-password

Create a folder named forgot-password and reset-password inside the src/app/(auth)/ and create page.tsx file inside of it.

Import the Forgot Password and Reset Password component

// app/(auth)/forgot-password/page.tsx
import ForgotPassword from '@/components/auth/forgot-password'
import React from 'react'

const ForgotPasswordPage = () => {
  return (
    <ForgotPassword />
  )
}

export default ForgotPasswordPage
Enter fullscreen mode Exit fullscreen mode
// app/(auth)/reset-password/page.tsx
import ResetPassword from '@/components/auth/reset-password'
import React from 'react'

const ResetPasswordPage = () => {
  return (
    <ResetPassword />
  )
}

export default ResetPasswordPage
Enter fullscreen mode Exit fullscreen mode

Step 4: Update Signincomponent

Open the sign-in.tsx file and then update with following code

// components/auth/sign-in.tsx
<FormField
    control={form.control}
    name="password"
    render={({ field }) => (
    <FormItem>
     <FormLabel>Password</FormLabel>
     <FormControl>
      <Input
        disabled={loading}
        type="password"
        placeholder="********"
        {...field}
         >
         </FormControl>
         <FormMessage />
         // Just add link below so user could redirect to forgot-password-url
         <Link href="/forgot-password" className="text-xs underline ml-60">
           Forgot Password?
          </Link>
                            </FormItem>
                        )}
                    />
Enter fullscreen mode Exit fullscreen mode

Add the Link component below the Form Message component

Step 5: Run Your Application

Start your development server:

pnpm dev

Enter fullscreen mode Exit fullscreen mode
  • Access your application at http://localhost:3000/forgot-password to test forgot-password.
  • Check your inbox for the reset-password link.
  • Log in at http://localhost:3000/signin once new password is created.

Image description

Image description

Image description

Final Thoughts

Congratulations! You have successfully integrating Forgot Password and Reset Password into your application, providing users with a seamless and secure authentication experience. 🎉

If you enjoyed this blog, please consider following and supporting me! Your encouragement motivates me to create more helpful content for you. 😊

Reference Links:

Email Verification Blog: https://dev.to/daanish2003/email-verification-using-betterauth-nextjs-and-resend-37gn

Email And Password with Better_Auth: https://dev.to/daanish2003/email-and-password-auth-using-betterauth-nextjs-prisma-shadcn-and-tailwindcss-hgc

OAuth Blog: https://dev.to/daanish2003/oauth-using-betterauth-nextjs-prisma-shadcn-and-tailwindcss-45bp

Better_Auth Docs: https://www.better-auth.com/

pnpm Docs: https://pnpm.io/

Docker Docs: https://docs.docker.com/

Prisma Docs: https://www.prisma.io/docs/getting-started

Shadcn Docs: https://ui.shadcn.com/

Next.js Docs: https://nextjs.org/

Tailwindcss Docs: https://tailwindcss.com/

Github repository: https://github.com/Daanish2003/better_auth_nextjs

Top comments (0)