I am trying to authenticate using next-auth v4 inside nextjs13. My aim is to store the user's email, name, image and Id from the auth provider (Google) into a MongoDB database. However, I keep getting error saying "OAuthAccountNotLinked".
The URL on the browser address bar also changes to "http://localhost:3000/login?callbackUrl=http%3A%2F%2Flocalhost%3A3000%2Fauth%2Flogin&error=OAuthAccountNotLinked"
Here are my various files:
app/auth/[...nextauth]/route.js file
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import User from "../../models/User";
import bcrypt from "bcryptjs";
import dbConnect from "../../lib/dbConnect";
import GoogleProvider from "next-auth/providers/google";
import { MongoDBAdapter } from "@next-auth/mongodb-adapter";
import clientPromise from "../../lib/mongodb";
const handler = NextAuth({
adapter: MongoDBAdapter(clientPromise),
session: {
strategy: "database",
},
callbacks: {
async signIn({ user, account, profile }) {
console.log("user: ", user);
console.log("account: ", account);
console.log(profile);
return true;
},
},
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
CredentialsProvider({
name: "credentials",
async authorize(credentials, req) {
await dbConnect();
const { email, password } = credentials;
if (!email || !password) {
throw new Error("Email and password required");
}
const user = await User.findOne({ email });
if (!user) {
throw new Error("Invalid Email or password");
}
const isPasswordMatched = await bcrypt.compare(password, user.password);
if (!isPasswordMatched) {
throw new Error("Invalid Email or password");
}
return user;
},
}),
],
pages: {
signIn: "/auth/login",
},
secret: process.env.NEXT_AUTH_SECRET,
});
export { handler as GET, handler as POST };
dbConnect.js
import mongoose from "mongoose";
const MONGODB_URI =
process.env.SERVER === "dev"
? "mongodb://127.0.0.1:27017/mosque-around-me"
: process.env.MONGODB_URI;
console.log(MONGODB_URI);
if (!MONGODB_URI) {
throw new Error(
"Please define the MONGODB_URI environment variable inside .env.local"
);
}
/**
* Global is used here to maintain a cached connection across hot reloads
* in development. This prevents connections growing exponentially
* during API Route usage.
*/
let cached = global.mongoose;
if (!cached) {
cached = global.mongoose = { conn: null, promise: null };
}
async function dbConnect() {
if (cached.conn) {
return cached.conn;
}
if (!cached.promise) {
const opts = {
bufferCommands: true,
};
cached.promise = mongoose.connect(MONGODB_URI, opts).then((mongoose) => {
return mongoose;
});
}
try {
cached.conn = await cached.promise;
} catch (e) {
cached.promise = null;
throw e;
}
return cached.conn;
}
export default dbConnect;
mongodb.js (MongoClient promise)
// This approach is taken from https://github.com/vercel/next.js/tree/canary/examples/with-mongodb
import { MongoClient } from "mongodb";
if (!process.env.MONGODB_URI) {
throw new Error('Invalid/Missing environment variable: "MONGODB_URI"');
}
const uri = process.env.MONGODB_URI;
const options = {};
let client;
let clientPromise;
if (process.env.NODE_ENV === "development") {
// In development mode, use a global variable so that the value
// is preserved across module reloads caused by HMR (Hot Module Replacement).
if (!global._mongoClientPromise) {
client = new MongoClient(uri, options);
global._mongoClientPromise = client.connect();
}
clientPromise = global._mongoClientPromise;
} else {
// In production mode, it's best to not use a global variable.
client = new MongoClient(uri, options);
clientPromise = client.connect();
}
// Export a module-scoped MongoClient promise. By doing this in a
// separate module, the client can be shared across functions.
export default clientPromise;
user.js (User model schema):
import mongoose from "mongoose";
import bcrypt from "bcryptjs";
// const jwt = require("jsonwebtoken");
const UserSchema = new mongoose.Schema(
{
name: {
type: String,
minlength: 2,
maxlength: 50,
required: [true, "Please provide first name"],
trim: true,
},
phoneNumber: {
type: String,
},
email: {
type: String,
match: [
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
"Please provide a valid email",
],
required: [true, "Please provide an email"],
unique: [true, "Someone is alreay using this email"],
},
authProvider: {
type: String,
required: [true, "Auth provider required"],
},
password: {
type: String,
minlength: 8,
required: [true, "Please provide password"],
},
location: {
type: String,
minlength: 3,
trim: true,
default: "my town",
},
lga: {
type: String,
minlength: 3,
trim: true,
default: "my town",
},
state: {
type: String,
minlength: 3,
trim: true,
default: "my town",
},
country: {
type: String,
minlength: 3,
trim: true,
default: "my town",
},
verified: {
type: Boolean,
default: false,
},
active: {
type: Boolean,
default: true,
},
verificationCode: {
type: String,
length: 4,
},
image: String,
role: {
type: String,
required: [true, "Please provide user role"],
default: "user",
enum: {
values: ["staff", "admin", "user"],
message: "Please select valid role",
},
},
},
{ timestamps: true }
);
// hash the password before saving it
UserSchema.pre("save", async function (next) {
// exit when login with google and facebook
if (!this.password) return;
// exit the function when other fields are updated
if (!this.isModified("password")) return;
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
});
UserSchema.methods.comparePassword = async function (userPassword) {
const isMatch = await bcrypt.compare(userPassword, this.password);
return isMatch;
};
// export new User model if not created already
export default mongoose.models.User || mongoose.model("User", UserSchema);