mongoosenext.jsmiddlewarenext-authauth.js

Using NextAuth (Auth.js) With MongoDB (mongoose) And Middleware


Intro

Hello, in this article, we want to explore how to use NextAuth with MongoDB and Middleware.

“Note: If you want to use middleware with Mongoose, it is not possible.”

Next.js Edge Runtime

Mongoose does not currently support Next.js Edge Runtime. While you can import Mongoose in Edge Runtime, you'll get Mongoose's browser library. There is no way for Mongoose to connect to MongoDB in Edge Runtime, because Edge Runtime currently doesn't support Node.js net API, which is what the MongoDB Node Driver uses to connect to MongoDB.

https://mongoosejs.com/docs/nextjs.html


Solution

  • Packages

    1. npx create-next-app@latest app
    2. npm install zod
    3. npm install mongoose
    4. npm install next-auth
    5. npm install bcryptjs @types/bcryptjs

    Steps

    1. Develop the sign-in and sign-out page and implement the frontend form with form validation

    /app/signin/page.tsx

    import SignInForm from "@/components/SignInForm";
    
    import { Metadata } from "next";
    
    export async function generateMetadata(): Promise<Metadata> {
      return {
        title: "Sign In",
      };
    }
    
    async function SignIn() {
      return (
        <div className="gird grid-cols-1 justify-items-center content-center h-full">
          <SignInForm />
        </div>
      );
    }
    
    export default SignIn;
    

    /components/SignInForm.tsx

    "use client";
    
    import { z } from "zod";
    
    import { useEffect, useState } from "react";
    
    import { signIn } from "next-auth/react";
    
    import { useSession } from "next-auth/react";
    
    import { useRouter, useSearchParams } from "next/navigation";
    
    const userSchema = z.object({
      email: z.string().email("Invalid Email."),
      password: z.string().min(5, "Password Must Be Least At 5 Character long."),
    });
    
    type User = z.infer<typeof userSchema>;
    
    function SignInForm() {
      const [err, setErr] = useState<{
        email: string | undefined;
        password: string | undefined;
      }>({ email: undefined, password: undefined });
    
      const { data: session } = useSession();
    
      const router = useRouter();
    
      const redirect = useSearchParams().get("redirect");
    
      async function submitHandler(formData: FormData) {
        const email = formData.get("email") as string | undefined;
        const password = formData.get("password") as string | undefined;
    
        if (!email || !password)
          return setErr({
            email: email ? undefined : "Please Enter Your Email.",
            password: password ? undefined : "Please Enter Your Password.",
          });
    
        const user: User = { email, password };
    
        const result = userSchema.safeParse(user);
    
        if (result.error) {
          const zodErrors = result.error.errors;
    
          const email = zodErrors.find(
            (item) => item.path.join("") === "email"
          )?.message;
          const password = zodErrors.find(
            (item) => item.path.join("") === "password"
          )?.message;
    
          const errors = { email, password };
    
          setErr(errors);
    
          return;
        }
    
        setErr({ email: undefined, password: undefined });
    
        try {
          const res = await signIn("credentials", {
            redirect: false,
            email,
            password,
          });
    
          if (res?.error) console.log("Faild To Sign In.");
        } catch (err) {
          console.log(err);
        }
      }
    
      useEffect(() => {
        if (session?.user) router.push("/signout");
      }, [session, router, redirect]);
    
      return (
        <form
          action={submitHandler}
          className="bg-slate-100 text-gray-700 p-4 rounded-md"
        >
          <h2 className="mb-2">Sign IN</h2>
          <div>
            <label htmlFor="email">Email:</label>
            <br />
            <input
              type="email"
              id="email"
              name="email"
              className="px-2 py-1 my-1 bg-transparent border border-gray-600 rounded-md"
              placeholder="Enter Your Email"
              autoFocus
            />
            {err.email && <small className="block text-red-500">{err.email}</small>}
          </div>
          <div>
            <label htmlFor="password">Password:</label>
            <br />
            <input
              type="password"
              id="password"
              name="password"
              className="px-2 py-1 my-1 bg-transparent border border-gray-600 rounded-md"
              placeholder="Enter Your Password"
              autoFocus
            />
            {err.password && (
              <small className="block text-red-500">{err.password}</small>
            )}
          </div>
          <button
            type="submit"
            className="block mx-auto px-2 py-1 mt-4 bg-gray-600 text-slate-100 hover:bg-slate-700 transition-all rounded-md"
          >
            Submit
          </button>
        </form>
      );
    }
    
    export default SignInForm;
    

    /app/signout/page.tsx

    import SignOutBTN from "@/components/SignOutBTN";
    
    import { Metadata } from "next";
    
    export async function generateMetadata(): Promise<Metadata> {
      return {
        title: "Sign Out",
      };
    }
    
    function SignOut() {
      return (
        <div className="gird grid-cols-1 justify-items-center content-center h-full">
          <SignOutBTN />
        </div>
      );
    }
    
    export default SignOut;
    

    /components/SignOutBTN.tsx

    "use client";
    
    import { signOut } from "next-auth/react";
    
    function SignOutBTN() {
      return (
        <button
          onClick={() => signOut({ callbackUrl: "/signin" })}
          type="submit"
          className="block px-2 py-1 bg-slate-200 text-gray-700 hover:bg-slate-100 transition-all rounded-md"
        >
          Sign Out
        </button>
      );
    }
    
    export default SignOutBTN;
    
    1. Connect to the database (db) and develop the user model

    /utils/db.ts

    import mongoose from "mongoose";
    
    async function connect(): Promise<void> {
      try {
        await mongoose.connect("mongodb://127.0.0.1:27017/test");
    
        console.log("Connected.");
      } catch (err) {
        console.log(err);
    
        throw new Error("Faild To Connect.");
      }
    }
    
    const db = { connect };
    
    export default db;
    

    /models/user.model.ts

    import mongoose, { Document } from "mongoose";
    
    import { User as UT } from "@/interface/User";
    
    interface UserSchema extends UT, Document {}
    
    const userSchema = new mongoose.Schema<UserSchema>({
      username: { type: String, required: true },
      email: { type: String, required: true, unique: true },
      password: { type: String, required: true },
      isAdmin: { type: Boolean, required: true, default: false },
    });
    
    const User =
      mongoose.models.User || mongoose.model<UserSchema>("User", userSchema);
    
    export default User;
    
    1. Develop the user static data and API (Route Handler)

    /data/users.ts

    import bcrypt from "bcryptjs";
    
    const users = [
      {
        username: "P_Co_ST",
        email: "example@example.com",
        password: bcrypt.hashSync("654321"),
        isAdmin: true,
      },
      {
        username: "Harchi",
        email: "example@example.com",
        password: bcrypt.hashSync("123456"),
        isAdmin: false,
      },
    ];
    
    export default users;
    

    /app/api/user/route.ts

    import { NextRequest, NextResponse } from "next/server";
    
    import db from "@/utils/db";
    
    import User from "@/models/user.model";
    
    import users from "@/data/users";
    
    export async function GET(request: NextRequest) {
      await db.connect();
    
      const result = await User.insertMany(users);
    
      return NextResponse.json(result);
    }
    
    1. Implement NextAuth

    Declare module next-auth and next-auth/jwt

    /types/next-auth.d.ts

    import { Session } from "next-auth";
    
    import { JWT } from "next-auth/jwt";
    
    import mongoose from "mongoose";
    
    declare module "next-auth" {
      interface Session {
        _id: mongoose.Schema.Types.ObjectId;
        username: string;
        email: string;
        isAdmin: boolean;
      }
    
      interface User {
        _id: mongoose.Schema.Types.ObjectId;
        username: string;
        email: string;
        password: string;
        isAdmin: boolean;
      }
    }
    
    declare module "next-auth/jwt" {
      interface JWT {
        _id: mongoose.Schema.Types.ObjectId;
        username: string;
        email: string;
        isAdmin: boolean;
      }
    }
    

    /auth.ts

    import NextAuth, { NextAuthConfig, User as UT } from "next-auth";
    
    import Credentials from "next-auth/providers/credentials";
    
    import db from "@/utils/db";
    
    import User from "@/models/user.model";
    
    import bcrypt from "bcryptjs";
    
    const authOptions: NextAuthConfig = {
      secret: process.env.AUTH_SECRET,
      session: {
        strategy: "jwt",
      },
      callbacks: {
        async jwt({ token, user }) {
          if (user?._id) token._id = user._id;
    
          if (user?.username) token.username = user.username;
    
          if (user?.isAdmin) token.isAdmin = user.isAdmin;
    
          return token;
        },
        async session({ session, token }) {
          if (token?._id) session.user._id = token._id;
    
          if (token?.username) session.user.username = token.username;
    
          if (token?.isAdmin) session.user.isAdmin = token.isAdmin;
    
          return session;
        },
      },
      providers: [
        Credentials({
          name: "User",
          credentials: {
            email: {
              label: "Email",
              type: "email",
            },
            password: {
              label: "Password",
              type: "password",
            },
          },
    
          async authorize(credentials): Promise<UT> {
            await db.connect();
    
            const user = await User.findOne({ email: credentials.email });
    
            if (
              user &&
              bcrypt.compareSync(credentials.password as string, user.password)
            )
              return user;
    
            throw new Error("Invalid Email Or Password, Please Try Again.");
          },
        }),
      ],
    };
    
    export const { handlers, signIn, signOut, auth } = NextAuth(authOptions);
    

    /app/api/auth/[...nextauth]/route.ts

    import { handlers } from "@/auth";
    
    export const { GET, POST } = handlers;
    

    Middleware

    To use middleware, given the note mentioned, we need to make changes in our code :

    1. Remove mongoose and use static user data with a little change in /auth.ts

    /data/users.ts

    import bcrypt from "bcryptjs";
    
    const users = [
      {
        _id: 1,
        username: "P_Co_ST",
        email: "example@example.com",
        password: bcrypt.hashSync("654321"),
        isAdmin: true,
      },
      {
        _id: 2,
        username: "Harchi",
        email: "example@example.com",
        password: bcrypt.hashSync("123456"),
        isAdmin: false,
      },
    ];
    
    export default users;
    
    1. Add the middleware

    /middleware.ts

    export { auth as middleware } from "@/auth";
    
    1. Using the authorized async function in your callbacks

    /auth

    import NextAuth, { NextAuthConfig, User as UT } from "next-auth";
    
    import Credentials from "next-auth/providers/credentials";
    
    import userItems from "@/data/users";
    
    import bcrypt from "bcryptjs";
    
    import { NextResponse } from "next/server";
    
    const authOptions: NextAuthConfig = {
      secret: process.env.AUTH_SECRET,
      session: {
        strategy: "jwt",
      },
      callbacks: {
        async jwt({ token, user }) {
          if (user?._id) token._id = user._id;
    
          if (user?.username) token.username = user.username;
    
          if (user?.isAdmin) token.isAdmin = user.isAdmin;
    
          return token;
        },
        async session({ session, token }) {
          if (token?._id) session.user._id = token._id;
    
          if (token?.username) session.user.username = token.username;
    
          if (token?.isAdmin) session.user.isAdmin = token.isAdmin;
    
          return session;
        },
        async authorized({ auth, request }) {
          const isAuthorized = !!auth;
    
          const isPrivateRoute =
            request.nextUrl.pathname.startsWith("/admin/dashboard");
    
          const url = new URL(request.nextUrl);
    
          url.pathname = "/";
    
          if (!isAuthorized && isPrivateRoute) return NextResponse.redirect(url);
    
          return true;
        },
      },
      providers: [
        Credentials({
          name: "User",
          credentials: {
            email: {
              label: "Email",
              type: "email",
            },
            password: {
              label: "Password",
              type: "password",
            },
          },
          async authorize(credentials): Promise<UT> {
            const users: UT[] = userItems;
    
            const user = users.find((item) => item.email === credentials.email);
    
            if (
              user &&
              bcrypt.compareSync(credentials.password as string, user.password)
            )
              return user;
    
            throw new Error("Invalid Email Or Password, Please Try Again.");
          },
        }),
      ],
    };
    
    export const { handlers, signIn, signOut, auth } = NextAuth(authOptions);
    

    Note: In this function, we check whether the user is logged in or not If there is no login, redirect to the login page Otherwise, it is authorized.