T3 : Ep - 2 (Credentials provider)

T3 : Ep - 2 (Credentials provider)

Table of Contents

After some digging I think I have found a good example for Credentials provider here, clone it and study it.

Let’s implement Credentials Provider in new T3 app.

I’ll take the backend approach, will start from creating a base then frontend.

Add tRPC route for auth:

Create a file at src/server/api/routers/ named auth.ts

With code:

import { loginSchema } from "~/validation/auth";
import { TRPCError } from "@trpc/server";
import bcrypt from "bcrypt";
import { createTRPCRouter, publicProcedure } from "../trpc";

const SALT_ROUNDS = 10;

export const authRouter = createTRPCRouter({
  register: publicProcedure
    .input(loginSchema)
    .mutation(async ({ input, ctx }) => {
      const { email, password } = input;

      const exists = await ctx.db.user.findFirst({
        where: { email },
      });

      if (exists) {
        throw new TRPCError({
          code: "CONFLICT",
          message: "User already exists.",
        });
      }

      const salt = bcrypt.genSaltSync(SALT_ROUNDS);
      const hash = bcrypt.hashSync(password, salt);

      const result = await ctx.db.user.create({
        data: { email, password: hash },
      });

      return {
        status: 201,
        message: "Account created successfully",
        result: result.email,
      };
    }),
});
  1. Install bcrypt package:
yarn add bcrypt
npm i --save-dev @types/bcrypt
  1. Prisma schema:
model User {
    id            String    @id @default(cuid())
    name          String?
    password      String?
    email         String?   @unique
    emailVerified DateTime?
    image         String?
    accounts      Account[]
    sessions      Session[]
    posts         Post[]
}

make sure you have this password in User model.

  1. Validation:
import z from "zod";

export const loginSchema = z.object({
  email: z.string().email(),
  password: z.string().min(4),
});

export const registerSchema = loginSchema.extend({
  username: z.string(),
});

export type ILogin = z.infer<typeof loginSchema>;
export type IRegister = z.infer<typeof registerSchema>;

So using this auth RPC user can register, now for login.

Add Credentials provider:

Update the file at src/server/auth/config.ts

with code:


import Credentials from "next-auth/providers/credentials";
import bcrypt from "bcrypt";
import { loginSchema } from "~/validation/auth";
import { db } from "~/server/db";
import NextAuth, { NextAuthConfig } from "next-auth";

export const authConfig = {
  callbacks: {
    jwt: async ({ token, user }: any) => {
      if (user) {
        token.id = user.id;
        token.email = user.email;
      }

      return token;
    },
    session({ session, token }: any) {
      if (token && session.user) {
        session.user.id = token.id as string;
      }

      return session;
    },
  },
  secret: process.env.NEXTAUTH_SECRET,
  pages: {
    signIn: "/login",
    newUser: "/register",
    error: "/login",
  },
  providers: [
    Credentials({
      name: "credentials",
      credentials: {
        email: {
          label: "Email",
          type: "email",
          placeholder: "jsmith@gmail.com",
        },
        password: { label: "Password", type: "password" },
      },
      authorize: async (credentials) => {
        const cred = await loginSchema.parseAsync(credentials);

        const user = await db.user.findFirst({
          where: { email: cred.email },
        });

        if (!user) {
          return null;
        }

        if (user.password) {
          const isValidPassword = bcrypt.compareSync(
            cred.password,
            user.password
          );

          if (!isValidPassword) {
            return null;
          }
        }

        return {
          id: user.id,
          email: user.email,
        };
      },
    }),
    // ...add more providers here
  ],
} satisfies NextAuthConfig;

Here we are adding our Credentials provider instead of discord.

Registering user:

Add a register page at /register

"use client";

import React from "react";
import { useForm, type SubmitHandler } from "react-hook-form";
import { useState } from "react";
import { IRegister } from "~/validation/auth";
import { api } from "~/trpc/react";
import { redirect } from "next/navigation";

const RegisterForm = () => {
  const [errorMessage, setErrorMessage] = useState<string | undefined>();

  const mutation = api.auth.register.useMutation({
    onError: (e: { message: React.SetStateAction<string | undefined> }) =>
      setErrorMessage(e.message),
    onSuccess: () => redirect("/login"),
  });

  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<IRegister>();

  const onSubmit: SubmitHandler<IRegister> = async (data) => {
    setErrorMessage(undefined);
    await mutation.mutateAsync(data);
  };

  return (
    <div className="radius flex flex-col items-center gap-2 border p-4">
      <form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-2">
        {errorMessage && (
          <p className="text-center text-red-600">{errorMessage}</p>
        )}
        <label>Email</label>
        <input
          className="rounded border px-4 py-1"
          type="text"
          {...register("email", { required: true })}
        />
        {errors.email && (
          <p className="text-center text-red-600">This field is required</p>
        )}
        <label>Password</label>
        <input
          className="rounded border px-4 py-1"
          type="password"
          {...register("password", { required: true })}
        />
        {errors.password && (
          <p className="text-center text-red-600">This field is required</p>
        )}

        <input type="submit" className="rounded border px-4 py-1" />
      </form>
    </div>
  );
};

export default RegisterForm;

And import this Form in it.

  1. Install React Hook Form:
yarn add react-hook-form    

In this form api from tRPC client is used. This is similar to post used in home page for getting the Hello YourName response.

This api is in src/trpc/react.tsx is created from AppRouter

export const api = createTRPCReact<AppRouter>();

Creating AuthRouter

Create a file at src/server/api/routers named auth.ts

with code:

import { TRPCError } from "@trpc/server";
import bcrypt from "bcrypt";
import { createTRPCRouter, publicProcedure } from "../trpc";
import { loginSchema } from "~/validation/auth";

const SALT_ROUNDS = 10;

export const authRouter = createTRPCRouter({
  register: publicProcedure
    .input(loginSchema)
    .mutation(async ({ input, ctx }) => {
      const { email, password } = input;

      const exists = await ctx.db.user.findFirst({
        where: { email },
      });

      if (exists) {
        throw new TRPCError({
          code: "CONFLICT",
          message: "User already exists.",
        });
      }

      const salt = bcrypt.genSaltSync(SALT_ROUNDS);
      const hash = bcrypt.hashSync(password, salt);

      const result = await ctx.db.user.create({
        data: { email, password: hash },
      });

      return {
        status: 201,
        message: "Account created successfully",
        result: result.email,
      };
    }),
});

Here in routers we are writting RPCs that are accessed from client.

Add this authRouter to appRouter at src/server/api/root.ts

export const appRouter = createTRPCRouter({
  post: postRouter,
  auth: authRouter
});

This way cleint can call, api.auth.register(….)

Signin

Create a route page at /login and import this form there:

"use client";

import React from "react";
import { useForm, type SubmitHandler } from "react-hook-form";
import { signIn } from "next-auth/react";
import { ILogin } from "~/validation/auth";

const LoginForm = () => {
  const error = null;

  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<ILogin>();

  const onSubmit: SubmitHandler<ILogin> = async (data) => {
    await signIn("credentials", { ...data, callbackUrl: "/dashboard" });
  };

  return (
    <div className="radius flex flex-col items-center gap-2 border p-4">
      <form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-2">
        {error && (
          <p className="text-center text-red-600">Login failed, try again!</p>
        )}
        <label>Email</label>
        <input
          className="rounded border px-4 py-1"
          type="text"
          {...register("email", { required: true })}
        />
        {errors.email && <span>This field is required</span>}
        <label>Password</label>
        <input
          className="rounded border px-4 py-1"
          type="password"
          {...register("password", { required: true })}
        />
        {errors.password && <span>This field is required</span>}

        <input type="submit" className="rounded border px-4 py-1" />
      </form>
    </div>
  );
};

export default LoginForm;

This should work directly as it uses the signIn from NextAuth

After logging in this should also create the session and the home page should look like this:

alt text

This way the Credentials provider is added to the project, here we can also create RBAC system too.

alt text

Using the default code T3 provided, it creates a new post and shows the latest one.

The Credentials provider can be accessed on this branch of t3 project.

Tags :