Search code examples
javascriptreactjsnext.jsnext-auth

How to pass arguments to Google signIn in NextJS backend?


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.


Solution

  • 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" });