I have implemented next-auth v5 in my next.js 14 application. I have implemented role-based authentication, I have two roles,
- provider
- admin
Now in my application, I have two dashboards, one for the provider (/provider/dashboard) and one for the admin (/provider/dashboard). I am injecting the user role into my session. Now if the user logs in, I want to redirect the user to their respective dashboards
Code
next auth config code
import type { DefaultSession } from "next-auth";
import bcrypt from "bcrypt";
import NextAuth from "next-auth";
import Credentials from "next-auth/providers/credentials";
import type { Provider, User } from "@rheumote/db/schema";
import type { UserRole } from "@rheumote/shared";
import { db, eq } from "@rheumote/db";
import {
provider as ProviderModel,
user as UserModel,
} from "@rheumote/db/schema";
import { SignInFormZodObject } from "@rheumote/validators";
import { Config } from "./auth.config";
export type { Session } from "next-auth";
declare module "next-auth" {
interface Session {
user: {
id: number;
practiceId: number;
role: UserRole;
} & DefaultSession["user"];
}
interface User {
practiceId: number;
role: UserRole;
}
interface JWT {
id: number;
role: UserRole;
}
}
async function getUser(email: string): Promise<
| (Provider & {
password: string;
email: string;
role: typeof UserRole | null;
})
| User
| undefined
> {
try {
const [userRow] = await db
.select()
.from(UserModel)
.where(eq(UserModel.email, email));
if (userRow?.role === "provider") {
const [provider] = await db
.select()
.from(ProviderModel)
.where(eq(ProviderModel.userId, userRow.id));
return {
...provider!,
email: userRow.email,
password: userRow.password!,
role: userRow.role,
createdAt: userRow.createdAt,
};
} else {
return userRow;
}
} catch (error) {
console.error("Failed to fetch user:", error);
throw new Error("Failed to fetch user.");
}
}
export const {
handlers: { GET, POST },
auth,
signIn,
signOut,
} = NextAuth({
...Config,
providers: [
Credentials({
type: "credentials",
async authorize(credentials): Promise<any> {
const parsedCredentials = SignInFormZodObject.safeParse(credentials);
if (parsedCredentials.success) {
const { email, password } = parsedCredentials.data;
const user = await getUser(email);
if (!user) return null;
const passwordsMatch = await bcrypt.compare(password, user.password!);
if (passwordsMatch) {
if (typeof user === typeof ProviderModel) {
return {
...user,
//@ts-ignore
practiceId: user.practiceId!,
};
} else {
return {
...user,
practiceId: -1,
};
}
}
}
return null;
},
}),
],
});
middle ware
import type { NextAuthConfig } from "next-auth";
import { NextResponse } from "next/server";
import { getBaseUrl, UserRole } from "@rheumote/shared";
export const Config = {
session: {
strategy: "jwt",
},
pages: {
signIn: "/signin",
},
callbacks: {
authorized({ auth, request: { nextUrl } }) {
const isAuthenticated = !!auth?.user;
const userRole = auth?.user.role;
console.log("middleware user", JSON.stringify(auth));
const isOnDashboard = nextUrl.pathname.includes("/dashboard");
const isOnNotFound = nextUrl.pathname.includes("/404");
const isPatientModule = nextUrl.pathname.startsWith("/patient");
const isAdminModule = nextUrl.pathname.startsWith("/admin");
const isProviderModule = nextUrl.pathname.startsWith("/provider");
console.log("config data", {
isAdminModule,
isProviderModule,
userRole,
isAuthenticated,
pathName: nextUrl.pathname,
});
// if (isAdminModule) {
// return true;
// }
if (isPatientModule) {
return true;
}
if (isOnNotFound) {
return true;
}
if (isAuthenticated && nextUrl.pathname.startsWith("/signin")) {
if (userRole === UserRole.Admin) {
return NextResponse.redirect(`${getBaseUrl()}/admin/dashboard`);
} else {
return NextResponse.redirect(`${getBaseUrl()}/provider/dashboard`);
}
}
if (
isProviderModule &&
userRole === UserRole.Provider &&
isAuthenticated
) {
return true;
}
if (isAdminModule && userRole === UserRole.Admin && isAuthenticated) {
return true;
}
// if (isOnDashboard && isAuthenticated) {
// return true;
// }
return false;
},
jwt({ token, user }) {
if (user) {
token.id = Number(user.id);
token.practiceId = user.practiceId;
token.role = user.role;
}
return token;
},
session: ({ session, token }) => {
console.log("session and token", { session, token });
session.user = {
...session.user,
id: token.id as never,
practiceId: token.practiceId as number,
role: (token.role as UserRole) ?? UserRole.Provider,
};
return session;
},
},
providers: [],
} satisfies NextAuthConfig;
signing component
"use client";
import type { SubmitHandler } from "react-hook-form";
import React from "react";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { zodResolver } from "@hookform/resolvers/zod";
import { Loader } from "lucide-react";
import { useForm } from "react-hook-form";
import type { SignInFormSchema } from "@rheumote/validators";
import { getBaseUrl } from "@rheumote/shared";
import { Button } from "@rheumote/ui/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@rheumote/ui/form";
import { Input } from "@rheumote/ui/input";
import { SignInFormZodObject } from "@rheumote/validators";
import { toast } from "@rheumote/ui/toast";
import { authenticate } from "./action";
const ProviderSigninPage = () => {
const router = useRouter();
const form = useForm<SignInFormSchema>({
resolver: zodResolver(SignInFormZodObject),
mode: "onChange",
defaultValues: {
email: "",
password: "",
},
});
const onLoginSubmit: SubmitHandler<SignInFormSchema> = async (data) => {
try {
const response = await authenticate(data);
if (response?.success) {
router.push(`${getBaseUrl()}/provider/dashboard`);
} else {
toast.error(response?.error);
}
} catch (e) {
console.error(e);
toast.error("An error occurred during authentication.");
}
};
const isFormLoading = form.formState.isSubmitting;
return (
<div className="flex h-screen min-h-screen w-full flex-col">
<Image
src="/rheumote_home.svg"
width={300}
height={300}
alt="rheumote logo"
className="mt-20 self-center"
/>
<div className="flex h-full w-full flex-col justify-center self-center">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onLoginSubmit)}
className="min-w-[300px] space-y-6 self-center"
>
<FormField
disabled={isFormLoading}
control={form.control}
name="email"
render={({ field }) => {
return (
<FormItem className="h-max w-full">
<FormLabel>Email</FormLabel>
<FormControl>
<>
<Input placeholder="Email" {...field} />
<FormMessage />
</>
</FormControl>
</FormItem>
);
}}
/>
<FormField
disabled={isFormLoading}
control={form.control}
name="password"
render={({ field }) => {
return (
<FormItem className="h-max w-full">
<FormLabel>Password</FormLabel>
<FormControl>
<>
<Input
type="password"
placeholder="Password"
{...field}
/>
<FormMessage />
</>
</FormControl>
</FormItem>
);
}}
/>
<Button className="flex w-full flex-row items-center gap-4">
{isFormLoading && <Loader className="animate-spin" />} Sign In
</Button>
<Link href="#" className="w-full text-red-500">
<p className="mt-10 w-full text-center">Forget Password?</p>
</Link>
</form>
</Form>
</div>
</div>
);
};
export default ProviderSigninPage;
How can i redirect the user to their respective dashboard on the basis of the user role on sign in?
Now if you look at the middleware code I am checking that If the user is on the login page and the user is authenticated, I am redirecting the user to their respective dashboard according to the user role but I get the error of "Too many redirects"
