So now that we've wrapped up OAuth providers for our app, let's go ahead and let's implement credential login. So for that we do have the new documentation here but it is missing some info from here. So that's why I also have old documentation here. So what I want you to understand is this. So CredentialsProvider can only be used if JSON Web Tokens are enabled for sessions.
And They don't have that stated in the new documentation or at least I could not find it. Maybe it's written in a different way. But the issue is that we now have to change our strategy. We have to explicitly write our strategy this time. So this is what I want you to do first.
I want you to be logged in. So make sure you are seeing this screen. And let's go inside of the app folder inside of page.tsx. And this is what we are going to do. So after here, let's go ahead and let's add this session await out so the thing we already did some time ago and let's go ahead and let's JSON stringify our session so let me refresh this and there we go So as you can see inside of here without doing anything I have the ID inside of my current user session.
So that's because by default we are using a database strategy. But now we're going to have to do a modification inside of this auth right here. So inside of this file auth.ts. Let's go below the pages and let's add strategy here. My apologies, session, strategy and now instead of database let's write JSON web token and now we're gonna have to log in again so I'm gonna go ahead and simply sign in with my github and first of all you're going to notice that now something is different.
So now we don't have that ID. And ID will be crucial for us to have whenever we want to get currently logged in session. Because that's how we are going to fetch everything associated with that user. So that's what I wanted to explain to you so that you understand why we are going to do what we're going to do next, right? So let's compare this one more time.
You can see that in here I only have the name, the email and expires, right? But if I change this to database and refresh and log in again you're gonna see that I have much more info like id session token and things like that right but we also know that if we want to use the credential login we have to use JSON Web Tokens. So this is what we're going to do. Go ahead and finally change the session strategy to be JSON Web Token. After that go ahead and log in so that you can see what you're working with here.
What we're going to have to do now is we're going to have to add some callbacks. So basically, we're going to have to learn how to extend our current session so that even if we are missing the ID initially we can still add it. And there actually are instructions on how to do that in the new documentation here. I'm not sure exactly where it is. Let me close the providers.
Maybe in the guides. There we go. In the guides we have extending the session with JVT. So they have instructions here on how to add ID to JVT type of strategy. So let's go ahead and do that.
Inside of callbacks here, we're gonna go ahead and first define JVT. We're gonna destructure the token and the user from here and if we have the user this if checks are important so make sure you add them. If we have the user we're gonna assign token.id to be user.id and remember to return token so we get rid of the errors here. So that's for the JWT and now we also have to modify the session here. The session gets the session and the token and remember token is now modified because it has the ID so this token now holds the ID so if we have token.ID again make sure you add these if clauses to the session.user.id we are now going to add token.id and return the session.
But now we have a problem here because our types are not set up for this exact configuration. And luckily, there are also instructions on how to fix that. Now I'm not entirely sure if that is inside of here, but it might be here when you click on TypeScript, they teach you how to extend and how to add custom fields to the session. So I'm just guiding you and explaining that you have all of those information in the documentation as well, right? So now let's go ahead and let's fix this TypeScript error here.
And I think this is actually enough for us even though we have the TypeScript error. If I refresh here, actually let's try the following. Let's try and log out first. How about we do that? So API out, sign out.
I'm going to log out and then I'm going to log in again. And I think that this time, there we go. You can see that now I have my ID. So just make sure you log out and log in because the JSON Web Token needs to be refreshed. And you can see that now I have ID in my session just as I had with my database strategy.
So that's the first thing that we had to do, right? Now we have the same functionality from this session await out, which is very important for us. And we also have the strategy JWT, which enables us to add credentials in the providers. And now let's just go ahead and quickly fix the types here. So this is what I want to do.
Declare module next-out JSONWebToken and inside of here modify interface JVT to hold ID which is a string or undefined. And let's just see I can't do that here all right and we also have to do this we have to import jvt from next out jvt and then this error goes away so yes this is an unused import but if you don't add it this is an error and once you have this and once you have this declared module you can see that now ID is a string and token.id is a string so you no longer have these errors here. Great! So now we are ready to add our credentials here. So we're gonna start very simply.
Let's go ahead and let's add the actual credentials, right? I'm going to add credentials from NextAuthProviders credentials, like this. And what you do usually is you pass in the credentials right like this right let's go ahead and let's make one thing easier for us let's add them as first item in the array so that we can then you know extend them from here so go ahead and execute them and add the options inside of them like this or you know we could just properly collapse this array like this there we go and inside of the credentials, I'm only gonna do this. I'm gonna open the object credentials and I'm gonna add email to have a label of email. Whoops, my apologies.
And type of email. And then I'm going to have the password, which will have a label of password and type of password. So Why am I adding this? Where is this exactly visible? Why is that inside of my documentation here?
So let me go back to credentials provider. So they teach you the same thing here and instead they just use you know username instead of email. Why is that? Where is this used? Well in order to take a look at that we have to revert our change for pages so don't worry just for now comment the pages out Make sure you just have the commas here and here so you have no errors.
And what I want you to do is to log out. So go ahead and go to API out, sign out. Confirm. And now you will be redirected to this page. And as you can see, we now have GitHub, Google and credentials and this is the default next out login screen.
So how do we get the email and the password field? Well that's what this is used for. So if we change this to username and give the type of text and refresh this page now it becomes username. If you hide Google and GitHub from here then and refresh here you can see they are not visible here. So that's what this is used for right now of course bring back Google bring back GitHub and refresh their back here and change this to email and change this type to email as well so that's what this is used for So technically we don't even need this, right?
But I'm just going to leave it here because I just told you we don't need it. That's not true. I'm not sure whether this is actually used for some other things inside of credential login, but I do know that it definitely controls the inputs which are visible here. So now we can go ahead and bring this back on and try and refresh again and there we go you are now redirected to your login page here. Since we use custom login pages we cannot rely on these things here.
We're gonna have to implement our own login fields, right? But here's the problem. We now also have to prepare another thing. An asynchronous method called Authorize. And this method will have the credentials and what I'm going to do is for now I'm going to return null here but what I want you to do is to add a console log of the credentials like this, right?
So we technically can't do anything at the moment this is basically the place for us to write our custom login logic right, because we have to do that Nextcloud does not do that for us. But this is enough for us to try and log in and hopefully we will see this on the server logs. So let's go back inside of our sign-in card. So if you don't remember, this component is inside of our Features, Auth, Components, Sign-in card. And in here I'm going to add the import for input from components UI input.
I'm also going to import useState from react. Let's go ahead and let's prepare email setEmail whoops from useState let's make it an empty string by default, and password, setPassword. And now let's also create an onSubmitForm, which we're going to call onEmail, or we can do onCredentialSignIn, like this. And the event will be a react form event, HTML form element. Now, let me just fix the types here.
I meant the syntax. So since this will be called by a form event, we have to prevent default here. And then we're gonna go ahead and call credentials we're gonna call the sign in method but usually inside of here we pass you know github or Google this time we're gonna pass credentials like this and then we're gonna pass email to be email password to be password and callback URL to go to slash And now let's go inside of the card content here and we're gonna add a form element. Let's give the form element on submit on credential sign in. Let's give it a class name of space Y 2.5 and inside of form we're gonna go ahead and add an input component.
The input will have a value of email on change which will take the event and call set email event target value we're gonna give it a placeholder of email a type of email and it's going to be required. And then we can copy and paste this input, give it the value of password, call setPassword, change the placeholder to password and the type to password as well. And finally, we're going to have a button component, continue. We're going to give this button a type of explicit submit, a class name of width full, and a size of Large. There we go.
So now you should have your own email and password inputs here. And one thing I also want to do is import the separator component from components UI separator and we are simply going to add that right here where the form ends and our social providers begin. Just so it looks a bit better. Now I want you to open your terminal and focus on where your app is running. Right, so localhost 3000, where you do npm run dev or bun run dev.
And because in auth.ts we added an authorize method which when the credential login is attempted it will simply log the credentials we should see our input here so I'm gonna go ahead and give this some space and let's go ahead and write testmail.com 123 and let's click continue and we should see it somewhere. So of course we have an error here but let's hopefully find something useful here. There we go TestMail.com password 123. So we are able to infer what we pass from our custom login page into this authorize method. Great.
Of course we get an error because we return null, which is technically equivalent of an error, because if user enters an email and password and we don't return the user, that means the user was not found, meaning either the email is incorrect or the password is incorrect. But that is all we can do here at the moment because what we have to implement first is the register functionality. We're gonna start with the UI first. So for now you can close the out file and I want you to copy the form element from here. From sign-in card.
So the start of the form element to the end of the form element. And I want you to go inside of the sign up card right here. And right above this div or simply inside of card content paste those elements and you can indent the beginning of the form here. So now let's also copy the imports so we are going to need separator button and input. You can add that here.
Like this we already have button great. And let's also copy this. Right here. We also need the useState from React so let's make sure we add that and besides that we are also going to need this. OnCredentialSignIn.
We're gonna call it onCredentialSignUp and what we are gonna do is simply E.preventDefault nothing more. Now let's copy the onCredentialSignUp and let's use that in the form instead. And now in between where the form ends and this div encapsulating our OAuth provider begins, we can add the separator component. I believe we have also added the name port from that. So we are doing this inside of the sign up card now.
So now if you click on sign up, you should also have email and password here. But we are going to add one more field here which will be name and set name. And now let's go ahead and let's copy this input one more time. The value here will be name, we're gonna call setName and the placeholder here is simply gonna be fullName. And the type will be text.
So all of these are going to be required. Great. So now we have the UI finished and now we have to implement our sign up functionality. So for that we have to visit back our schema. So let's close everything and let's go inside of source database schema and find the user database, the user table, my apologies.
There we go. So we have ID, name, email and image. And now we are going to add password. Password will not be required and password will not actually be the password. Password will just be hash.
So if you want to you can also call this hash, right? But I like to call it password. What's important is that you never actually store password inside, right? So it is a text and we are going to call this field password. Now that you added that, we have to run our fun little commands.
So let me close this. Let me shut down this app. We have to run bun run database generate, bun run database migrate. There we go. So now if you take a look in your drizzle here you have a second query which simply adds the column password and the password again is not going to be the actual password is just going to be the hash and now if you run band database studio here and I think I already have it open right here in the user column you should now also have password there we go but it is null, right?
So we were able to easily migrate this because passwords are not required so our existing tables were not really affected. So we easily did that migration. And now let's do bun run dev again and now what we're going to do is we are going to create an endpoint so that users who want to create an account with credential login get this property or this column filled with actual hashing algorithm. So let's go inside of our HONO, so source app API, inside the route here let's create users.ts. Here's a quick tip do not call it auth.
Do not add a prefix auth. That's because auth API slash auth is already taken so it will simply conflict and it won't work so do not call it out go ahead and call it users well the name of the file doesn't really matter right but when you add it here don't call it out don't do that right So we are gonna call it users. The reason I chose the name users for this endpoint is because that's what it's doing. It will create a user. So let's go ahead and let's import Hono from HONO and let's write const app new HONO and export default app.
Now let's go inside of the route and we can now import users from users and we can now chain users and users. And now let's go inside of here. And before we begin working here, I want to install a package which we're gonna use for encryption. So go ahead and run bunadbcryptjs or npm install bcryptjs. And I believe we also need to add the types for it I'm just not entirely sure yes we also need the types so bun add the add types bcryptjs or npm install dev dependencies bcryptjs And then you can run dev again.
Great. So inside of here we're going to have a post method. So make sure you chain a post method here and we are also going to validate our fields so for that we're gonna need Zod and we are also going to need zValidator from HonoZotValidator. So let's go ahead and let's add zValidator here. We are validating the JSON field and the JSON field will accept the name of the new user which will be a string, the email of the user which will be an email and the password which will be a string and you can of course add your own rules here minimum of 3, maximum of well why would we add a maximum right but yeah let's go ahead and add a maximum of I don't know 20 like this and now let's also add the actual controller so asynchronous controller here which has the context will do the following from c.request.valid.json we are going to extract now validated name, email and password.
And now let's go ahead and let's create the hashed password. So we're gonna do await bcrypt.hash and we have to import bcrypt so let's go ahead and add import bcrypt from bcrypt.js like this So bcrypt.hash password and the second argument is the salt length. So let's go ahead and put 12 inside. And now let's go ahead and do await database which you can import from at slash database drizzle dot insert into the users table which you can import from at slash database drizzle dot insert into the users table which you can import from at database schema right here. The following values.
We're going to add an email name and password will be hashed password. So we are not going to store the actual password that the user has written in the input. We are storing the hash in our database. So even if our database gets hacked, no user will be compromised in a sense that all of their passwords will now be known. So this is especially useful and well required because users reuse their passwords.
So you don't want to be responsible for them having to have all of their accounts compromised if your database gets hacked. By using hashes in your database, you don't have them playing in the open in text fields. And let's simply return c.json null and 200 like this there we go so now let's go ahead and let's do the following So I'm gonna go back inside of my sign up card and I'm gonna add the same rules for the password here. So I'm gonna give it the minimum of three and the max of 20, right? So we also want those rules here.
So they are matching and I'm just not sure if it's min length or yes I think it might be min length and max length. We're gonna try with this and then we're gonna change it. I think I have my app running so let's actually try it out immediately. I'm just gonna refresh my page here. So let's see if the min length is working.
So I have a name here. I'm gonna add an email here and if I just write one there we go. So please lengthen this text to three characters or more. Great, so now it's working. Perfect, now let's go ahead and let's turn this new route which we've just created in the users in a React query mutation.
So we're gonna go inside of our features and we're gonna add this inside of the Auth hook, inside of the Auth feature even though we call this users you know we know we're only gonna use it for Auth and the reason we call it users is to avoid conflicts with you know existing out API which comes from next out so let's just simply add our hooks here so multiple Inside of here. I'm simply going to add use sign up Dot TS and let's see if we can copy an existing mutation like useGenerateImage. Let's copy this and let's paste it inside of useSignUp and this is what we are going to change. So we are not calling AI generate image so we can remove this part. Instead we're calling API.users and simply .post, right?
So replace this with .users. So the response type is the Client API users post request and request type is Client API users post request specifically the JSON property. So this will be useSignUp. The mutation here will also actually call the users. And then the JSON will be name, email and password.
So it matches. There we go. So now that we have use sign up, let's go inside of our sign up card right here and now we actually have to add this. So let's go ahead and simply add the mutation first. I'm gonna do that here.
Const mutation use sign up from hooks use sign up. I'm gonna change this to const mutation useSignUp from hooks useSignUp. I'm going to change this to features auth hooks useSignUp and I'm going to move it here. So I have visual, you know, separation for my features, my components and my global imports here. Now that I have the mutation inside of the onCredentials signup, I'm going to do the following.
I'm going to call mutation.mutate. I'm going to pass in the name, the email and the password. And then on success, after we've successfully registered and I know that I have a user in the database, well, we can't really do anything now but what we will do is call sign up right for now we can just console.log registered so let's go ahead and let's do the following make sure that in one of your terminals you have bunrun Database Studio running so that you can check out your newly created user now. So let me refresh this. So let me refresh my Drizzle Studio here.
Make sure that you are looking at the users table. So I only have two users and all of them have accounts, meaning they are OAuth logged in. And I'm gonna expand this. And I'm also just gonna keep my network tab open. And I'm gonna look for users.
That's the endpoint here. So let me refresh this page. Make sure you are on sign up, right? So not login, make sure you are in the sign up. I'm going to call this test.
My email will be testmail.com and my password is going to be 123. And let's click continue. And there we go. We are calling the users and looks like we got back null 200. Let's see if our payload is correct.
Test test 123. Perfect. And now let's check our Drizzle Studio here. Let them refresh. And hopefully if we've done everything correctly there we go we have a third user inside of our database and what's cool is the password field so this is the password field let me go ahead and paste that inside of here as you can see unless we know the way we encrypted it, we don't actually know what this means.
So none of these actually remaps back to 1-3 in any text form. So that's why we are using hashing. Great! So user is now successfully registered inside of our application and there is just one improvement that I want to do instead of the sign up card while we are mutating let's disable all the fields so disabled if mutation is pending For the inputs and here for the button. And we can also add this to the buttons below, right?
No point in trying to change to provider sign in here. And let me just collapse these attributes here. And let's try it one more time just so we can this time see the effect of you know kind of progress. So I'm gonna call this new one. Oops, so new one, new one and one two three and let's click continue and there we go.
You can see how now it's disabled. And now let's also handle kind of an error because of course errors can happen. So let me go back inside of my network and I'm searching for users. And I will try to log in with the same user and let's see what happens. So I still seem to get 200 but I'm not sure if I should get 200.
So let me go ahead and go inside of the users here. So yes let's go ahead and do the following thing first so let's try and get my query here to be await database and Let's go ahead and write select from users where equals from drizzle ORM. So make sure you import equals from drizzle ORM here. And let me just chain these elements instead. It's easier to look at it that way.
So where the users.email equals our email field. And then if from our query, we got even one result we're going to return an error. Oops, email taken with a 400 like this or email already in use so let's try it out again I'm gonna go ahead and call network here. Try and log in. And there we go.
Now I'm getting an error here. So email already in use. And now let's go ahead and let's try to reflect that error. And we can do that by going inside of terminal. Let me shut down this app and this as well.
And let's go ahead and add a toaster so Bonnex, chat-cnui, latest add, sonar after sonar has been installed you can run dev again let's go inside of our main layout so our root layout located in source app layout here and let's import toaster from components UI sonar and you can simply add it anywhere like this And now let's go inside of the useSignUp method in AuthHooks useSignUp and we're gonna do the following. So if not response okay we're gonna throw new error Something went wrong. And now inside of here, let's add on error. We're gonna do toast from sonar.error. Something went wrong.
Let's go ahead and try again. So now if I try logging in with an existing email so I believe I have like test right so let's try test mail.com there we go so now I have feedback that something went wrong but if I change to test 2 well we can also add on success right so So we have onError, let's add onSuccess here. Post success user created Let's click continue and there we go, user created. Great! So first things first, I want to go back inside of my Drizzle Studio.
Make sure you have that running because remember we now created an extra user which has the same email. So that's not good. Let's make sure we don't actually work with those users so I'm gonna go inside of here and find these new users right so new one with the same email and this one with the same email and I'm also gonna remove all the other ones. So basically all emails which I can recognize that I have written, I'm just gonna remove them from my database because I don't wanna have any conflicts while I'm doing this. And now let's go ahead and let's actually finish our, where is it, auth.cs our authorized method inside of here so this is where we actually do the login so first things first I want to create a credentials schema so let's go ahead and write const credentials schema will be z from Zod so make sure you add this import I'm just going to move it here z.object which accepts email which is a string and email and the password which will be a z.string and here's a quick tip for you when logging in don't validate the password as a minimum of three letters required.
So when registering that's completely fine. But when logging in you are not creating a new user. You are simply trying to find an existing user. So let's say in your first version of the app you didn't add a minimum and maximum length of the password. You're gonna have a user who created their password with two letters and now in the future you're gonna change the requirement So every new user that register is gonna have three letters, right?
And if you do that here, when logging in, your user who previously registered will not be able to log in and they're not gonna know why. So no need to add that validation for logging in. Right? Great. And now let's go ahead and we also have the database perfect so let's go ahead and do the following.
Let's also import Bcrypt from Bcrypt.js and now inside of the Authorize method we're going to do the following first let's see if we have the proper validated fields so validated fields are going to be credentials schema safe parse credentials and then if the validated fields did not get success so don't forget the exclamation point here return null otherwise from validated fields dot data you can now extract email and password. And now let's go ahead and let's attempt to find the user from await database.select.from users. We have to add the users schema so just make sure you've added this, I'm gonna change it to this where equals users email and email So first we are attempting to even find the existing user by this email. And let me just go ahead and import equals from drizzle ORM here. I'm just gonna move that here.
Let me move this here. There we go. And now when we find the single user, yes you can also you know write query if you want to and then you can write const user query like this. So if there is no user or if user does not have a password which means this user was probably created using Google or GitHub login but we are somehow trying to log in with credentials But we can't do that because we have to validate what the user just wrote in the password field. But if there is no password field in the database, there's nothing we can compare the hash with.
So if there is no user in the database or if the user in the database exists but the password field doesn't exist we cannot proceed with credential login so we return back null and then finally let's check if the passwords match by using await bcrypt.compare in the first argument passing the password which the user wrote in the input field from login and we're going to compare it with the hash from the existing user.password And then if passwords don't match again break the method. This means invalid password. So make sure you put exclamation point at the end here. And then finally we can safely return the user. So if they manage to get to this part it means that all the validation above has passed and the user correctly entered their password.
Here's another quick tip don't tell the user specifics about the error. So don't tell the user invalid password or don't tell the user this is only for oauth login or this user doesn't exist. Don't be specific with your errors it can be used for attacks right so simply don't return the user you are not able to find it no need to explain why further. Great! And if we've done this correctly we should now be able to register and log in and so let's go back inside of our sign up card and I've added a little to-do here to call sign up.
Well now we're gonna actually do it so on success here we're gonna add sign in with credentials and pass in the email, the password and the callback URL So basically the same thing that we are calling in the sign in card on credential sign in we also do the exact same thing. Let's go ahead and try it out now. So inside of my users here I only have two users and both of them are my Google and GitHub users. So I'm going to go ahead and let me refresh this so make sure you have your app running. We should finally be able to register now.
So bunrundev here, bunrundatabase studio in another terminal and I should be able to test out my credential login any moment now. So make sure that you are inside of sign up here. I'm gonna create test, testmail.com 123 and let's click continue and user has been created and there we go. I am logged in with the user test and email testmail.com. I don't have an image because I didn't use OAuth providers but I have my user ID from the database.
Perfect! Now let's go ahead and let's try and call API sign out. And this time let's log in. So I'm gonna call testmail.com let me enter an invalid password there we go so this was an invalid password and we actually have an error inside of our configuration here you can't really see it so I'm gonna zoom in in the video So we can use that to display the error here. And now let's try correct.
So test email 123, continue, and there we go. So the login is working. Perfect. So now what I want to do to wrap this up is display that error in our login form. So inside of here I'm gonna enter something that does not exist for my password and I should display that error here.
So let's go ahead and quickly do that. Let's go inside of the sign in card right here and let's go and add use a search params so const params from use search params from next navigation. So make sure you've added it from here let me just move that here there we go, useSearchParams and we're gonna focus on the error so params, whoops, yes, params.getError So why error? Well, that's what's assigned inside of my URL here. Error.
So that's what I want to read. So let's go ahead and get that. Once we have it, let's go right here. We're gonna go just outside card header before card content begins and if we have errors we're gonna use double exclamation points error so we turn this into a boolean then we're gonna render the following We're gonna render a div with a triangle alert from Lucid React, so make sure you add this icon here. We're gonna give the triangle alert a class name of size 4 and we're gonna add a paragraph invalid email or password and let's give this div a class name of BackgroundDestructive slash 15 meaning it's going to have an opacity as you can see here.
Let's give it a padding of 3. RoundedMD. Let's give it flex. ItemsCenter. GapX2, meaning gap between those two elements of 2.
Text small and text destructive. And let's give it a margin bottom of 6. There we go. So now when we enter something that doesn't exist, we're gonna have invalid email or password. So if I go to sign up and click back to sign in I'm gonna try and enter a random email that doesn't exist.
I will click continue and there we go. Invalid email or password. And if you want to, you can also be consistent because right now if I attempt to create you know the same user I will get an error in form of this toast message so how about we stay consistent with our errors and instead do this Let's go ahead and copy this and go inside of our signup card and paste that here. The problem is we don't have this error field right we don't have it inside of our URL. Make sure you added the triangle input import from Lucid React inside of our sign up card.
So we are in the sign up card right here. But what we do have is the mutation access. So what we can do here is check if we have mutation error like this and then go inside of use sign up and remove the on error toast because we don't need two error messages. So now if I try and register with test, There we go. Let's actually just change the email.
Let's actually change this here. Sign up. Let's just say something went wrong. So we don't want to be too specific, just something went wrong. There we go.
So now we have errors for registering, as you can see, and we also have errors for invalid passwords. There we go. And let's just confirm one more time. Our login works amazing, amazing job.