
T3 : Ep - 2 (Credentials provider)
- Atul
- Type script
- January 6, 2025
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,
};
}),
});
- Install bcrypt package:
yarn add bcrypt
npm i --save-dev @types/bcrypt
- 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.
- 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.
- 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:
This way the Credentials provider is added to the project, here we can also create RBAC system too.
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.