您好,在本文中,我们想探索如何将 NextAuth 与 MongoDB 和中间件结合使用。
“注意:如果你想在 Mongoose 中使用中间件,这是不可能的。”
Mongoose 目前不支持 Next.js Edge Runtime。虽然您可以在 Edge Runtime 中导入 Mongoose,但您将获得 Mongoose 的浏览器库。 Mongoose 无法在 Edge Runtime 中连接到 MongoDB,因为 Edge Runtime 目前不支持 Node.js net API,而 MongoDB Node Driver 使用 Node.js net API 连接到 MongoDB。
https://mongoosejs.com/docs/nextjs.html
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: "[email protected]",
password: bcrypt.hashSync("654321"),
isAdmin: true,
},
{
username: "Harchi",
email: "[email protected]",
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
和 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;
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: "[email protected]",
password: bcrypt.hashSync("654321"),
isAdmin: true,
},
{
username: "Harchi",
email: "[email protected]",
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
和 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;