I am trying to create a role based google signIn in a nextJs 13 app which uses next-auth such that it calls 2 different Tables to create the user based their role.
I'm using mongodb/mongoose for DB. The model for user is :
import mongoose, { Schema, model, models } from "mongoose";
const userSchema = new Schema({
email: {
type: String,
required: [true, "Email is required"],
unique: [true, "Email already exists"],
index: true,
},
username: {
type: String,
required: [true, "Username is required"],
match: [/^[a-zA-Z0-9]+$/, "Username is invalid"],
index: true,
},
image: {
type: String,
},
userType: {
type: String,
enum: ["typeA", "typeB"],
required: [true, "User type is required"],
},
});
const typeASchema = new Schema({
typeA: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
},
other1: [{
type: mongoose.Schema.Types.ObjectId,
ref: "other_table1",
}],
});
const typeBSchema = new Schema({
typeB: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
},
other2: [{
type: mongoose.Schema.Types.ObjectId,
ref: "other_table2",
}],
});
const User = models.User || model("User", userSchema);
const TypeA= models.TypeA || User.discriminator("TypeA", typeASchema);
const TypeB= models.TypeB || User.discriminator("TypeB", typeBSchema);
export { User, TypeA, TypeB};
Then in my /app/api/auth/[...nextauth]/route.js, I define :
import NextAuth from 'next-auth';
import GoogleProvider from 'next-auth/providers/google';
import { connectToDB } from '@/utils/database';
import {User, TypeA, TypeB} from '@/models/user';
function getRandomAvatarURL() {
}
const handler = NextAuth({
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
})
],
callbacks: {
async session({ session }) {
const sessionUser = await User.findOne({
email: session.user.email
})
session.user.id = sessionUser._id.toString();
session.user.image = sessionUser.image;
return session;
},
async signIn({ profile, userType}) {
try {
await connectToDB();
// check if a user already exists
const userExists = await User.findOne({
email: profile.email
});
// if not, create user and add to db
if(!userExists) {
const name = profile.name.split(" ");
const firstName = name[0] || ".";
const lastName = name.slice(1).join(" ") || ".";
const username = `${firstName}${lastName}`.replace(/\s/g, "");
if (userType === "typeA"){
await TypeA.create({
email: profile.email,
username: username,
image: getRandomAvatarURL(),
userType: "typeA",
})
} else if (userType === "typeB"){
await TypeB.create({
email: profile.email,
username: username,
image: getRandomAvatarURL(),
userType: "typeB",
})
}
}
return true;
} catch (error) {
console.log(error)
if (error.code === 11000){
console.log("Unique constraint violation error! Username already Exists!")
}
return false;
}
}
},
})
export { handler as GET, handler as POST };
In my Nav component I call the function like:
//imports
const Nav = () => {
// other code ...
const handleSignin = async(providerId, userType) => {
console.log('provider.id:', providerId); // returns correctly
console.log('userType:', userType); //returns correctly
await signIn(providerId, { callbackUrl: "/dashboard"}, {credentials:{userType: userType}});
}
//...
return(
//....
{providers &&
Object.values(providers).map((provider) => (
<button
type="button"
key={provider.name}
onClick={() => handleSignin(provider.id, "refree")}
>
<Image src="/assets/icons/signin.svg" width={20} height={20} className="rounded-none" alt="signin" />
</button>
))}
);
};
export default Nav;
I tried all the possible combinations for front end and backend, some of which are:
Frontend:
const handleSignin = async(providerId, userType) => {
await signIn(providerId, { callbackUrl: "/dashboard"}, {credentials:{userType: userType}});
}
const handleSignin = async(providerId, userType) => {
await signIn(providerId, { callbackUrl: "/dashboard"}, {credentials:userType});
const handleSignin = async(providerId, userType) => {
await signIn(providerId, { callbackUrl: "/dashboard"}, {userType: userType});
}
const handleSignin = async(providerId, userType) => {
await signIn(providerId, {userType: userType, callbackUrl: "/dashboard"});
}
Backend:
async signIn({ profile, userType}) {
try {
await connectToDB();
console.log(userType)
async signIn({ profile, credentials}) {
try {
await connectToDB();
console.log(credentials)
It always returns undefined. I can't seem to capture the arguments passed from frontend to backend. Any help or pointer is deeply appreciated. The flow works when instead of passing userType argument I simply try to create the user of any one type on signIn.
The third argument of signIn
in nextauth, authorizationParams
, is passed to the provider(google), not to the signIn
callback.
You can make a custom provider and get the userType
from the user
of the signIn
callback as follows:
providers: [
{
id: "googleUserA",
name: "GoogleUserA",
type: "oauth",
wellKnown: "https://accounts.google.com/.well-known/openid-configuration",
authorization: { params: { scope: "openid email profile" } },
idToken: true,
checks: ["pkce", "state"],
profile(profile) {
return {
id: profile.sub,
name: profile.name,
email: profile.email,
image: profile.picture,
userType: "userA",
};
},
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET,
},
{
id: "googleUserB",
name: "GoogleUserB",
type: "oauth",
wellKnown: "https://accounts.google.com/.well-known/openid-configuration",
authorization: { params: { scope: "openid email profile" } },
idToken: true,
checks: ["pkce", "state"],
profile(profile) {
return {
id: profile.sub,
name: profile.name,
email: profile.email,
image: profile.picture,
userType: "userB",
};
},
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET,
},
],
callbacks: {
signIn: async ({ user, profile }) => {
console.log(user.userType); // userA or userB
...
And FrontEnd will be like...
signIn("googleUserA", { callbackUrl: "/dashboard" });