Next auth (auth.js) V5 redirect according to the user role (role based redirect)

31 Views Asked by At

I have implemented next-auth v5 in my next.js 14 application. I have implemented role-based authentication, I have two roles,

  1. provider
  2. 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"

enter image description here

0

There are 0 best solutions below