
Join the Conversation!
Subscribing gives you access to the comments so you can share your ideas, ask questions, and connect with others.
"Please login to view comments"
Subscribing gives you access to the comments so you can share your ideas, ask questions, and connect with others.
Complete source code available till this point of lesson is available at
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
##Looks like we found a thief monkey By the way, I liked the trick how you reached till here. You have a good sense of humor. You will improve a lot if you join our course with this passion.
var
(function-scoped, outdated)let
(block-scoped, modern and recommended)const
(block-scoped, cannot be reassigned)_
, or $
let let = 5;
is invalid)myVar
and myvar
are different)string
, number
, boolean
, null
, undefined
, bigint
, symbol
Objects
, Arrays
, Functions
Subscribing gives you access to a brief, insightful summary of each lecture to stay on track.
00:00:02 In this lesson, we'll implement email password signup flow through server actions.
00:00:07 So let's head over to the lib folder and create a new folder called actions.
00:00:14 Within the actions folder, create a new file called auth.
00:00:19 And within here, we can export a new async function called signUpWithCredentials, just like so.
00:00:31 To this function, we'll pass some params needed to sign our user up, and we need to define the type for that param.
00:00:38 So let's head over to our action.d.ts file, which is where we define the sign-in with OAuth programs.
00:00:46 Within here, we can also define an interface of OAuth credentials, and we need a name to be able to authenticate the user.
00:00:55 We need a username of a type string.
00:00:58 We need an email of a type string.
00:01:01 And finally, we need a password of a type string.
00:01:05 So now we know that our programs have to be of a type of credentials.
00:01:11 Great.
00:01:11 Now we can start implementing it by saying that this server action will return a promise, which will resolve into an action response.
00:01:22 like this.
00:01:23 Within the function block, we can first try to validate the data we're passing into this function by saying const validation result is equal to await and
00:01:35 now we're going to call our action handler coming from dot dot slash handlers forward slash action and to it we're going to pass an object containing our
00:01:47 params and containing the schema based off of which we want to validate those programs.
00:01:52 So that's going to be a sign up schema coming from validations.
00:01:59 Can you now see how we're going to be using this action handler we created?
00:02:04 In a very simple way, we'll basically use it to validate the action we're trying to make.
00:02:10 So once again, we check whether the schema and params are provided and validated, we check whether the user is authorized,
00:02:18 we connected the database, and then we return the params in the session if all of those things are valid.
00:02:24 But if validation result is an instance of error, in that case we know that something went wrong, so we can return a handle error to which we pass the
00:02:38 validation result.
00:02:39 as error response.
00:02:42 But if we don't get into that if, then we get back all the params.
00:02:46 So let's destructure them.
00:02:48 By saying const, we'll get something from validation result dot params.
00:02:55 That something will be whatever we're passing into it, such as name, username, email, and password.
00:03:04 Let's also start a new Mongoose session as before by saying const session is equal to await mongoose.startSession.
00:03:15 We can import mongoose from mongoose right at the top.
00:03:19 And do you remember why we're starting the session?
00:03:22 Well, so we can start a new transaction.
00:03:25 So if something goes wrong in the middle of the process, we can stop this entire thing from running.
00:03:30 Now we can open up a new try and catch block.
00:03:33 In the catch, we can first abort that session by saying session.abortTransaction.
00:03:41 And then we can call the handle error, pass the error into it as errorResponse.
00:03:47 And we can also call a finally to end the session by saying await session.endSession no matter if it's exceeded or failed.
00:03:56 Now we can implement the try part of this block.
00:04:00 First, we want to check if a user with a given email already exists in the database within the session.
00:04:06 By saying const existingUser is equal to await userModel.find1 based on the email like this.
00:04:21 And we can also attach the session on it.
00:04:23 So if something breaks, we can stop it.
00:04:25 Then if an existing user exists, we're going to throw a new error saying user already exists.
00:04:32 Because this is sign up, we must not have a user with the same credentials.
00:04:36 Then we need to check whether the username they're trying to sign up with already exists.
00:04:41 By saying const existing username is equal to await user.find1 based on the username, and we also attach the session to it.
00:04:53 And if an existing username exists, then we throw a new error saying username already exists.
00:05:00 Finally, if we are ready to create a new user, we can create a new password.
00:05:06 Specifically, we have to hash it, so if somebody gets access to our database, they cannot see that password in plain text.
00:05:13 For that, we'll use a library called bcrypt.
00:05:17 It's a library that encrypts our passwords.
00:05:20 So let's install it by running mpm i bcrypt js.
00:05:27 And alongside that, we'll also need to install its types for TypeScript safety by running mpm i dash dash save dash dev add types forward slash bcrypt js.
00:05:40 Once you install it, we can hash our password by saying const hashed password is equal to await bcrypt.hash and then we pass the password and the salt,
00:05:56 which is the complexity which we want to use in hashing our password.
00:06:00 10 or even 12 is the default.
00:06:03 So let's import bcrypt right here at the top by saying import bcrypt from bcrypt.
00:06:10 And once we have the hashed password, we can create a new user by saying const destructure this new user is equal to await user.create.
00:06:23 And to it, we can pass an array with an object of username, name, and email.
00:06:31 And we can also attach that to a specific session so we can end it if something goes wrong.
00:06:37 Now, the reason why I had to destructure this user is because the user.create, when you pass an array to it, returns an array of all the newly created users.
00:06:47 So we had to destructure the first and only one.
00:06:50 But since we're only creating one in this case, we don't even have to pass an array into this.
00:06:56 We can just create one user, which means that that user will instantly be created.
00:07:00 So this is a simpler way of using the create method when you're only trying to create a single thing.
00:07:06 Once we create a user, we also want to create an account.
00:07:10 So let's say await account.create and we can pass an object of user ID is equal to new user dot underscore ID.
00:07:21 We can pass the name.
00:07:22 We can pass the provider.
00:07:24 In this case, it'll be credentials.
00:07:26 Remember, this is GitHub for GitHub.
00:07:28 It is Google for Google.
00:07:31 And if we use email and password, it is just credentials.
00:07:34 The provider account ID can be set to the email and the password will be set to the hashed password.
00:07:44 And don't forget to import the account from models.
00:07:48 Also, it seems like this new user underscore ID is complaining.
00:07:52 Let's see why is that.
00:07:53 Is it because they removed this array right here?
00:07:56 Maybe I made a mistake.
00:07:58 Let's go ahead and research the create method together.
00:08:01 This way I can show you exactly how I would approach researching something from the docs.
00:08:07 So if you head over here, we can see that we have a Mongoose.model, and you can use the .create functionality on that model to create new instances of
00:08:15 that model.
00:08:17 So if you want to create a single document, you can do that in a couple of ways by saying new something and then saving it,
00:08:25 or by calling the .create and then passing an object to it, which is exactly what I have done with this user.
00:08:34 Or you can use the insert many and then pass an array.
00:08:37 But in this case, I want to head over to the API and head over to the model API and search for create.
00:08:46 If you head over to the API, you can see exactly how a specific thing works.
00:08:51 So here it says that the first parameter is either an array or an object.
00:08:57 And it says documents to insert as a spread or array.
00:09:01 Okay, that's interesting.
00:09:02 And I want to know what does it return?
00:09:04 So it returns a promise, but what does that promise contain?
00:09:09 It should contain the new user, right?
00:09:11 At least that's what I'm hoping for.
00:09:14 Do they give me an example?
00:09:16 Let's see.
00:09:17 Well, they do talk about how I can call it by creating a new character, by just passing an object, which is what we have done,
00:09:25 but they don't really say what is included into this promise.
00:09:29 That's a shame, I really wish they told me what does it return.
00:09:32 What if we bring it back to what I had before, where I pass in an array right here, and then I destructure it from the array?
00:09:39 You can see that now, this new user is not actually complaining.
00:09:43 But I think that's just because it currently considers this new user to be a type of any, so it cannot actually complain.
00:09:50 This way, it is also just an array of any's.
00:09:53 In this case, the docs weren't enough.
00:09:55 So let's see if GitHub Copilot helps here.
00:09:57 I'll open up a new chat and pass this thing into it.
00:10:01 And I'll say, how does this new user look like?
00:10:07 In other words, what will be returned after we await the promise returned from the user.create.
00:10:20 Let's see what it says.
00:10:22 Okay, we're getting back an example, which is always good.
00:10:25 The new user object will be an instance of the user model, which includes the properties defined in the schema.
00:10:30 It says this typically includes the fields that were provided during creation and other fields such as underscore ID.
00:10:37 Okay, that's great.
00:10:38 So we know that it will contain the underscore ID, but what I'm more wondering about is why the TypeScript not understand that.
00:10:46 Might be because we actually have to specify it as I user doc coming from database user models.
00:10:54 What this does is it tells it that it will extend all the properties that a user has, such as name, email, bio, and more,
00:11:03 as well as all the properties that a Mongoose document has, such as the underscore ID, which is exactly what we want to do.
00:11:09 But in this case, our TypeScript is giving us an error, saying that this conversion doesn't really work.
00:11:15 Let's see if Copilot can help me fix it.
00:11:17 It's saying, to fix this, you need to use a create method by passing a session as an option within the object being called.
00:11:24 Oh yeah, how I didn't think of that before.
00:11:27 It's saying that it will possibly return any.
00:11:30 That's because we're using this transaction, which means if something goes wrong, this could actually result in being nothing.
00:11:38 If I remove that session, you can see that now this works like a charm.
00:11:43 It says that the document does indeed contain the document ID.
00:11:46 And I think even if I don't pass it, it won't complain.
00:11:50 Yep, that's true.
00:11:52 So if we do include a session in here, which we want to do to make sure that this is an atomic transaction, we might need to wrap this into an atomic transaction
00:12:02 as well.
00:12:02 By passing a second parameter over here, session is equal to session, which means that we might even be able to remove this iUserDoc.
00:12:11 Oh, nevermind, it still complains.
00:12:14 What if we tried fixing this using Copilot?
00:12:18 It says that the problem is that UserCreate should return a single document and not an array.
00:12:24 To fix this, you need to use the new user and save instead of UserCreate.
00:12:28 But that's weird, isn't it?
00:12:30 Didn't the docs tell me that I can just pass a regular object?
00:12:33 Well, maybe whatever we pass, it always returns an array of things created.
00:12:39 So we still need to destructure it.
00:12:41 Well, I'm not sure.
00:12:43 Let's see.
00:12:44 User.create, we have to add a weight in front of it.
00:12:48 No, if we do it like this, it just knows that this is a single document and not an array of documents.
00:12:54 But as soon as I add that session thing, it treats it as an array.
00:12:59 Oh, is that possible?
00:13:01 The reason why I had to turn this into an array is for it to actually consider this as an actual options we pass to the account create and not as another
00:13:14 document which we want to add.
00:13:16 Okay, I get it now.
00:13:17 So let me explain it from scratch.
00:13:19 This is a super cool thing that I haven't seen in a while.
00:13:23 So what's happening here is that the user.create is actually accepting either an array of documents or all the documents spread one after another.
00:13:34 Okay.
00:13:35 So you can either pass an array of documents like this, and then keep adding more within it.
00:13:40 Or you can pass a single one, and then you can pass a new one, and a new one, and a new one, and you can continue adding as many documents as you want,
00:13:51 spreading them all right here as separate parameters.
00:13:54 But the weird thing is, if you decide to continue spreading them, you can never get to the second parameter of the userCreate function,
00:14:02 the parameter called options.
00:14:04 Why can you not get to it?
00:14:06 Because if you continue spreading objects, it's never going to end, right?
00:14:11 So the only way to get to the optional options object is to turn the first thing into an array, no matter how many elements you want to insert.
00:14:21 So what you have to do is turn this into an array.
00:14:25 Even though we're inserting a single document, which immediately means that you have to destructure the user here, because now what is being returned is
00:14:33 an array of new documents created.
00:14:35 And now you can see that this is not complaining.
00:14:38 And then you can pass the options object as the second parameter.
00:14:42 You can see that now it recognizes it as the Mongoose create option.
00:14:46 to which we can pass this session, allowing us to turn this into an atomic transaction.
00:14:52 Okay, this was a good one.
00:14:53 And you can see that functions like this really excite me, which might be a bit problematic, but it is what it is.
00:14:59 And what's even better is that this has nothing to do with Mongoose.
00:15:03 Sure, they're implementing it in that way.
00:15:06 But this is just how pure JavaScript functions work.
00:15:11 You can make it accept an array of documents or just spread those documents.
00:15:16 And based on that, you can have different versions of those functions depending on the parameters that you passed into them.
00:15:23 And as you can see, only the options where the docs is an array actually have the options object.
00:15:30 The others don't.
00:15:31 So you can never get to the second parameter by spreading all the documents individually.
00:15:37 Pretty crazy stuff.
00:15:39 I haven't seen this in a long time, which is why I was so confused with how it was working.
00:15:44 But now it makes total sense why we're passing an array and why I have to do array destructuring right here.
00:15:51 This was some advanced JavaScript stuff.
00:15:54 Not Mongoose, not Next.js, just how pure JavaScript works.
00:16:00 And there's nothing that excites me more than that.
00:16:02 So I'll definitely cover this in the advanced JavaScript course.
00:16:05 With that in mind, the rest of the stuff is simple.
00:16:09 We can simply await the sign-in functionality coming from add forward slash auth.
00:16:14 To it, we can pass credentials because that is the provider we're using.
00:16:19 And then you can pass the email and the password as well as a redirect of false because we don't want to be redirecting.
00:16:26 Finally, once you sign in, we can return a success of true.
00:16:32 Now let's remove this unused import right here.
00:16:35 Now, why is this bcrypt still complaining?
00:16:39 We have installed both bcrypt as well as its types.
00:16:42 Oh, I know why.
00:16:43 This was supposed to be bcrypt.js.
00:16:45 With that, we're good.
00:16:47 So now let's actually call this signupWithCredentialsServerAction by heading over to our signup page.
00:16:55 And on submit, we can now call the signupWithCredentialsServerAction.
00:17:00 Right here, instead of this data promise resolve, I'm now going to call the sign up with credentials coming from lib auth actions.
00:17:11 We also want to head over into the auth form and modify the on submit type.
00:17:17 By saying that it'll be data T promise, and then action response instead of just returning success boolean.
00:17:24 So let's set it to action response.
00:17:28 And now we can actually call this on submit on handle submit in the submit handler.
00:17:33 We'll get access to the data.
00:17:36 And then we can get the result of this server action call by saying const result is equal to await on submit to which we pass the data as action response
00:17:50 if If there is a result, question mark dot success.
00:17:53 In that case, we want to return a toast component coming from add hooks use toast.
00:18:00 The title of the toast will be set to success and description will be equal to, well, depending on the form type.
00:18:09 So if form type is equal to sign in.
00:18:13 we can say something like signed in successfully else we can say signed up successfully finally at the end we want to navigate to the home page by using
00:18:23 the router functionality so let's say const router is equal to use router coming from not next router but next navigation this is a common mistake and
00:18:34 then right here at the bottom still within the if we can say router We're once again using that routes functionality.
00:18:47 Else, if it's not successful, in that case, we can return another toast with a title of error.
00:18:56 And we can be a bit more descriptive about that error by saying result.status.
00:19:02 Description will be result?error?message.
00:19:09 And the variant will of course be destructive.
00:19:13 There we go.
00:19:14 So let's properly close it.
00:19:16 And very soon we'll be able to sign up.
00:19:18 But before we do that, we'll have to add the credential authentication method in NextAuth.
00:19:24 So head over to auth.ts and add a new provider, which will be custom.
00:19:30 It'll be called credentials, and you can call it as a function.
00:19:35 Within it, I'll create a new async method called authorize, which is going to accept those credentials.
00:19:42 And here, we're basically working on custom authorization logic for credentials-based login.
00:19:48 The Authorize function is a part of the credentials provider in NextAuth.
00:19:53 It is specifically used for verifying credentials during sign-in, validating an email-password pair, and returning details if needed.
00:20:01 So let's validate them together by saying const validatedFields is equal to signInSchema.safeParse.
00:20:14 We parse the credentials, then if.
00:20:17 validated fields, In that case, we extract the email and password from the validated fields by destructuring it from validatedFields.data.
00:20:31 Then we try to get access to an existing account if it already exists.
00:20:35 By saying const data, which we rename to existing account is equal to await API.accounts.getByProvider to which we pass the email.
00:20:50 And this will be as action response, just so TypeScript knows what to expect.
00:20:57 And specifically of a type I user doc.
00:21:02 So we know exactly what is in there.
00:21:04 If we don't get back an existing account, we'll return null.
00:21:08 But if we do, we can try to get existing user based on that account by saying const destructured data as existing user and make it equal to a call of await
00:21:24 API dot users dot get by ID to which we can pass the existing account dot user ID dot to string.
00:21:35 And this will also be as ActionResponse iUserDoc.
00:21:40 Finally, if there is no existing user, we return null, but if there is one, we try to match the password that the user is trying to log in with.
00:21:49 So we can say const is valid password, and that'll be equal to await bcrypt.compare, where we compare the password with the existing account dot password,
00:22:04 which we know is there so we can put the exclamation mark at the end.
00:22:07 So let's import bcrypt from bcryptjs and the password is actually connected to an existing account, not user.
00:22:18 So it's good that we caught this.
00:22:19 And it looks like right here at the top, it's saying user ID does not exist on iUserDoc.
00:22:26 Yep, this was supposed to be iAccountDoc because first we're getting the account and then we're getting the user.
00:22:34 Finally, if the password is valid, so we can say if is valid password, in that case, we will return an object that'll contain the ID equal to existing
00:22:45 user ID, the name equal to existing user.name, the email equal to existing user, And finally, an image equal to existing user dot image.
00:23:00 Outside that if, if something goes wrong, we can just return null.
00:23:05 So yep, this is what it means to implement a custom authentication solution.
00:23:10 I actually think this return was supposed to go one level deeper.
00:23:13 There we go.
00:23:14 And now if we scroll up, we can see that credentials is complaining a bit here, saying, did you mean credential?
00:23:20 No, we did want to implement a custom credentials here.
00:23:24 So what you have to do is import credentials coming from next-auth forward slash providers, forward slash credentials.
00:23:39 There we go, and now it's no longer complaining.
00:23:42 And finally, we are ready to test out the email password flow to make sure everything works.
00:23:47 So, let me reload the page.
00:23:49 So, back in the browser, first of all, there's no errors, which is always a good sign, and we are ready to give it a shot.
00:23:56 This time, I'll use a different email.
00:23:58 So, I'll go with my JavaScript Mastery, 00atgmail.com, I'll enter some kind of a password with my name and with my username.
00:24:11 And I'll click sign up.
00:24:13 Okay.
00:24:14 Our password validation is working, which is good to know.
00:24:17 So I also have to enter one uppercase and one lowercase letter, as well as one special character.
00:24:23 That was super seamless.
00:24:25 The error was gone as soon as they did that.
00:24:28 So now I'll sign up.
00:24:31 And we got a 500. Schema validation failed.
00:24:35 This is an error coming from the logger.
00:24:38 So, let's try to figure out what this issue is all about.
00:24:41 If we head over to our Auth form, you can see that right here we're calling the OnSubmit.
00:24:48 This OnSubmit is just a prop that we pass into the Auth form from the SignUp page.
00:24:54 So right here on submit is basically just a sign up with credentials server action under the hood.
00:25:00 And did you hear what I said?
00:25:02 This is a server action.
00:25:04 And if it is, it must only be called on the server.
00:25:07 So it looks like I forgot to pass the use server directive right here at the top, which is mandatory for all of our action files.
00:25:16 Don't forget this because it might be very hard to debug this if you don't add it.
00:25:21 So now I'll head back over to the form.
00:25:24 I'll enter my JavaScript Mastery 00 at gmail.com email address.
00:25:29 I'll enter a password.
00:25:31 Let's also do a name that's going to be Adrian, and we can do a username of JavaScript Mastery, and I'll click sign up.
00:25:42 As you can see, it's signing up, but this time we got a different kind of validation error.
00:25:46 We get a bit more info saying that provider account ID is required, provider path provider is required, name path is required.
00:25:57 And the user ID path user ID is required.
00:26:00 If we head back over here, we have a couple of warnings.
00:26:03 The first one is saying that to pass a session to model.create, you must pass an array as the first argument.
00:26:11 Okay, this is good to know.
00:26:13 So let's see where we didn't pass that.
00:26:16 If we head over to here, we can search for user.create.
00:26:21 Here, we are passing an array indeed.
00:26:23 What about account.create?
00:26:25 Oh, yep.
00:26:26 Here, we need to turn this into an array just so we can get access to that second session parameter.
00:26:31 Okay.
00:26:32 Now that we resolved this warning, let's look into this error saying account validation failed for the provider account ID.
00:26:39 It's saying that it is required.
00:26:41 Same thing for provider, name, and user ID.
00:26:45 Let's see where we're passing all of those things.
00:26:48 We're passing them right here into this account create.
00:26:51 But I do think that we have those values, right?
00:26:54 They should be coming from the new user.
00:26:56 So what do you say that we just console log this entire object to see whether we actually have the values in there?
00:27:03 I'll just console log everything we're passing into the account create.
00:27:06 Once we do that, I'll just click sign up one more time.
00:27:09 And now we get a different error for some reason.
00:27:12 So I'll remove this console log from here.
00:27:14 And looking at this part right here, we're just trying to sign our user in right here.
00:27:18 And that's what actually breaks.
00:27:20 So at the end of the signup process, we of course want to sign our user in.
00:27:24 But what it's actually saying is, hey, I cannot sign in because that user you're trying to sign in wasn't actually created.
00:27:32 And you can see that in MongoDB Atlas or right here in this VS Code extension.
00:27:36 The user and the account are not getting created.
00:27:39 And now that I'm looking at it, I think I am missing the part where I commit the session.
00:27:45 That is the most important part of this whole function.
00:27:49 So you have to do something like await session.commitTransaction.
00:27:54 Why do we do that?
00:27:55 Well, if everything goes right up to this point, that means that everything did, in fact, go right, which means that we are ready to commit all of those things,
00:28:05 which means, hey, hash the password, create a user, create an account, and finally sign that user in.
00:28:12 So before signing in, you have to commit the transaction.
00:28:16 Now that we have this in, let's go back to the browser and let's try to sign up for one, hopefully, last time.
00:28:23 I'll press sign up.
00:28:25 And success!
00:28:27 Signed up successfully and we got redirected back to the homepage.
00:28:31 That means that the sign up has been successfully implemented.
00:28:35 And if we left that console log in the homepage, which I think we did, you can now see this newly updated session containing our new credentials part of
00:28:43 the information.
00:28:45 So now our users can successfully log in and register into the platform using OWA providers like Google or GitHub, or using their email and password.
00:28:55 Looking great.
00:28:57 So with that in mind, I'll close all of the currently open files.
00:29:01 Oh, I do notice that this file is red and we don't want to have any errors right here.
00:29:07 But this is just a small text issue right here, an apostrophe, which we have to escape.
00:29:12 So I'll say, don't, and then use the apostrophe sign right here.
00:29:16 With that said, we have seven file changes.
00:29:18 One is within the auth.ts file, which belongs to NextAuth, where we're adding this new credentials provider, basically implementing completely custom auth
00:29:29 into our application.
00:29:31 on top of the Google and GitHub providers.
00:29:33 Now, did this take some time to develop?
00:29:36 The answer is yes.
00:29:38 Was the logic fairly complex?
00:29:39 Well, yes, as well, especially if we wanted to make it as scalable and as error-prone as we did.
00:29:46 Could have you used something like Clerk to make it easier?
00:29:49 You could.
00:29:50 But that's not why you're here.
00:29:52 You're here to learn production-ready best practices in implementing authentication and everything else regarding your Next.js applications.
00:30:02 After that, we have also installed bcrypt, which is a package that helps us to hash our passwords.
00:30:07 We have also created a new server action called signupwithcredentials that does all the work for us.
00:30:15 And finally, we have hooked it up with our frontend form.
00:30:19 So with that in mind, I am ready to commit this by saying implement password and email auth, commit sync.
00:30:30 And with that, we are ready to do one last step of this authentication process, which is to implement the sign-in.
00:30:37 Now that we have the registrations with OAuth providers and email and password, implementing sign-in will be a breeze.
00:30:44 So let's do that next.