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.”
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.
npx create-next-app@latest app
npm install zod
npm install mongoose
npm install next-auth
npm install bcryptjs @types/bcryptjs
/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;
/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;
/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);
}
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;
To use middleware, given the note mentioned, we need to make changes in our code :
/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;
/middleware.ts
export { auth as middleware } from "@/auth";
/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.