So let's go ahead and let's learn how to update the settings and how to update the active session of this user because just modifying the database is not enough. We also have to update all of those libs and hooks which hold the current session. So I want to start by creating a schema. Let's go inside of schemas here and let's export const settings schema which is going to be an object and for now we are only gonna try and update the name field. So go ahead and add z.optional because user doesn't have to always update that name.
And it's gonna be a string. Like that. And now let's go ahead inside of our actions and let's create a new file called settings.ts. Let's mark it as use server. Let's import everything as z from Zod.
Let's import the settings schema. Let's import the database. Let's import get user by id from data user and let's import current user from libauth And now let's export const settings to accept the values which are a type of z.infer type of settings schema. And in here let's get the user using await current user. If there is no user, we are going to return an error, unauthorized.
And we also have to turn this into an asynchronous function. So make sure that you add asynchronous here. So this await doesn't have an error. And now we checked for this user here but let's also confirm that they actually exist in the database and it's not some leftover session. So let's do const database user is going to be await get user by id using the user.id here.
If there is no database user we can return the exact same thing. So if there is no database user return unauthorized like this and then let's go ahead and let's do await database.user.update where we have a matching ID of database user.id and the data is simply going to spread all the values with us and at the end Let's go ahead and let's return success settings updated like this So now let's go back inside of our app folder protected settings page right here and we can remove this too, so we no longer need that And let's go ahead and remove the imports as well. So just leave use client. And instead, let's go ahead and import from add slash components UI card, the card, card header and card content. Let's go ahead and replace this entire thing to use the card element.
Let's max the width to 600 pixels. Let's add a card header with a paragraph, which is simply going to say settings and a little settings emoji. And let's simply style it so it looks the same as our other titles. There we go. And now inside of CardContent, I simply want to add a button component.
So go ahead and import a button from Components UI button. We're going to add a button and say UpdateName. So for now, we're just manually going to try it like this. And let's add on click to call the settings server action so import this and let's manually pass in the values for name to be new name and now let's use this on click and let's pass on click here and let's also import from react useTransition so I want to do this so I can see when it's pending so const isPending startTransition from useTransition let's wrap this inside of startTransition like this and then we can use this is pending to disable this button so we know something is going on. There we go.
So open up your Prisma Studio, make sure it's running and confirm that you have only one, only that you don't have any users which have the name of new name. So right, we are trying to update this so make sure that you only have some other names inside of here. And now we're going to try and click on update name here. There we go, see it's completed. And now I'm going to go ahead and refresh this.
And there we go, you can see that my name in the database has updated to new name. But if I go back to my server right here, you can see that my name in the database has updated to new name but if I go back to my server right here you can see that my name is still my old name tutorial if I go into the client My name is still my old name tutorial here. So what we have to do is we also have to manually update the session every time we change a specific field. So for that I want to go back inside of auth.ts and in here this is what we're going to do. We're going to add this little console.log here.
I am being called again. Like this. And let's go ahead and open our terminal so we can see exactly when this is being called. And this is what I want to do now. You have two options to update the session so I'm going to show you both of those.
So you can do it completely using client-side inside of settings page right here. If you want to you can do the following. You can import use session from next out react and then in here you can go ahead and use this session. And you can destructure, update. So what you can do is call .then here, and then manually update the session like this.
So let's take a look at it now. I'm going to click save here. And once this is completed, you can see that it's being called up all the time. So it's constantly being updated. But still, even though it's being updated, you can see that my names are simply not changing.
They are still staying at my old names even though I'm obviously updating as you can see in my database. My name is no longer tutorial. So how do I update my name? Well we have to do the following. We have to go inside of our token here and we have to assign the name manually.
So token.name is existingUser.name. And while we are here, let's also do token.email to be existingUser.email. Let's also go ahead, I believe this is all we need. So name, email, role is the factor. I think that's the only thing we're going to update from the settings.
And then we have to do the same thing here. So if session.user, session.user.name is token.name. And let's also do session.user.email is token.email. Like that. So let's try it out again.
I'm going to go ahead here. And you can see how it immediately works now because our token now is passing the new values name and email. So the moment I added that even without testing my name has been updated. So this is what I want to do now. I want to go back to my settings page and change this to say something different.
So let's confirm my name is still my old name but if I click on update name here, wait for it to confirm, there we go. My name has been updated in real time to something different. Great, so now we are ready to actually create the form. So let me just remove the console log from my token here. We no longer need this.
Do I have a console log? Okay, I no longer have a console log. Great. But just before we do that, on the settings page, we cannot show the same settings to all users right so if the user has logged in using Google or GitHub we have to show them different settings right because they cannot change their password for example because they don't have a password. They also cannot change their email because it's linked to the account model in the database.
So what I want to do is I want to go back to auth.ts here and I want to fetch the account. So let's go ahead and create data account.ds and let's import the database from s-lib database and let's export const getAccountByUserId using the user ID which is a string. Let's make this an asynchronous function. Let's open a try and catch block. Let's return null here.
And in here, let's get the account using await database.account. Find first where we have a matching user ID. And let's return account. Like this. So make sure you have this little util.
Now let's just visit our nextout.d.ts and let's add a new field here called isAuth to be a boolean like this. And now let's go back inside of auth.ts and inside of this token after we confirm that we have this user, let's get the existing account. Using await, get account by user ID and let's pass in the existing user.id. And then what we're going to do is add a token, isOauth is going to be existing account. But we are going to turn that into a boolean by adding double exclamation points at the end.
And then in our session here, we can do session.user.isOauth to simply be token.isOauth. And let's add as boolean here to get rid of this TypeScript error. Great, so now we have that. And now we are ready to go back inside of our app, protected settings page right here, and we are ready to create our form. So let's go ahead and import everything as Z from Zod.
Let's go ahead and let's import useForm from React HookForm. Let's go ahead and let's import ZodResolver from HookFormResolver.zod Let's import our setting schema here from schemas and Let's go ahead and let's import ZodResolver from HookFormResolver.zod. Let's import our setting schema here from schemas. And let's go ahead and let's import everything we need from components UI form. So we are going to need the form element, the form field, form control, form item, form label, form description and form message to show the errors.
Message. And we are also going to need a separate input component from components UI input like this. Now let's go ahead and let's actually define our form. We use form. And we have to give it a type of z.infer type of settings schema and in here let's go ahead and let's write a resolver the Zod resolver and pass in the settings schema and let's go ahead and let's give it a default values of name that's the only one which we have defined.
So for now leave it as empty, later we are going to fill it with the actual user name. And let's change this on click to instead be on submit which accepts the values which are type of z.infer, type of settings, schema. And then we are simply going to pass in the values here inside, like that. And let's also prepare some states here so import use state from react so we can show the errors and success messages so const error set error use state and let's go ahead and give it a type of string or undefined and let's do the same thing for the success message and then in here we're not always going to fire the update instead if we have data.error, we're gonna show setError to be data.error and if we have success, we're going to call the update and setSuccessDataSuccess like this. Great!
And you can also add a .catch here manually to set error something went wrong so if we don't catch something in our server action we have a fallback for this. Great and now we can remove this here and instead what we can do is we can render our form element and we can spread the form inside and then we can add a native form element which is going to have a class name of space y6 and onclick which is going to be form handle submit and let me just collapse these fields form handle submit our on submit function so we successfully passed those values here. Now inside of the form let's add a self-closing tag form field which is going to have control of form.control, is gonna have a name of name, which is the field we are trying to update. And it's gonna have a render from where we are going to destructure the individual field. And then we can render the form item with the form label which will say name Then inside we can add a form control and we can add our input component finally and we can spread this field prop which we destructured above and let's give it a placeholder of John Doe and we can use the disabled prop to be isPending which we get from our start transition here and the structure is pending here like that.
And now let's go outside let's go and wrap this form field inside of a div like this. And let's give it some different spacing, So space y4 because we're going to have multiple inputs inside and outside of this div now render a button Which is simply going to say save and let's give it a type of submit like this So now it's time for us to give this an actual default value So why is this submitting when I click on it? Something seems a bit wrong. Let me just see on submit values. So this seems to be immediately submitting once I click on that.
So let's see what I did wrong. Oh, it's because in the form I use on click, it should be on submit. My apologies. So in your native form, make sure you use on submit. Great.
So now when I click, it doesn't submit. So now we have to fill this with the currently logged in user information and for that we can use our hook. So let's go ahead and get the user using use current user from hooks use current user. And in here, let's go ahead and add user question mark name or undefined. So don't put an empty string because then that will update the Prisma inside of our settings.
You can see that we just spread the values so it's gonna receive name to be an empty string and it's going to change that in the database but if you give it explicitly undefined then it's not even going to add name field to the values here So that's why I want you to do it like this. And now if I refresh here I believe I should have my name here but I don't seem to have that. Let's see why that is happening. So I'm gonna go ahead and console.log the user to see if we have some issues here. So I have my user and my name seems to be empty.
I believe that's because I just submitted the name from before. Yeah, because we had that onSubmit function on click and then we had a default value of an empty string which submitted it. All right, so yeah, we made a small mistake. You probably have the same thing if you do, no worries. So let's try and change our name.
So I'm going to call this new name change and let's go ahead and save. And let's see if that's going to update our properties. If I go to server components, there we go. New name change, client component, new name change, settings, new name change. If I refresh here, there we go, new name change, perfect.
You can see how now it's working just as fine. So the reason our name was empty is because by default I was holding an empty string here and inside of this form I had on click. So when we clicked on the field, it fired a submit action and it changed the name to an empty string. So just bring it back to this and you can freely update your name. Let's go ahead and try this one more time.
If I click save here, this is going to update my name. And there we go, server and client components are updated. Great. So now I want to go ahead and actually show these errors and success messages. So let's go ahead and Let's import form success from form success.
And let's do the same thing for form error to be form error. If we have an error in any case. And above this button here, let's enter form error and have a message of error. And let's do the same thing for form success to have a message of success and let's give this button a disabled if is pending. So now when I update my name again I should get a success message that everything was okay and that my settings are updated.
Great! So now we have to add some fields from chat.cnui so that we can update the other stuff. So let's add the fields switch and select. So inside of our terminal here, I'm going to shut down the app. I'm going to shut down my Prisma Studio.
Now I'm going to write npx chat-cnui latest add switch. So that's going to be on or off for two-factor authentication. And after switch, we also have to add select so we can change our user role. Obviously, changing the user role in settings is just for development, right? Usually you do that using direct database access or by implementing an API to update the roles.
So that's a new how you're going to handle that. So just refresh your page if you've shut down the app. And now let's go ahead back inside of our schema so that we can add those new fields properly. So let's go inside of schemas right here and let's go ahead and let's add is to factor enabled to be z.optional and z.boolean. Then let's go ahead and add a role to be z.enum an array of user role from Prisma Client so make sure you add this import .admin and user role .user and now let's add an email field to be z.optional z.string.email and password is going to be z.optional z.string.minimumOf6 and a new password is going to be z.optional, it's going to be the exact same thing and now here's a cool thing with Zot we can use dot refine to check if the password and the new password match.
So if data.password Sorry, not if they match, but we can check that by default they are optional. But if the user enters a new password, they also have to enter their current password. So let's get data here dot password. So if data dot password is entered and data dot new password is not entered return false. And we can do if data dot new password is entered and there is no data.oldPassword also return false otherwise return true and we can write custom messages for that by adding a little comma here and opening up an object.
So message is gonna be new password is required and path is gonna be new password. Right? Or You can write two defines if you want to. For example, you can chain this refine with another one and this one is going to check if new password is written and old password is not. And then change this one to password is required and the path to be password like that.
So then you handle both cases in the refine here. Great, so we have added everything we need inside of our settings schema. So now let's go ahead and let's go back inside of our app protected settings page right here and we have this existing form filled so now it's time for us to add a new form field to change the email. So let's copy and paste this and let's go ahead and modify our default values for the email. You can see how now we have that to be user.email or undefined.
Alright, and now in here change this one to be the name of email, to have a type of a label of email. Let's change the placeholder to be John Doe example.com. And let's give it a type of email. And now, as you can see, we have our email field here, just refresh. And this will give you the current email that you're logged in with.
So when you're testing this, make sure that you're logged in with the credentials user. So don't use OAuth providers for this part. All right, so we have that. And now let's go ahead and let's add a form filled for the password. So let's copy this.
Name is going to be password. And we're going to have a password here. And let's go ahead and give this a type of password. And let's go ahead and give it a placeholder of 123456. Like this.
And by default, let's add password to be undefined. So we're not going to fill the password because we don't have the user's password we only have the hash so by default we're going to put it as undefined meaning if user saves this form we are not going to update the password at all. So this is for the password field and now we have to copy this field and paste it. And this one has to be new password. So this is going to be new password.
And let's go ahead and modify that as well. So new password is also undefined like this. There we go. So now we have the fields to update the name, the email, the password and the new password. So now what we have to do is we have to create a form which is going to be able to change the role for.
So in order to do that, we have to import some things from our new component which we added called select, UI select. So just confirm that you've added that, right? We added npx chatcny select and switch. Make sure you run those commands. So in here import select, select content, select item, select trigger, and select value.
And now we can go back and actually create this. So let's copy the last form field which is the new password form field. Let's change this one to have a name of role. And let's clear up the form item. So let's just add form label, which is going to be role.
And then inside we're going to use the select component. And let's give it a prop disabled, off is pending let's give it on value change to be field on change and let's give it a default value of field.value and inside let's open up a form control and let's add a select trigger with a select value, which is gonna be a placeholder of select a role. Outside of select trigger, sorry, outside of select, oh, outside of form control, add a select content, which is gonna have select item, which is going to have, it's not gonna be a self-closing tag, so like this. The string is gonna be admin as an option, and we have the passing the value to be user role, which we can import from Prisma Client so make sure that you import this So this one is going to select admin and then you can copy and paste this and this one will select role, sorry user and the label is going to be user like this. Great, so now you can see that we have an option to select the role.
Perfect. So let's go ahead inside of our default values here and let's add the role. So role is gonna be user role or undefined as well. Like that. So when I refresh here, I believe this should be pre-selected.
For me, it's admin. I might have changed it from the last time. Also, if the form is too big for you, if you can't scroll up or down, you can just zoom out. You know, I'm not really making this responsive or anything. Great.
So one thing I've noticed we are missing is the form message which will show an error. So we are not using the form message anywhere as you can see. So let's go ahead and simply add it to the fields we need. So first we have the name field. So outside of form control add form message.
So this will handle any errors. And let's do the same thing for the email outside of form control for the password same thing and for the new password same thing and finally for here for here we have to put it, I'm not exactly sure where, I think we can just do it outside of select, like this. So now, for example, if I enter a password, there we go, you can see that new password is required. Or if I do the opposite thing, let me refresh. If I do the opposite thing and enter this password, then the password is required.
So our refine is working quite nicely. Great. And there's a last field which we have to allow for updating which is the two-factor authentication. So for that we have to import switch component. So import switch from components UI switch.
And let's go down here and copy this field, form field, new password. So this one with the item label control because the select you can see that it's quite custom, right? So we have to, we're going to have to modify it. And let's change the name of this one to be is to factor enabled. Like that.
And let's go ahead now and let's modify this a bit. So I'm going to remove everything inside of form item here. And I'm going to give this form item a class name. Flex, FlexRow, ItemCenter, JustifyBetween, RoundedLarge, Border, Padding3, and ShadowSM. Item a class name of flex flex row item center justify between rounded large border padding three and shadow sm and then inside I'm going to open up a div with a class name space y point five and I'm going to add a form label component to say, to factor authentication.
And in here, I'm gonna add form description to say, enable to factor authentication for your account. And outside of this div, we can add a form control and simply render the switch component inside. It's a self-closing tag. And let's give it a disabled prop of is pending, a checked of field.value, unchecked change to field on change. Like this.
There we go. We can now modify this and we can save all of those things. But obviously we have to modify our settings a bit. Because right now I can change my email to whatever I want without any confirmation. Same thing for the password.
We are not exactly checking if they are matching. So let's go ahead and revisit our settings action and let's modify it so that it can actually handle all of those different changes because when we enter a new password we have to hash that password right? And let's go ahead and do the following so the first thing I want to do is I want to check if the user who is trying to change these fields is logged in using credentials or if they're logged in using OAuth So in here after we check that we have a database user let's check if user is OAuth. Let's go ahead and do the following. Values.email which is a field they should not update is automatically going to be undefined.
Values.password will be undefined. Values.newpassword will be undefined. And values.isToFactorEnabled is going to be undefined. So these are the fields that OAuth users cannot modify because their email is handled by the provider, they don't have a password and two-factor is also handled by their provider. So it makes sense that if we disable those fields in the API or the server action we also hide those fields from them right here on the client side.
So let's go ahead and do that. So I'm gonna find the field which I don't want to show. So we don't want to show the email and the password. So let's find where the email starts right here. And let's do if user isOauth is equal to false.
Then we're going to show this form field, which has the email, this form field, which has the password, and this form field, which has the new password. So all the way until we get to the role. So obviously we have an error because we don't have a parent component. So wrap the entire thing inside of a fragment. All the way to the end here of the new password.
And then if you want to, you can indent that so it looks a little bit better. Like this. So right now everything should look exactly the same, right? So I'm going to log out and I'm going to log in using my GitHub. And now those fields should be hidden from me because I'm logged in using OAuth.
Looks like they're not hidden. That's very interesting. Oh, let me just refresh this. Perhaps the logout didn't work exactly as expected. So let me try logging in again, because I logged in as I stayed logged in as the previous user.
There we go. Now when I'm in OAuth you can see that I can only see my name and my role and I should also hide the two-factor authentication so let's do that as well. I'm just going to copy this query here. I'm going to find my field for the two-factor authentication and I'm only going to render it if OAUT is false. So right here to the end.
Like this. And then I can indent that as well and no need for a fragment here because it's a single field. There we go. So OAUT users should only be able to update their name and their role. Perfect.
So I suggest that you log out now and log in back to your credentials. So we can continue developing our settings. Great. So you can see how we have much more options here as credentials. And in here, we confirm that if client side somehow fails and they send updates for this, we will never allow them to update those.
Great, so now we have to do the logic if the user is trying to update their email. So if values.email and if values.email is not the same as user.email so we are only going to send a new verification token if the user is trying to update an email which is different from what it was before. So let's do const existing user to be await get user by email from data user so we have to confirm that the email they are changing to isn't used by another user so values.email. If we have an existing user by that email we are going to return an error which is going to say email already in use And we also have to confirm that we are not that user. So existing user and if existing user.id is not the same as our user ID.
So now if I try and use an email from my other account, I believe I should get back an error because I'm trying to take someone else's account. There we go. So email already in user. So what I meant to say was email already in use. There we go, so you can see how that now has switched, is protecting my other users.
So try going to your database and take a look at another email that you have and try to use it here and you should be prevented from updating to that email. There we go, you can see how when I refresh it's back to normal. Great. And now if we pass all of that verification, we have to create a new token for them to verify. So const verification token is simply going to be await generate verification token from lib tokens right here make sure you import that and pass in values.email so we know to which email to send that and then let's do await send verification email so make sure you add this import from libmail and in here we're gonna pass verification token dot email as the first argument and verification token dot token as the second argument like that And let's break this function and let's write success verification verification email sent.
Like this. So if I try to update my email to something else gmail.com in production this is going to send the email verification to this email. There we go verification email sent. Right now this email is not being sent because remember we have to add a domain to resend so that we can send emails to anyone else. Great so we just handled the case for emails but how about for passwords?
So we have to do the same thing for passwords now before we update it. So if values.password and if values.newPassword and if the database user has a password at all in that case first we have to check if they have entered a correct password so const passwords match is going to be await bcrypt so let's import bcrypt again you can use bcrypt or bcrypt.js I'm going to use bcrypt.js here. Bcrypt.compareValues.password with the database user password, which is a hash. So we are going to know whether the password is correct or not without actually knowing what is the password. So if the passwords don't match, we're going to return an error incorrect password.
Like this. Otherwise, we have to hash the new password. So hashed password is going to be await decrypt.hash values new password and 10 salt rounds like this. And then we can manually modify values.password which will be updated here in the database to be the new hashed password and let's do values new password to be undefined because we don't even have that field in our database. Great, great job!
So now if I try and enter a wrong password here and try to update some other password I believe I should get an error because I don't have a matching password, right? So let's try this out. Let me just refresh here. Should I've gotten an error here? I believe I should have.
So let's try it one more time. So password here, I'm gonna change this to something wrong. My new password, let's click save here and there we go. Now I have incorrect password. So just make sure you refresh because if you have that email here then it's gonna send you a verification for that email.
But now if I try and use 123456 and my new password is going to be 654321, let's click save and then I should get settings updated like this. Great! And obviously we have to remove the error from when we submit. So let me try and log out now and let me try tutorial mailing at gmail.com. Let me try my old password, 123456.
So if I try to log in with that, wrong, 654321. If I try to log, tutorial mailing at gmail.com. If I try to log in with this, so my new password there we go. It is officially working. Perfect.
And now let's just confirm that our roles are working. So right now I'm an admin, but if I change this to user and if I click save, let's see what's going to happen. There we go. I don't have permission to view this and I should get forbidden API route. In here I'm getting forbidden server action if I change it back to an admin and click save and then try again there we go allowed API route and allowed server action and lastly let's change two-factor authentication So you know that I just logged out and logged in without two-factor enabled.
So let's try enabling this now and let's click save to see if we're gonna get any errors or if this is going to work just fine. So if I refresh here or simply go to the server for example. There we go, two-factor authentication is now on. Perfect. So let me go ahead and log out and let me try and log in.
So tutorialmailingatgmail.com 123, no, wrong password, 654321 because I changed my password. So let's try that out. Now it should prevent me from logging in and it does perfect and I have a new email remember this one only lasts five minutes so make sure you enter this fast. Let's go ahead and confirm it and if this is still working I should be redirected back to my settings page and looks like I am. Great, great job and looks like one thing here is not updating correctly.
So my two-factor authentication seems to not be updating here. So it's on here, but here it's off. Oh, yeah, I think inside of our page, we forgot to add the default value for that. Yes, we don't have a default value for two-factor authentication. So let's add is two-factor enabled, user is two-factor enabled or undefined.
Like that. So I think that now when I refresh here, there we go. You can see how now it's turned on. Great! So everything is working just fine.
I think this was a very cool exercise. We did a big project but we exclusively focused on you know mastering NextOut and all the ways you can do things. So yeah about this update thing I'm not even sure if you need it or not so It's very inconsistent on my part. I actually, when I developed this the first time, I did not need to do this. So I didn't need to manually update my name and email every time in this sessions and callbacks.
So it's very inconsistent. I'm not sure how it works but this is you know a bulletproof way for you to know that it's been updated but it's always updated once you log out and log in. And here's another thing that you can do so you don't have the update on the client you can also extract update from here and then let's go back inside of app protected settings page. Let's remove the update from here and let's remove the import of use session here. We no longer need that so let me find there I go remove this import.
Let's go back inside of our actions settings. So imagine if this is your route handler, you can do this as well. So you can now also import update from out, I believe. And then simply at the end of this thing after you update the user in the database call the update here manually or maybe this is not how it's working so perhaps I need to do this const user like that or let's call this new user, updated user. And then in here, name, updated user.name.
Is this working? Wait, what is this accepting? It's accepting a user. Oh, so I have to write the user manually and then name is going to be updated user.name, email, updated user.email. What else do we have?
Is to factor enable, updated user is to factor enabled. And we also have role, updated user role. So you can do it like this if you want as well is that all the fields we have let me just quickly check we also have the password yes so but I mean password really doesn't matter You don't even have to update that. Yeah, we don't store that in our session. So I think that this should still be working now.
So if I try changing my name, let's try update server test and click save here. I think this should still work just as fine. Let's go ahead and see. There we go. So name is update server test.
Yeah, you can see how, oh, you can see how on client it didn't update. Yeah, so maybe it's not the perfect solution. So yeah, you can see there's a lot of things you have to take care of when working with NextOut. It's obviously a great library, but yeah, sometimes it's just a little bit inconsistent. So yeah, bring back use session here in the settings page, bring back the use session hook and call the update here.
And then, well, you can leave it here as well if you want to update it from the server side as well. Great, so we've wrapped that up. What's left is to deploy our actual application. So deployment is pretty easy. The only thing that we have to take care of is the Google and GitHub callbacks.