
No Comments Yet
Be the first to share your thoughts and start the conversation.
Be the first to share your thoughts and start the conversation.
Complete source code available till this point of lesson is available at
How did you manage to remove the blur property and reach here?
Upgrading gives you access to quizzes so you can test your knowledge, track progress, and improve your skills.
By logging in, you'll unlock full access to this and other free tutorials on JSM Pro.
Why? Logging in lets us personalize your learning experience, track your progress, and keep you in the loop with new workshops, coding tips, and platform updates.
You'll also be the first to know about upcoming launches, events, and exclusive discounts.
No spam—just helpful content to level up your skills.
If that sounds fair, go ahead and log in to continue →
Enter your name and email to get instant access
In this lesson, the focus is on implementing a functionality to add a user's OAuth account to a database during the sign-in process. The instructor details the steps involved in creating accounts and users based on the OAuth provider (GitHub or Google), handling multiple scenarios, performing database transactions, and ensuring data consistency with Mongoose sessions.
00:00:02 In this lesson, I'll teach you how to add the user's OAuth account to the database.
00:00:07 So once the user tries to sign in, we'll create both a new user and an account for that user.
00:00:13 So let me actually sketch that up right here in the readme.
00:00:16 If I head over right here, we have three different scenarios.
00:00:21 If a user uses GitHub OAuth, we'll create an account containing GitHub OAuth info, and then create a user with a GitHub name,
00:00:30 username, and image.
00:00:31 Okay, so first we create an account, and then we create a user.
00:00:36 If a user uses a Google OAuth, then we'll create an account containing Google OAuth info, and then create a user with Google name,
00:00:45 username, and image.
00:00:46 And finally, if a user uses GitHub OAuth first or Google OAuth first, and then the other one later, we'll create that OAuth account and then update the
00:00:56 user info to show the latest OAuth name and image.
00:01:00 The username will stay the same once it has been created.
00:01:03 It won't fluctuate.
00:01:04 I hope this makes sense.
00:01:06 And knowing this and understanding the edge case that we encountered, which might have actually been fixed by the time you're watching this video,
00:01:13 but even if it is, follow exactly what I'm doing.
00:01:17 Because it never hurts to know how to implement both API routes and server actions.
00:01:22 Each one has its advantages.
00:01:25 So in this case, we have to create a new API route.
00:01:28 So let's head over to app, api, auth, and let's create another folder right here and call it sign in with oauth.
00:01:41 And within this folder, we can create a new file called route.ts.
00:01:46 So it's auth, sign in with OAuth, and then route.ts.
00:01:50 Within here, we can export a new async function, post, because it's of a post request to create a new user that's going to accept a request.
00:02:02 of a type request right here at the top.
00:02:05 And in there, we can destructure a couple of things from the body, which we'll pass in.
00:02:10 So that's going to be equal to request.json.
00:02:14 Things that will destructure, which we'll later on pass, are going to be the provider to know which provider we're authenticating with.
00:02:21 Then the provider account ID, as well as the user object, the user data.
00:02:27 Finally, we can try to connect to the database by saying await dbconnect.
00:02:33 It is as simple as that, since we already have the functionality implemented.
00:02:38 And then we want to create a new mongoose session by saying const session is equal to await mongoose, which you can import from mongoose.startSession.
00:02:52 And don't forget to import mongoose coming directly from mongoose, but it's going to be just like this.
00:03:00 mongoose from mongoose no curly braces right there just a regular import so let's fix it here as well it's going to be just a regular lowercase mongoose
00:03:11 import Now, why are we starting this so-called Mongoose session?
00:03:14 What does it do?
00:03:16 Well, Mongoose sessions are a part of MongoDB's transactions feature, allowing multiple operations to be executed at a single atomic unit.
00:03:28 They ensure that all operations either succeed or none are applied, making them useful for maintaining data consistency in complex operations like multi-document updates.
00:03:41 What does it mean?
00:03:42 Well, it means that if we try to create a user, so let's put it this way, or an account, if we try to create an account,
00:03:52 and then later on we try to create a user based on that account, right?
00:03:58 If this one fails, that means that this one must also not happen at all, because we cannot create a user if an account doesn't already exist.
00:04:09 So by starting a Mongoose session, it allows us to start a new transaction by saying session.startTransaction.
00:04:17 And then if any properties in the transactions fail, we immediately fail the whole transaction altogether and cancel what has happened so far.
00:04:27 With me so far in programming, these are called atomic functions, functions that try to do either one thing or nothing at all.
00:04:35 Great.
00:04:36 Now that we have created this session, we can open up a new try and catch block in the catch.
00:04:42 We'll simply get an error of a type unknown.
00:04:45 And if we do get an error, we can await session.
00:04:50 that abort transaction, because obviously something went wrong.
00:04:55 So we want to roll back the changes we have made so far.
00:04:58 As we said, either all of them happens or none happen at all.
00:05:03 After that, we can return the call to the handle error functionality coming from lib.
00:05:09 And to it, we can pass the error, pass the API, and then say this is going to be an API error response.
00:05:17 Okay, great.
00:05:18 And we can also have a finally block.
00:05:21 So this will happen at the end, no matter whether it succeeds or not.
00:05:25 We're just going to end the session because we're no longer tracking for changes in that specific transaction.
00:05:31 So now let's try to validate the data first by saying const validated data is equal to, and right here, we have to actually create a new ZOT validation
00:05:42 for signing in with the OAuth schema.
00:05:45 So let's head over to validations.ts And this time we don't have the account schema or the user schema.
00:05:53 We want to create a new one.
00:05:54 And this one will be called sign in with OAuth schema by saying export const sign in with OAuth schema is equal to z.object to which we can pass a couple
00:06:10 of things.
00:06:11 If we're signing in with OAuth, then we have a provider.
00:06:15 In this case, it's going to be an enum, meaning one of the possible options, either Google or GitHub.
00:06:24 We also need to get a provider account ID, which is going to be of a type z.string.min1 provider account is required.
00:06:35 And finally, we have to get the user, which is going to be a bit different from the user schema we created above.
00:06:41 So we can say z.object that's going to have a name of z.string.min1 name is required.
00:06:49 username of z.string min3.
00:06:52 Username must be at least three characters long.
00:06:55 It's going to have an email of z.string.email.
00:06:59 Please provide a valid email address.
00:07:01 And finally, we're going to have an image that's going to be equal to z.string.url and something like invalid image URL.
00:07:12 and dot optional.
00:07:15 Great.
00:07:16 So now we have this sign in with OAuth schema, which we can try to validate right here by saying something like sign in with OAuth schema,
00:07:25 which we can import from validations, dot save parse, And to it, we pass the data, such as the provider, provider account ID,
00:07:35 and the user object.
00:07:37 Let's fix this typo right here.
00:07:39 And we can do a check right here by saying, if not, validated data dot success.
00:07:45 In that case, we can throw a new validation error.
00:07:50 Remember, that's the one we created before, to which we pass the validated data dot error dot flatten.
00:07:59 to flatten all the errors into a single array dot field errors, like so.
00:08:06 If everything succeeds, we can then destructure the values that we are passing about a user, such as the name, username,
00:08:14 email, and image coming from the user object.
00:08:18 Finally, we have to generate a new username.
00:08:21 Because even though we can just get it from here, that username can maybe contain some spaces, like this.
00:08:28 Or maybe some special characters which we don't support.
00:08:31 Or who knows what else, right?
00:08:33 So we have to somehow parse it into a thing that we can actually process.
00:08:37 And for that, I'll use a library called slugify.
00:08:41 So you can head over into your terminal and run MPMI slugify.
00:08:46 And this will help us to always make sure that our username follows specific rules, such as being lowercase with no spaces or no special characters.
00:08:55 Using it is super simple.
00:08:57 You can just say const slugified username.
00:09:02 Is equal to slugify, which you have to import right at the top by saying import slugify from slugify.
00:09:10 And then going down, we can pass to it.
00:09:13 The username we're trying to slugify as well as the options such as lower is true strict.
00:09:22 is true and trim is true as well.
00:09:25 It's better to stay on the safe side.
00:09:27 Now that we have this username, we want to check if a user already exists by saying let existing user is equal to await user object or should I say user
00:09:40 model dot find one and to it we can pass the email within an object.
00:09:45 Then we can say dot session and pass in this session we have created.
00:09:50 What does this mean?
00:09:51 Well, remember when we created this Mongoose session?
00:09:54 This is not regarding the user session.
00:09:57 This is regarding the Mongoose session.
00:09:59 So this is one of the actions that if another action we're trying to perform where the database fails, it'll actually fail this one as well.
00:10:08 So only if everything succeeds, then we can proceed to make those database changes.
00:10:13 Next, we can check if there is no existing user.
00:10:17 In that case, we can create a new user.
00:10:20 By saying const newUser is equal to await user.create to which we can pass an array and then an object containing the name,
00:10:32 username equal to sluggified username, email, and image.
00:10:37 Basically everything regarding this user.
00:10:39 Another thing we can do is actually ensure that this operation is also part of the Mongoose transaction.
00:10:46 So if it fails later on, we'll not actually create the user.
00:10:50 Next, we can create an else right here.
00:10:52 If it is an existing user, then we can create a new object called updated data to which we can define the type.
00:11:00 It'll have an optional name of string, an optional image of string as well.
00:11:05 Because if you remember, I told you that one of the rules of what we will achieve is that if a user has already signed in using a different OAuth provider,
00:11:14 then we'll upload its name and image with a new OAuth provider.
00:11:18 So we can say if existingUser.name is not equal to the name that it currently has, then we can pass it into this new updatedData object by saying updatedData.name
00:11:32 is equal to name.
00:11:34 And we can repeat the same thing with the image.
00:11:36 Finally, if object.keys of updatedData.length is greater than zero.
00:11:45 That means that we can update the user because there were some changes.
00:11:49 So only if changes were made, then we can await user.update1.
00:11:55 We're updating the one where the underscore ID is equal to existingUser.underscoreID.
00:12:02 and we're updating it with a $set property where we set it to the updated data.
00:12:09 And we can also chain the .session on it, so if something goes wrong, we can stop the session.
00:12:15 I hope this makes sense.
00:12:17 Now, we can go outside of this if-and-else statement and try to find the account for this user by saying const existingAccount is equal to await This time
00:12:29 we're using the account model dot find one, where the user ID is equal to existing user dot underscore ID, or we can search it by provider as well,
00:12:42 or we can search it by provider account ID.
00:12:44 And this will also be a part of the Mongoose session transaction.
00:12:49 Now, this existing user would only work if we're working with an existing user.
00:12:55 But if we have a new user, then we would have to replace this right here and say either new or existing.
00:13:03 What we can do instead is just do array destructuring here when we create a new user.
00:13:09 and just update the existing user variable, which is this one right here.
00:13:15 So instead of using const, we can actually use let.
00:13:18 And then instead of creating a new variable here, we can just reinitialize the existing user to be equal to this new user.
00:13:25 Because once we reach the second point, we don't really care whether this user already existed before, or not, it's important that we actually have the
00:13:33 user to which we can attach the account to.
00:13:35 I know this is a bit confusing, but bear with me.
00:13:38 Finally, if we don't end up creating the existing account, so if there is no existing account, finally, if the account doesn't yet exist,
00:13:47 so if there is no existing account, in that case, we can await account.create to which we pass an array of an object where user ID is equal to existing
00:14:04 user dot underscore ID.
00:14:06 We also can pass the name, the image, the provider and the provider account ID.
00:14:12 And we end up creating all of it.
00:14:14 We can also pass the session.
00:14:16 So if something goes wrong, we can stop it from being created.
00:14:19 So as you can see, we're first trying to sign our user in using OAuth.
00:14:24 If everything is good, we generate its username, then we try to find that user.
00:14:29 If a user doesn't yet exist, then we create it.
00:14:34 If they do exist, we simply update their data using the new OAuth provider.
00:14:39 Then we try to find the existing account attached to that user ID.
00:14:44 If the account doesn't exist, we finally create it.
00:14:46 And now at the end, we finally have to commit our transaction by saying await session.commitTransaction, because this is the only way in which we'll apply
00:14:57 all of the changes to the database atomically.
00:15:00 What does it mean atomically?
00:15:01 Well, it means that we will apply either all of them or none of them.
00:15:06 This is a super useful senior level concept that you need to know if you're applying for some jobs.
00:15:12 What does it mean for a function to be atomic or for a transaction to be atomic as well?
00:15:16 It means that it only does one thing.
00:15:18 And if anything goes wrong, it'll not actually apply it at all.
00:15:23 With that said, let's see if we have any errors right here at the top.
00:15:26 Oh, it looks like I missed adding the await right here.
00:15:29 We have to await the request.json.
00:15:32 Great job if you spotted that before.
00:15:34 If not, I think now we're good with this new post request that allows us to create accounts using OAuth providers such as Google or GitHub.
00:15:43 Now let's head over to our lib API.ts and let's mention this new API.
00:15:51 by creating a new object called auth.
00:15:53 And within auth, we can say oauth sign in, which is going to be a function that takes in a user, a provider, and the provider account ID.
00:16:07 These are going to be of a type sign in with oauth params.
00:16:14 So this is another interface which we can create.
00:16:17 You can create it either right here in types global, or we can create a new file called action.d.ts.
00:16:25 Again, the name doesn't really matter here, but here we can create a new interface called sign in with OAuth params.
00:16:34 And it's going to take in a provider, which will be either of a type GitHub or type Google.
00:16:40 It'll also request a provider account ID of a type string.
00:16:44 And finally, a user containing its email, name, image, and finally, username.
00:16:52 Now it knows what we're trying to get.
00:16:54 So let's end it by opening up a new function block.
00:16:57 And within it, we can call the fetch handler, which we worked so hard to create.
00:17:03 And it's going to make a call to API base URL, forward slash auth, forward slash sign in with OAuth.
00:17:13 You have to name it exactly as you named this file right here.
00:17:17 Sign in with OAuth or folder, should I say.
00:17:21 Once you do it that way, you can provide the payload, the JSON payload that we're destructuring at the top of that file.
00:17:27 So you can say method will be set to post and the body will be equal to JSON stringify where you pass the user, the provider and the provider account ID.
00:17:37 And that's going to look something like this.
00:17:40 So this is just a helper to allow us to more easily call this API route.
00:17:46 So now we can head over to the Auth.ts file, which we created before.
00:17:51 And here we'll be able to add callbacks, which will make sure that our users are authenticated.
00:17:56 The callbacks will be called whenever a user signs in using any provider.
00:18:01 And within them, we'll have to check if the sign-in account type is credentials, If yes, then we skip it.
00:18:08 We'll handle it the other way around when doing email password-based authentication.
00:18:12 But if the account type is not credentials, then we'll call this new sign in with OAuth app and create OAuth accounts.
00:18:21 So let me teach you how to do that right away.
00:18:23 Right below the providers, I'll add the callbacks functionality right here, which is basically just an object.
00:18:29 It's the callback that decides what's going to happen after using a sign in with specific OAuth or credentials, allowing us to do some further verification
00:18:38 or decide if the auth flow should continue or not.
00:18:42 So here we can create a new async function called sign in.
00:18:47 That's going to take in an object of user profile and account.
00:18:53 So we're just destructuring that and then if.
00:18:57 account?type is equal to credentials.
00:19:02 So we're trying to log in with the email and password.
00:19:05 We'll return true, meaning just let me pass.
00:19:08 Let me continue doing the auth in some other way.
00:19:10 But if there is no account or no user, then the user cannot sign in.
00:19:17 So we'll return false.
00:19:18 After that, we have to gather all the user info by saying const user info is equal to name is user.name and I'll add the exclamation mark at the end.
00:19:31 So we let TypeScript know that we indeed know this name will be here and make sure to use the uppercase I for the user info.
00:19:39 Then we pass the email, we pass the image, and finally we pass the username which will be equal to if account.provider is triple equal to GitHub,
00:19:52 then we'll get the username through profile?login as string.
00:19:59 Else we'll get it from user.name?toLowerCase as string.
00:20:07 So this is different depending on whether it's GitHub or Google.
00:20:10 And this was supposed to be toLowerCase.
00:20:13 So let's fix that just like so.
00:20:16 Now, after we have the user info, we can call our API by getting the response.
00:20:22 And that's going to be equal to await API coming from lib API dot auth dot oauth sign in.
00:20:33 And then to it, we can pass the user, which is going to be equal to user info.
00:20:38 And we can also pass the provider, which is going to be equal to account dot provider.
00:20:43 and we have to let it know that it'll either be a string of GitHub or a string of Google.
00:20:49 Nothing else.
00:20:50 Finally, we also need to pass a provider account ID coming from account.providerAccountId as string.
00:21:01 And then all of this will be as action response, because it's an actual API call.
00:21:07 What this OAuth sign-in returns is actually going to be a response.
00:21:12 And from here, we can destructure the success property, because we know that every API call returns the success, which is either boolean true or false.
00:21:22 And we know that because we have created the way in which this API response is returned to us.
00:21:27 So finally, we can say if there is no success, return false, else return true.
00:21:33 And in this case, we don't have to say as string for account provider ID, but we do have to fix some warning right here saying that we're converting the
00:21:42 type right here.
00:21:42 So let's try to understand what it is saying here.
00:21:45 It's saying that the conversion of type void, which means that this function is actually not returning anything, but we're trying to say that it'll return
00:21:54 an actual action response, may be a mistake, because neither type sufficiently overlaps with the other.
00:22:01 If this was intentional, convert the expression to unknown first.
00:22:06 Okay, but why would it say that this API call response would be nothing, void?
00:22:12 Well, let's go into it.
00:22:13 I'm going to command-click it and look into this function to see what we're returning.
00:22:18 And if you pay close attention to this, even though we're calling this fetch handler, we're not returning its response.
00:22:25 Whereas for all the other ones, we are not opening up a function block.
00:22:31 Rather, we're using the instant return functionality.
00:22:34 If something is just a single line, or if you omit using the curly braces, then it means that it is an instant return.
00:22:42 And here, I forgot to do that.
00:22:44 So what you can do is either add the return here, or just automatically remove the curly braces, which will count it as an instant return.
00:22:52 And now if you go back, you'll see that it actually knows that this will return.
00:22:56 the action response type, which has the success, data, and more.
00:23:01 Great.
00:23:01 This is a case where TypeScript really saved our ass.
00:23:04 Now that we have this async sign in functionality, let's also create two additional callbacks.
00:23:11 One for the JWT, JSON Web Token, and one for the session.
00:23:15 To customize the way in which our JWT is returned, and then we can modify that session to the frontend.
00:23:22 So let's add them above by saying async session, which takes in an object of session and token, and simply adds the user ID to the session by saying session.user.id
00:23:37 is equal to token.sub as string.
00:23:41 Finally, we can return the modified session like so.
00:23:46 And now we can also add the async JWT functionality that accepts the token and the account.
00:23:54 And here, we want to check if an account exists.
00:23:57 In that case, we can try to get an existing account by provider by saying const.
00:24:04 destructure it and call await API.accounts.getByProvider.
00:24:14 If you remember well, then you'll know that this is a helper for one of the account routes that we have created before.
00:24:21 which calls the account provider and tries to check whether the existing provider already exists.
00:24:27 To it, we can pass either the email or the account ID by saying if account.type is triple equal to credentials, meaning email and password,
00:24:37 then we can pass over the token.email and add an exclamation mark at the end.
00:24:43 Else we can pass the account.providerAccountId.
00:24:47 That API call as any other returns the data.
00:24:52 So we can say data and rename it to existing account and success.
00:24:58 We can also close this API response right here in a set of parentheses Just so we can give it a type as action response,
00:25:08 specifically the one that returns the I account doc.
00:25:13 So this is going to be the account interface.
00:25:16 Finally, if there is no success, meaning something went wrong.
00:25:22 Or there is no existing account.
00:25:25 So we can say no existing account.
00:25:29 Then we're going to return the existing token without modifying it.
00:25:32 But if we do have an existing account, then I'll try to get the user ID by saying existing account dot user ID.
00:25:40 And if a user ID exists, then we'll say token.sub is equal to user ID dot to string.
00:25:48 So we're trying to get the user ID and attach it to a token.
00:25:52 Finally, we can return that token that now has been modified and it includes the new user ID.
00:25:58 This was a bit complicated.
00:26:00 I know we're doing enterprise level authentication here by making sure that all of the callbacks, all of the middleware and everything else is there in
00:26:10 order to run this smoothly.
00:26:12 Now we are ready to test out if everything works well.
00:26:16 So let's head over to root page, which is our homepage.
00:26:20 We had this test route.
00:26:22 Let's remove that test for the time being, and let's head over to the top of this file.
00:26:27 Instead of a waiting test, we can say something like const session is equal to await auth, which we simply call like this,
00:26:37 and we can import it from add forward slash auth.
00:26:41 Then let's simply run a console log where we can console log the session and actually console log the session variable.
00:26:51 So now if I head back over to our homepage and open up the terminal, you can see that the session is obviously null because we haven't tried to sign in before.
00:27:01 But if I head over to sign up or login and I try to log in with either one of these providers, let's go with Google first.
00:27:10 You could see access denied.
00:27:12 You do not have the permission to sign in and then another error.
00:27:15 And what if we try GitHub?
00:27:17 If I head over here, we get the same thing.
00:27:20 So let's go ahead and debug this together so that we can finally create our user and its corresponding account in the database.