DEV Community

Daanish2003
Daanish2003

Posted on

Email and Password auth using Better_Auth, nextjs, prisma, shadcn and tailwindcss

In this blog post, we'll explore how to set up email and password authentication in a modern web application using a powerful stack that ensures efficiency, scalability, and developer productivity. By the end, you'll have a working email and password system integrated into a full-stack application.

Tech Stack:

  • Better_Auth v1: A lightweight and extensible authentication library for TypeScript.
  • Next.js: A React framework for building server-rendered web applications.
  • Prisma: A modern ORM for database management.
  • Shadcn: A utility-first component library.
  • TailwindCSS: A popular utility-first CSS framework.

Prerequisites:

Before we begin, make sure you have the following tools installed:

  • Node.js (LTS version)
  • npm, yarn, or pnpm (package managers)
  • A PostgreSQL database instance (local or hosted, such as Supabase or PlanetScale). I'll be using Docker for local development
  • Basic knowledge of TypeScript, Next.js, and Prisma.

Step 1: Setting up your Next.js project

Create a New Project Directory:

Begin by creating a folder for your project and navigating into it via the terminal. Use the following commands:

mkdir better_auth_nextjs_oauth
cd better_auth_nextjs_oauth
Enter fullscreen mode Exit fullscreen mode

Install Next.js in the Directory:

Initialize a new Next.js project within the folder using the command below. You can use your preferred package manager (e.g., npm, yarn, or pnpm). For this guide, we'll use pnpm.

pnpx create-next-app@latest .
Enter fullscreen mode Exit fullscreen mode

💡Tip: If you don't have pnpm installed, visit pnpm.io and follow the instructions for your operating system.

Configure the Project Options:

Follow the prompts displayed in the terminal to configure your project. You will be asked to make selections such as enabling TypeScript, which is recommended for better type safety and development experience.

Use the arrow keys to navigate through the options and choose according to your preferences.

Image description

Step 2: Installing Dependencies

Install the necessary packages for the tech stack by running the following commands:

pnpm add better-auth@latest
pnpm add -D prisma
pnpm add @prisma/client
pnpm install react-icons --save
pnpm add oslo
pnpm add zod
Enter fullscreen mode Exit fullscreen mode

To install shadcn-ui, a utility-first component library, run the following command:

pnpm dlx shadcn@latest init
Enter fullscreen mode Exit fullscreen mode

This will set up shadcn-ui in your project, allowing you to start using its components right away.

When prompted, choose from the available options to customize your shadcn-ui setup. Follow the instructions in the terminal to select the components and configurations that best suit your project needs.

Image description
To add reusable components from shadcn-ui, you can use the following command:

pnpx shadcn@latest add button card form input label
Enter fullscreen mode Exit fullscreen mode

Step 3: Setting Up the Database (Local or Cloud)

Option 1: Local Development with Docker:

💡Tip: If you're using a cloud-based database provider like Neon or PlanetScale, you can skip this step and proceed to configure your database connection directly through their provided URLs.

Create a docker-compose.yml File

To set up a PostgreSQL database using Docker, create a docker-compose.yml file in your project’s root directory. This file will define the configuration for your database container.

Here's an example configuration for the docker-compose.yml file:

# docker-compose.yml
version: '3.9'
services:
  db:
    container_name: BETTER_AUTH_NEXTJS_OAUTH
    image: postgres:17-alpine
    restart: always
    ports:
      - 5432:5432
    volumes:
      - postgres-data:/var/lib/postgresql/data
    environment:
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_USER=${POSTGRES_USER}
      - POSTGRES_DB=OAUTH
volumes:
  postgres-data:
Enter fullscreen mode Exit fullscreen mode

This configuration sets up a PostgreSQL container with a persistent volume and environment variables for your database credentials.

Set Up Environment Variables for the Database

Create a .env file in the root of your project and define the necessary environment variables for your database connection. Here's an example configuration:

//.env
POSTGRES_PASSWORD="your_password_here"
POSTGRES_USER="your_username_here"
DATABASE_URL="postgresql://your_username_here:your_password_here@localhost:5432/mydb?schema=public"
Enter fullscreen mode Exit fullscreen mode
  • Replace your_password_here and your_username_here with your own PostgreSQL credentials.
  • DATABASE_URL should point to your local database or a cloud database if you are using a provider.

Run the Docker Container

Once you’ve configured the docker-compose.yml file and set up the environment variables, you can start the Docker container by running the following command:

docker compose up -d
Enter fullscreen mode Exit fullscreen mode

This command will start the PostgreSQL container in detached mode (-d), ensuring your database is up and running in the background. You can now proceed with the rest of the setup in your Next.js application.

If you ever need to stop the container, you can use:

docker compose down
Enter fullscreen mode Exit fullscreen mode

Option 2: Using a Hosted Database

If you're using a cloud database provider (such as Neon, PlanetScale, or others), you can skip the local Docker setup. Instead, simply copy the provided Database URL from your cloud provider and add it to your .env file.

For example:

# .env
DATABASE_URL="postgresql://username:password@host:port/database_name?sslmode=require"
Enter fullscreen mode Exit fullscreen mode

Ensure that the DATABASE_URL contains the correct connection details such as your:

  • username
  • password
  • host (database server URL)
  • port (usually 5432 for PostgreSQL)
  • database_name

Once you've added the correct URL, your application will be able to connect to the hosted database seamlessly.

Step 4: Setting Up Prisma

Initialize Prisma CLI

This will create a new prisma folder in your project, containing the schema.prisma file and configuration for Prisma.

pnpm dlx prisma init
Enter fullscreen mode Exit fullscreen mode

This will create a new prisma folder in your project, containing the schema.prisma file and configuration for Prisma.

Configure the Database Connection:

Open the prisma/schema.prisma file and ensure that the DATABASE_URL points to your PostgreSQL database. It should look something like this:

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}
Enter fullscreen mode Exit fullscreen mode

Prisma will automatically read the DATABASE_URL from your .env file.

Create a Database Folder

Inside the src directory of your project, create a folder named db. This will house all database-related files.

Create a Database Client File

Inside the db folder, create a file named index.ts. Add the following code to configure and export a singleton instance of the Prisma Client:

// src/db/index.ts
import { PrismaClient } from '@prisma/client'

const prismaClientSingleton = () => {
  return new PrismaClient()
}

declare const globalThis: {
  prismaGlobal: ReturnType<typeof prismaClientSingleton>;
} & typeof global;

const prisma = globalThis.prismaGlobal ?? prismaClientSingleton()

export default prisma

if (process.env.NODE_ENV !== 'production') globalThis.prismaGlobal = prisma
Enter fullscreen mode Exit fullscreen mode

Create Schema for auth

In this step, you'll define a database schema to manage authentication-related data, such as users, sessions, accounts, and verification tokens.

  1. Open the prisma/schema.prisma file in your project.
  2. Replace its contents with the following schema:
// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id               String    @id @default(cuid())
  name             String
  email            String
  emailVerified    Boolean   @default(false)
  image            String?
  createdAt        DateTime  @default(now())
  updatedAt        DateTime  @updatedAt
  twoFactorEnabled Boolean   @default(false)
  Session          Session[]
  Account          Account[]

  @@unique([email])
  @@map("user")
}

model Session {
  id        String   @id @default(cuid())
  expiresAt DateTime
  token     String   @unique
  ipAddress String?
  userAgent String?
  userId    String
  user      User     @relation(fields: [userId], references: [id], onDelete: Cascade)
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@map("session")
}

model Account {
  id           String    @id @default(cuid())
  accountId    String
  providerId   String
  userId       String
  user         User      @relation(fields: [userId], references: [id], onDelete: Cascade)
  accessToken  String?
  refreshToken String?
  idToken      String?
  expiresAt    DateTime?
  password     String?
  createdAt    DateTime  @default(now())
  updatedAt    DateTime  @updatedAt

  accessTokenExpiresAt  DateTime?
  refreshTokenExpiresAt DateTime?
  scope                 String?

  @@map("account")
}

model Verification {
  id         String    @id @default(cuid())
  identifier String
  value      String
  expiresAt  DateTime
  createdAt  DateTime? @default(now())
  updatedAt  DateTime? @updatedAt

  @@map("verification")
}
Enter fullscreen mode Exit fullscreen mode

Explanation of the Schema

  • User: Contains basic user information and manages relationships to Account and Session models.
  • Session: Tracks user sessions, including tokens, IP addresses, and user agents.
  • Account: Manages linked OAuth accounts for third-party login providers such as Google or GitHub.
  • Verification: Handles email or token-based verification (e.g., passwordless login or two-factor authentication).

Generate and Migrate the Prisma Schema

After defining your models in the schema.prisma file, you need to generate the Prisma client and apply the database migrations.

  1. Generate Prisma Client

    First, generate the Prisma Client, which provides an API to interact with your database:

pnpx prisma generate
Enter fullscreen mode Exit fullscreen mode

This will generate the necessary client files for accessing the database from your application.

  1. Run Migrations

Apply the migrations to your database by running the following command:

pnpx prisma migrate dev --name auth
Enter fullscreen mode Exit fullscreen mode

This will apply the migration to your database and generate the required tables (e.g., for User, Session, Account, etc.).

Once this setup is complete, Prisma will be ready to use for querying your database in your application.

Step-4: Integrate Better_Auth

Setting Up Environment Variables:

Add the following configuration to your .env file to provide the necessary environment variables for authentication:

# 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
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • BETTER_AUTH_SECRET: A secret key used by Better_Auth for signing JWTs. You can generate one using pnpm dlx auth secret.
  • BETTER_AUTH_URL: The base URL for your application (used for redirecting after authentication).
  • NEXT_PUBLIC_APP_URL: The public URL of your app, which is used for client-side requests.

Setting Up the Authentication Configuration:

  • Create the auth.ts File

    Inside the lib folder of your project, create a file named auth.ts. This file will contain the configuration for Better_Auth.

  • Add the Following Code

    Copy and paste the code below into the auth.ts file:

    // auth.ts
    import prisma from '@/db';
    import {
        betterAuth
    } from 'better-auth';
    import { prismaAdapter } from 'better-auth/adapters/prisma';
    
    export const auth = betterAuth({
        appName: "better_auth_nextjs",
        database: prismaAdapter(prisma, {
            provider: "postgresql"
        }),
        emailAndPassword: {
            enabled: true,
            autoSignIn: true,
            minPasswordLength: 8,
            maxPasswordLength: 20,
        },
    });
    

Explanation of the Code:

  1. betterAuth: Initializes the authentication library with the following parameters:
  2. appName: Specifies the name of your application. In this case, it is "better_auth_nextjs". This name might be used in logs or display contexts.
  3. database: Connects the Prisma adapter to the PostgreSQL database configured in your Prisma setup. It uses the prismaAdapter method, which links better-auth to your Prisma client (prisma) and ensures it works seamlessly with the PostgreSQL database defined in your .env file.
  4. emailAndPassword: Configures email-and-password authentication with the following settings:
    • enabled: true: Enables email-and-password as an authentication method.
    • autoSignIn: true: Automatically login the user in after signup.
    • minPasswordLength: Sets the minimum password length to 8 characters for added security.
    • maxPasswordLength: Sets the maximum password length to 20 characters to avoid excessively long passwords.

Setting Up the Authentication Client

  • Create the auth-client.ts File

    Inside the lib folder, create a new file named auth-client.ts. This file will manage client-side authentication interactions.

  • Add the Following Code

    Paste the code below into the newly created file:

// src/lib/auth-client.ts
import {
    createAuthClient
} from "better-auth/react";

export const authClient = createAuthClient({
    baseURL: process.env.NEXT_PUBLIC_APP_URL,

})

export const {
    signIn,
    signOut,
    signUp,
    useSession
} = authClient;
Enter fullscreen mode Exit fullscreen mode
  • createAuthClient:
    • This function initializes the client-side authentication logic using Better_Auth.
    • The baseURL is sourced from the NEXT_PUBLIC_APP_URL environment variable, ensuring compatibility with both local and production environments.
  • Exported Methods:

    • signIn: Initiates the sign-in process for users.
    • signOut: Logs the user out.
    • signUp: Handles user registration.
    • useSession: Retrieves and manages the current user's authentication session.

    💡Why This File is Important

    The auth-client.ts file abstracts client-side authentication functions, making them reusable and straightforward to integrate into React components. By centralizing the authentication logic, you maintain cleaner code and enhance the maintainability of your project.

Creating an API Route Handler for Authentication

To handle authentication API requests, you'll need to set up a route handler in your application. This will process all requests directed to the /api/auth/* endpoint (or any custom base path you configure).

  1. Create the Route Handler

    In your framework's designated directory for API routes (e.g., /app/api/auth for Next.js), create a catch-all route handler to manage authentication requests.

  2. Add the Following Code

    For a Next.js application, create a file named route.ts inside the /app/api/auth/[...all] directory and paste the following code:

// /app/api/auth/[...all]/route.ts
import { auth } from "@/lib/auth"; // path to your auth file
import { toNextJsHandler } from "better-auth/next-js";

export const { POST, GET } = toNextJsHandler(auth);
Enter fullscreen mode Exit fullscreen mode

Explanation:

  1. toNextJsHandler:
    • Converts the Better_Auth configuration into route handlers compatible with Next.js.
    • Automatically sets up handlers for the HTTP methods (e.g., POST and GET) required for authentication.
  2. Catch-All Route (/api/auth/*):
    • Ensures that all authentication-related requests (e.g., sign-in, callbacks) are routed through this handler.
    • This centralizes your authentication logic for better maintainability.

💡Why This Setup is Necessary

By creating this route handler:

  • You streamline the handling of authentication-related API requests.
  • The /api/auth/* endpoint acts as a dedicated entry point for Better_Auth to manage user sessions, social logins, and other auth operations.

Step 5: Create Sign-In Component

Create a Reusable CardWrapper Component

To improve modularity and reusability, let's create a CardWrapper component. This component will wrap content inside a styled card layout, making it easy to display various types of content consistently throughout the app.

Here’s the improved version of the CardWrapper component:

// src/components/card-wrapper.tsx
import React from 'react';
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from './ui/card';
import Link from 'next/link';

interface CardWrapperType {
  children: React.ReactNode;
  cardTitle: string;
  cardDescription: string;
  cardFooterLinkTitle?: string;
  cardFooterDescription?: string;
  cardFooterLink?: string;
  className?: string;
}

const CardWrapper = ({
  children,
  cardTitle,
  cardDescription,
  cardFooterLinkTitle = 'Learn More', // Default value
  cardFooterDescription = '',
  cardFooterLink,
  className = '',
}: CardWrapperType) => {
  return (
    <Card className={`w-[400px] relative ${className}`}>
      <CardHeader>
        <CardTitle>{cardTitle}</CardTitle>
        <CardDescription>{cardDescription}</CardDescription>
      </CardHeader>
      <CardContent>{children}</CardContent>
      {cardFooterLink && (
        <CardFooter className="flex items-center justify-center gap-x-1">
          {cardFooterDescription && <span>{cardFooterDescription}</span>}
          <Link href={cardFooterLink} className="underline text-blue-500 hover:text-blue-700">
            {cardFooterLinkTitle}
          </Link>
        </CardFooter>
      )}
    </Card>
  );
};

export default CardWrapper;

Enter fullscreen mode Exit fullscreen mode

Explanation:

  1. Component Structure:
    • children: This is the content that will be rendered inside the card.
    • cardTitle: The title of the card, which will be prominently displayed at the top.
    • cardDescription: A brief description under the title that provides more context for the card.
    • cardFooterLink : This is a link for navigating to different page
    • cardFooterDescription : A brief description of footer
    • cardFooterLinkTitle : Title for footer link
  2. Styling:
    • The Card container has utility classes like w-full, max-w-md, mx-auto, bg-white, rounded-lg, and shadow-md to control the width, margin, background color, border radius, and shadow of the card. This ensures the card looks well-aligned, centered, and visually distinct.
    • Inside the card header, the CardTitle and CardDescription have additional styling for text size, weight, and color to improve readability and contrast.
  3. Modularity:
    • By separating the card layout into its own reusable component (CardWrapper), it becomes easy to reuse this structure throughout the app wherever you need a card with a title and description. This promotes consistency in the design and reduces code duplication.

Create FormError and FormSuccess Components:

To improve error and success message handling in forms, let's create reusable components for displaying error and success messages.

  1. FormError Component:

    The `FormError` component will display error messages with an icon and a      styled container to alert the user.
    
// app/components/form-error.tsx
import React from 'react'
import { AiFillExclamationCircle } from "react-icons/ai";

type FormErrorProps = {
    message?: string
}

const FormError = ({message}: FormErrorProps) => {
    if (!message) return null
  return (
    <div className="bg-destructive/15 p-3 rounded-md flex items-center gap-x-2 text-sm text-destructive">
        <AiFillExclamationCircle className='w-4 h-4'/>
        {message}
    </div>
  )
}

export default FormError
Enter fullscreen mode Exit fullscreen mode

Explanation:

  1. Props:
    • message: A string containing the error message that will be displayed. If no message is provided, the component returns null.
  2. Styling:

    • bg-red-100: Light red background for the error message.
    • text-red-600: Red text color for visibility and to emphasize the error.
    • p-3: Padding inside the message box.
    • rounded-md: Rounded corners for a smooth appearance.
    • flex items-center gap-x-2: A flex container with horizontal spacing between the icon and the message text.
    • The AiFillExclamationCircle icon is used to represent the error visually.
  3. FormSuccess Component:

The FormSuccess component is similar to the FormError, but it's used for success messages.

// app/components/form-success.tsx
import React from 'react'
import { AiFillExclamationCircle } from "react-icons/ai";

type FormErrorProps = {
    message?: string
}

const FormError = ({message}: FormErrorProps) => {
    if (!message) return null
  return (
    <div className="bg-destructive/15 p-3 rounded-md flex items-center gap-x-2 text-sm text-destructive">
        <AiFillExclamationCircle className='w-4 h-4'/>
        {message}
    </div>
  )
}

export default FormError
Enter fullscreen mode Exit fullscreen mode

Explanation:

  1. Props:
    • message: A string containing the success message to be displayed. If no message is provided, the component returns null.
  2. Styling:
    • bg-green-100: Light green background to indicate success.
    • text-green-600: Green text color to match the success color scheme.
    • p-3: Padding inside the message box for better spacing.
    • rounded-md: Rounded corners for a sleek and modern design.
    • flex items-center gap-x-2: Flexbox container with horizontal spacing between the success icon and message text.
    • The AiFillCheckCircle icon represents a successful action.

Create the useAuthState.tsHook file

Inside the src folder, create a new folder named hooks and inside hooks folder create a new file named useAuthState.ts . This file contain the hooks which is responsible for handling error, success and loading state and message

import { useState } from 'react';

export const useAuthState = () => {
  const [error, setError] = useState('');
  const [success, setSuccess] = useState('');
  const [loading, setLoading] = useState(false);

  const resetState = () => {
    setError('');
    setSuccess('');
    setLoading(false);
  };

  return { error, setError, success, setSuccess, loading, setLoading, resetState };
};

Enter fullscreen mode Exit fullscreen mode

Explanation:
The useState hook from React is used to manage the component's state variables. Each state variable (e.g., error, success, loading) will be used to track a specific part of the authentication process.

  • error: Stores error messages (if any) during the authentication process.
    • Initial Value: '' (empty string), meaning no error by default.
    • Function: setError updates the error message.
  • success: Stores success messages when an authentication process completes successfully.
    • Initial Value: '' (empty string), meaning no success message by default.
    • Function: setSuccess updates the success message.
  • loading: Indicates whether an authentication process (e.g., login, signup) is ongoing.
    • Initial Value: false, meaning the process is not loading by default.
    • Function: setLoading updates the loading state.
  • Purpose: Resets all three states (error, success, and loading) to their initial values.
    • setError(''): Clears the error message.
    • setSuccess(''): Clears the success message.
    • setLoading(false): Sets the loading state to false.
  • Use Case: Typically used to clear all messages and reset the loading state when starting a new authentication flow or when leaving the authentication page.

Create zod scheme for signin and signup

Inside the srcfolder, create a new folders helpers/zod and then create a new files named signup-schema.tsx and login-schema.tsx .This file is responsible for type check the input values given by the user. If user sends invalid field type it returns the error message.

// src/helpers/zod/signup-schema.ts
import { z } from "zod";
export const SignupSchema = z
  .object({
    name: z
      .string()
      .min(2, { message: "Minimum 2 characters are required" })
      .max(20, { message: "Maximum of 20 characters are allowed" }),
    email: z
      .string()
      .email({ message: "Invalid email address" })
      .min(1, { message: "Email is required" }),
    password: z
      .string()
      .min(8, { message: "Password must be at least 6 characters long" })
      .max(20, { message: "Password must be at most 20 characters long" }),
  })
Enter fullscreen mode Exit fullscreen mode
// src/helpers/zod/login-schema.ts
import { z } from "zod";

const LoginSchema = z.object({
    email: z
    .string()
    .email({message: "Invalid email"})
    .min(1, {message: "Email is required"}),
    password: z
    .string()
    .min(8, {message: "Password must be at least 8 characters long"})
    .max(20, {message: "Password must be at most 20 characters long"}),
})

export default LoginSchema
Enter fullscreen mode Exit fullscreen mode

Explanation:
This imports the z object from the zod library, which provides methods for defining and validating schemas.

  1. name Field
  2. Type: A string. Ensures the name field is a string.
  3. Minimum Length: min(2), meaning the name must have at least 2 characters.
    • Custom error message: "Minimum 2 characters are required".
  4. Maximum Length: max(20), meaning the name can have at most 20 characters.
    • Custom error message: "Maximum of 20 characters are allowed".
  5. email Field
  6. Type: A string. Ensures the email field is a string.
  7. Email Format: .email(), validates that the string is a properly formatted email address.
    • Custom error message: "Invalid email address".
  8. Non-Empty: .min(1), ensures the email field is not empty.
    • Custom error message: "Email is required".
  9. password Field
  10. Type: A string. Ensures the password field is a string.
  11. Minimum Length: min(6), enforces a minimum password length of 6 characters.
    • Custom error message: "Password must be at least 6 characters long".
  12. Maximum Length: max(64), enforces a maximum password length of 64 characters.
    • Custom error message: "Password must be at most 64 characters long".

Create the sign-in.tsx file

Inside the components/auth folder, create a new file named sign-in.tsx. This file will contain the component responsible for rendering the sign-in UI and handling the authentication logic.

Here's how to structure your component:

// components/auth/sign-in.tsx
"use client"
import React from 'react'
import CardWrapper from '../card-wrapper'
import FormError from '../form-error'
import { FormSuccess } from '../form-success'
import { FcGoogle } from 'react-icons/fc'
import SocialButton from './social-button'
import { FaGithub } from 'react-icons/fa'
import { useAuthState } from '@/hooks/useAuthState'
import LoginSchema from '@/helpers/zod/login-schema'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../ui/form'
import { Input } from '../ui/input'
import { Button } from '../ui/button'
import { signIn } from '@/lib/auth-client'
import { useRouter } from 'next/navigation'

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

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

    const onSubmit = async (values: z.infer<typeof LoginSchema>) => {
        try {
          await signIn.email({
            email: values.email,
            password: values.password
          }, {
            onResponse: () => {
              setLoading(false)
            },
            onRequest: () => {
              resetState()
              setLoading(true)
            },
            onSuccess: (ctx) => {
                setSuccess("LoggedIn successfully")
                router.replace('/')
            },
            onError: (ctx) => {
              setError(ctx.error.message);
            },
          });
        } catch (error) {
          console.log(error)
          setError("Something went wrong")
        }
      }

    return (
        <CardWrapper
            cardTitle='Sign In'
            cardDescription='Enter your email below to login to your account'
            cardFooterDescription="Don't have an account?"
            cardFooterLink='/signup'
            cardFooterLinkTitle='Sign up'
        >
            <Form {...form}>
                <form className='space-y-4' onSubmit={form.handleSubmit(onSubmit)}>
                    <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>
                        )}
                    />
                    <FormField
                        control={form.control}
                        name="password"
                        render={({ field }) => (
                            <FormItem>
                                <FormLabel>Password</FormLabel>
                                <FormControl>
                                    <Input
                                        disabled={loading}
                                        type="password"
                                        placeholder='********'
                                        {...field}
                                    />
                                </FormControl>
                                <FormMessage />
                            </FormItem>

                        )}
                    />
                    <FormError message={error} />
                    <FormSuccess message={success} />
                    <Button disabled={loading} type="submit" className='w-full'>Login</Button>
                </form>
            </Form>
        </CardWrapper>
    )
}

export default SignIn
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • State management: The component uses useAuthState for managing error, success, and loading states.
  • Email and Password sign-in: The onSubmit functions handle authentication with Email and Password respectively. They use the signIn.email method from the auth-client.ts file.
  • React Hook Form: It uses this library to handler the form check the types using zod
  • Form feedback: FormError and FormSuccess components display any errors or success messages during the sign-in process.
  • UI: The Button component is used to create sign-in buttons for GitHub and Google. The buttons are disabled while the sign-in process is in progress

This component will handle the authentication process and provide users with an interface to sign in using their Email and Password.

Create the sign-up.tsx file

Inside the components/auth folder, create a new file named sign-up.tsx. This file will contain the component responsible for rendering the sign-up UI and handling the authentication logic.

Here's how to structure your component:

// src/components/auth/sign-up.tsx
// components/auth/sign-up.tsx
"use client"
import React from 'react'
import CardWrapper from '../card-wrapper'
import FormError from '../form-error'
import { FormSuccess } from '../form-success'
import { FcGoogle } from 'react-icons/fc'
import SocialButton from './social-button'
import { FaGithub } from 'react-icons/fa'
import { useAuthState } from '@/hooks/useAuthState'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../ui/form'
import { Input } from '../ui/input'
import { Button } from '../ui/button'
import { SignupSchema } from '@/helpers/zod/signup-schema'
import { signUp } from '@/lib/auth-client'
import { useRouter } from 'next/navigation'

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

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

    const onSubmit = async (values: z.infer<typeof SignupSchema>) => {
        try {
            await signUp.email({
                name: values.name,
                email: values.email,
                password: values.password,
            }, {
                onResponse: () => {
                    setLoading(false)
                },
                onRequest: () => {
                    resetState()
                    setLoading(true)
                },
                onSuccess: () => {
                    setSuccess("User has been created")
                    router.replace('/')
                },
                onError: (ctx) => {
                    setError(ctx.error.message);
                },
            });
        } catch (error) {
            console.error(error)
            setError("Something went wrong")
        }

    }

    return (
        <CardWrapper
        cardTitle='SignUp'
        cardDescription='Create an new account'
        cardFooterLink='/login'
        cardFooterDescription='Already have an account?'
        cardFooterLinkTitle='Login'
        >
            <Form {...form}>
                <form className='space-y-4' onSubmit={form.handleSubmit(onSubmit)}>
                <FormField
                        control={form.control}
                        name="name"
                        render={({ field }) => (
                            <FormItem>
                                <FormLabel>Name</FormLabel>
                                <FormControl>
                                    <Input
                                        disabled={loading}
                                        type="text"
                                        placeholder='john'
                                        {...field} />
                                </FormControl>
                                <FormMessage />
                            </FormItem>
                        )}
                    />
                    <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>
                        )}
                    />
                    <FormField
                        control={form.control}
                        name="password"
                        render={({ field }) => (
                            <FormItem>
                                <FormLabel>Password</FormLabel>
                                <FormControl>
                                    <Input
                                        disabled={loading}
                                        type="password"
                                        placeholder='********'
                                        {...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 SignUp
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • State management: The component uses useAuthState for managing error, success, and loading states.
  • Email and Password sign-up: The onSubmit functions handle authentication with Email and Password respectively and creates new user in database. They use the signIn.email method from the auth-client.ts file.
  • React Hook Form: It uses this library to handler the form check the types using zod
  • Form feedback: FormError and FormSuccess components display any errors or success messages during the sign-in process.
  • UI: The Button component is used to create sign-in buttons for GitHub and Google. The buttons are disabled while the sign-in process is in progress

This component will handle the authentication process and provide users with an interface to sign in using their Email and Password.

Create Signout Button

Inside the src/components/auth folder, create a new file named sign-out.tsx .This file will contain the component responsible for rendering the sign-out user from the app.

"use client"
import React from 'react'
import { Button } from '../ui/button'
import { signOut } from '@/lib/auth-client'
import { useRouter } from 'next/navigation'

const SignOut = () => {
    const router = useRouter()
  return (
    <Button
    onClick={async() => {await signOut({
      fetchOptions: {
        onSuccess: () => {
          router.push("/signin")
        }
      }
    })}}
    >Logout</Button>
  )
}

export default SignOut
Enter fullscreen mode Exit fullscreen mode

Explanation:
The SignOut component provides a simple button for users to log out of their account. It uses an asynchronous function to handle the sign-out process and redirects users to the sign-in page upon successful logout.

Create page.tsx and layout.tsx for signup and signin page

Create (auth) folder inside the src/app folder and then create a layout.tsx file , signupfolder and signin folder, and then create a page.tsxfile inside the both the folders.

// src/app/(auth)/layout.tsx
import React from 'react'

const AuthLayout = ({children}: {children: React.ReactNode}) => {
  return (
    <div className='flex items-center justify-center w-screen h-screen'>{children}</div>
  )
}

export default AuthLayout
Enter fullscreen mode Exit fullscreen mode
  • Layout Component: The AuthLayout component wraps the entire sign-in page content (children). This layout is useful for organizing the structure of the page and allows for consistent styling and placement of content.
// /app/(auth)/signin/page.tsx
import SignIn from '@/components/auth/sign-in'
import React from 'react'

const SignInPage = () => {
  return (
    <SignIn />
  )
}

export default SignInPage
Enter fullscreen mode Exit fullscreen mode

The SignIn component from components/auth/sign-in is imported and used to display the sign-in form.

// /app/(auth)/signup/page.tsx
import SignUp from '@/components/auth/sign-up'
import React from 'react'

const SignupPage = () => {
  return (
    <SignUp />
  )
}

export default SignupPage
Enter fullscreen mode Exit fullscreen mode

The SignUp component from components/auth/sign-up is imported and used to display the sign-in form.

// src/app/page.tsx
import SignOut from "@/components/auth/sign-out";

export default function Home() {
  return (
    <SignOut />
  );
}
Enter fullscreen mode Exit fullscreen mode

The SignOut component from components/auth/sign-out is imported and used to display the sign-in form.

Step 6 - Run the Application:

To start your development server and preview your application:

  1. Open your terminal and navigate to your project directory.
  2. Run the following command:
pnpm dev
Enter fullscreen mode Exit fullscreen mode

This command will launch the development server and make your application accessible at http://localhost:3000 by default. You can now test and view your application in the browser.

Once the development server is running, visit the following URL in your browser to access the sign-in page:

http://localhost:3000/signin
Enter fullscreen mode Exit fullscreen mode

This will load the sign-in page of your application, where you can test the OAuth authentication flow.

Image description

http://localhost:3000/signup
Enter fullscreen mode Exit fullscreen mode

Image description

Congratulations! You have successfully integrated OAuth login 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:

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 (1)

Collapse
 
jijojose profile image
Jijo Jose

The form-success.tsx will throw an error.

This fixed it:

//components/form-success.tsx
import React from 'react'
import { AiFillExclamationCircle } from "react-icons/ai";

type FormESuccessProps = {
    message?: string
}

const FormSuccess = ({message}: FormESuccessProps) => {
    if (!message) return null
  return (
    <div className="bg-green-400/15 p-3 rounded-md flex items-center gap-x-2 text-sm text-destructive">
        <AiFillExclamationCircle className='w-4 h-4'/>
        {message}
    </div>
  )
}

export default FormSuccess
Enter fullscreen mode Exit fullscreen mode

and them change the import to

import FormSuccess from '../form-success' //Remove the Braces
Enter fullscreen mode Exit fullscreen mode