DEV Community

Rabeea Ali
Rabeea Ali

Posted on • Updated on

How to reset password via code using Laravel API

Every Laravel project has build in the password reset process, it is working perfectly with web, but once when you come to mobile apps you going to face a problem with this way, because you make users go to web page to reset their password! & I think this not the best way to do such a thing, so the code password reset is pretty useful when you have mobile apps.

You will find the full code with better code written at the end of post.

Prerequisites:

• A Laravel project
• An Account in Mailtrap & get credentials and put it in .env file.

Step 1: Create a new reset code table

Each Laravel project comes with password_resets table, I will leave this table(If your Laravel app just API for mobile apps then use password_resets) for web users and create another table for mobile apps users as follow:

1- run this command in terminal

 php artisan make:model ResetCodePassword -m
Enter fullscreen mode Exit fullscreen mode

2- open reset_code_passwords_table migration and will be something like this:

Schema::create('reset_code_passwords', function (Blueprint $table) {
    $table->string('email')->index();
    $table->string('code');
    $table->timestamp('created_at')->nullable();
});
Enter fullscreen mode Exit fullscreen mode

Now migrate the table with php artisan migrate.

3- open ResetCodePassword then fill the $fillable with columns names:

protected $fillable = [
    'email',
    'code',
    'created_at',
];
Enter fullscreen mode Exit fullscreen mode

Note: feel free to change anything as you need

Step 2: Create files

  • Create a new directory in controllers called Api then create these three controllers:
ForgotPasswordController.php   // step 1
CodeCheckController.php        // step 2
ResetPasswordController.php    // step 3
Enter fullscreen mode Exit fullscreen mode
  • Create a mail class for email:
php artisan make:mail SendCodeResetPassword
Enter fullscreen mode Exit fullscreen mode

Now let's fill these files, first one ForgotPasswordController.php will be something like this:

class ForgotPasswordController extends Controller
{
    public function __invoke(Request $request)
    {
        $data = $request->validate([
            'email' => 'required|email|exists:users',
        ]);

        // Delete all old code that user send before.
        ResetCodePassword::where('email', $request->email)->delete();

        // Generate random code
        $data['code'] = mt_rand(100000, 999999);

        // Create a new code
        $codeData = ResetCodePassword::create($data);

        // Send email to user
        Mail::to($request->email)->send(new SendCodeResetPassword($codeData->code));

        return response(['message' => trans('passwords.sent')], 200);
    }
}
Enter fullscreen mode Exit fullscreen mode

in CodeCheckController.php:

class CodeCheckController extends Controller
{
    public function __invoke(Request $request)
    {
        $request->validate([
            'code' => 'required|string|exists:reset_code_passwords',
        ]);

        // find the code
        $passwordReset = ResetCodePassword::firstWhere('code', $request->code);

        // check if it does not expired: the time is one hour
        if ($passwordReset->created_at > now()->addHour()) {
            $passwordReset->delete();
            return response(['message' => trans('passwords.code_is_expire')], 422);
        }

        return response([
            'code' => $passwordReset->code,
            'message' => trans('passwords.code_is_valid')
        ], 200);
    }
}
Enter fullscreen mode Exit fullscreen mode

in CodeCheckController.php:

class CodeCheckController extends Controller
{
    public function __invoke(Request $request)
    {
        $request->validate([
            'code' => 'required|string|exists:reset_code_passwords',
            'password' => 'required|string|min:6|confirmed',
        ]);

        // find the code
        $passwordReset = ResetCodePassword::firstWhere('code', $request->code);

        // check if it does not expired: the time is one hour
        if ($passwordReset->created_at > now()->addHour()) {
            $passwordReset->delete();
            return response(['message' => trans('passwords.code_is_expire')], 422);
        }

        // find user's email 
        $user = User::firstWhere('email', $passwordReset->email);

        // update user password
        $user->update($request->only('password'));

        // delete current code 
        $passwordReset->delete();

        return response(['message' =>'password has been successfully reset'], 200);
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Handel Mail and blade file

In SendCodeResetPassword.php mail:

class SendCodeResetPassword extends Mailable implements ShouldQueue
{
    use Queueable, SerializesModels;

    public $code;

    public function __construct($code)
    {
        $this->code = $code;
    }

    public function build()
    {
        return $this->markdown('emails.send-code-reset-password');
    }
}
Enter fullscreen mode Exit fullscreen mode

In send-code-reset-password.blade.php

@component('mail::message')
<h1>We have received your request to reset your account password</h1>
<p>You can use the following code to recover your account:</p>

@component('mail::panel')
{{ $code }}
@endcomponent

<p>The allowed duration of the code is one hour from the time the message was sent</p>
@endcomponent
Enter fullscreen mode Exit fullscreen mode

Step 4: Create routes

In routes/api.php create links for your controllers:

Route::post('password/email',  ForgotPasswordController::class);
Route::post('password/code/check', CodeCheckController::class);
Route::post('password/reset', ResetPasswordController::class);
Enter fullscreen mode Exit fullscreen mode

Now you can open postman and try these links:

http://your-domain.com/api/password/email       => step 1
http://your-domain.com/api/password/code/check  => step 2
http://your-domain.com/api/password/reset       => step 3
Enter fullscreen mode Exit fullscreen mode

Source code on Github

Top comments (27)

Collapse
 
drummonddev profile image
Daniel Drummond

I got confused! We created 3 controllers at the beginning of the tutorial

But we are using only 2 of them, the ResetPasswordController controller is not being used

There will not be a name change in the tutorial. I say:
When referring to the 1st controller (CodeCheckController) wouldn't it be ResetPasswordController?

Can you help me with this question?

Collapse
 
muhammadfaisal profile image
Muhammad Faisal

Try to check on his Github reporsitory, maybe he forgot to write the Step 3..

Collapse
 
drummonddev profile image
Daniel Drummond

Thanks Muhammad

I'll see how you did

Thread Thread
 
muhammadfaisal profile image
Muhammad Faisal • Edited

Yes, he hasn't updated the tutorial, if you follow it completely, you will definitely get an error.

Try to clone the repository from his Github, and follow the path of where he puts the files, where he writes the functions, I'm sure you'll understand completely..
I've had success implementing it, although there is some code that needs to be changed.
Want me to tell you here?

What I understand here is, the authentication process that Laravel has in the process on the Web, we can't fully utilize in the Mobile API process.
But we have to almost do it manually

Thread Thread
 
drummonddev profile image
Daniel Drummond

Hey Muhammand

I already cloned the GitHub repository, but I couldn't solve this problem. Can you help me, because I've tried several ways to get the user and save the password, but I'm not getting it.

Is there any way to debug when we are using API so I can track what is happening?

As you can see in the image, I try to save the password but without success

Image description

I'm using this tutorial to learn about Laravel API, I'm new to Laravel. If you can help me I will be very grateful

Thread Thread
 
muhammadfaisal profile image
Muhammad Faisal

Sorry for the late reply because I didn't check my email in a few days.

To understand how the authentication process provided by Laravel, let alone using the library they have prepared (in this case Sanctum), is a bit difficult.

If only for initial learning, and to understand how the authentication process provided by Laravel, you can try using a Laravel-made library called Laravel Breeze, you can read and try it here

Back in the initial discussion, if we use the library for the authentication process on the website, it will be very easy, because it is automatic and suitable.
But since we are using this library for processes on mobile, then we need to create some of the processes ourselves.

For some of the improvements I made, well... quite a lot, hehe.
I'll try to explain some basic things you should at least pay attention to and configure in the next comment

Thread Thread
 
muhammadfaisal profile image
Muhammad Faisal

So, some of the things you can do are
A. Make sure the configuration in the .env is correct. If it is correct, when you run the php artisan migrate command, this command should successfully create the initial database in your database.
B. Change the route\api.php code from the sample code. Because maybe I'm using the latest Laravel version 9, so this may be different from the example in the tutorial, but it can cause errors. Change it to like this:

Route::post('password/email', [ForgotPasswordController::class, '__invoke']);
Route::post('password/code/check', [CodeCheckController::class, '__invoke']);
Route::post('password/reset', [ResetPasswordController::class, '__invoke']);
Enter fullscreen mode Exit fullscreen mode
  • password/email = url address to go to in API endpoint
  • ForgotPasswordController= name of your controller file
  • __invoke= the name of the method contained in the controller file (no matter what the name is)

C. Fix the code logic contained in the Models\ResetCodePassword.php file, in the section:

public function isExpire()
    {
        if ($this->created_at > now()->addHour()) {
            $this->delete();
        }
    }

Enter fullscreen mode Exit fullscreen mode

change to

public function isExpire()
    {
        if (now() > $this->created_at->addHour()) {
            $this->delete();
            return true;
        }

        return false;
    }

Enter fullscreen mode Exit fullscreen mode

D. Make sure that in each file that ends in Requests\Auth\, if there is an exists:users code, make sure the name is the same as the one in your database. You can try to learn in Laravel Validation

Well maybe it's not very complete because it's going to be very long, but those are some of the crucial things I'm changing until the code from this tutorial works..
Keep trying, if it fails, try to browse for the error, you can do it

I hope this helps

Thread Thread
 
maykelesser profile image
Maykel

@muhammadfaisal hey man, thanks for the explanation! I'm trying to implement, as @drummonddev. Tried to follow the github repo as well, and i'm stucked in one thing: The recovery code on the first endpoint came empty. I saw the __invoke method on ForgotPasswordController have the $codeData calling a PasswordReset::create function, but this function never existed on this tutorial or in the repo as well. Tried to build on my own, but without success. Can you help me with that?

I've tried like this:
`public static function create($data)
{
$code = rand(100000, 999999);

    $codeData = self::create([
        'token' => $code,
    ]);

    return $codeData;
Enter fullscreen mode Exit fullscreen mode

}`

Thread Thread
 
muhammadfaisal profile image
Muhammad Faisal

Okay you're welcome, happy to help.

First, what do you mean by

"The recovery code on the first endpoint came empty"

you mean the function doesn't generate the token code, right?

So to explain it better, maybe I'll try to help explain some of the functions of this code one by one through the flow, ok:

  1. We access the function in our Controller via the Route that we have defined before, for example we have a Route like this: Route::post('password/email', [ForgotPasswordController::class, '__invoke']);, then you can access via Postman like this http://192.168.88.27:8000/api/password/email (make sure you run ipconfig in cmd to find out the local ip and adjust the host url, if you are developing on a local machine)

  2. After we access the __invoke method in the Controller, here is an explanation of some of the code:

  • ResetCodePassword::where('email', $request->email)->delete(); : To delete the record in the reset_code_passwords db that you created via migration, so it doesn't duplicated
  • $codeData = ResetCodePassword::create($request->data()); Serves to input token code to reset_code_passwords db. ::create() is Laravel's Eloquent ORM default function, you can read about it here
  • $request->data() is obtained from the method for generating the token code in the ForgotPasswordRequest.php file, you can check it. It's just to randomize token numbers
  • Mail::to($request->email)->send(new SendCodeResetPassword($codeData->code)); : to send to email. SendCodeResetPassword() functions to store the generated code from $codeData, and send it to the view with the name emails.send-code-reset-password

$codeData->code, obtained from existing data in the database with column code..

Hope you understand and this helps

Thread Thread
 
maykelesser profile image
Maykel

@muhammadfaisal thanks for the fast reply! :) hope you have a great day!
about the recovery code, i mean, i've received the email, and also the data was recorded into the DB. But, the code recorded was null by some reason. I think i figured it out while i'm texting you (i'm using the default reset database table, so maybe the $code must be called as $token instead). I'll try and answer you asap! Thanks for clarify!

Thread Thread
 
maykelesser profile image
Maykel

as i talked, it worked! :)
but the second endpoint (/password/code/check) throws me an error: Call to a member function addHour() on string.

Its about this one - this addHour function doesnt exist?

if (now() > $this->created_at->addHour()) {
$this->delete();
return true;
}

Thread Thread
 
maykelesser profile image
Maykel

i've casted the created_at as datetime in the model, and worked too! :)

Thread Thread
 
muhammadfaisal profile image
Muhammad Faisal • Edited

You're welcome, I'm happy if I can help people.. :D

Ok, I see where the problem is now..
I think you should debug one by one the variable, to see the value you expect to get.

For example when I want to make sure that isExpire() method will return the right value and logic, first usually I will do these things:

echo now();
echo $this->created_at;
echo $this->created_at->addHour();

Make sure these echoes print the right values.

Then make sure the logic works:
If current time is greater than the db time added 1 hour time, then it'll return true =
The time is expired.

Example:
Now: 18:00
Db time: 17:30
Added 1 hour: 18:30
Then it's not expired.

But
Now: 18:00
Db time: 16:55
Added 1 hour: 17:55
Then it's expired, the method should return true..

Thread Thread
 
maykelesser profile image
Maykel

Yes, it worked after i've cast the created_at inside the PasswordReset model :) as the value are passed as string, it throws the error! But now, all the flow are working as expected! Thank you, for real!

Thread Thread
 
muhammadfaisal profile image
Muhammad Faisal

Glad to hear it works, you're welcome 😃

Collapse
 
drummonddev profile image
Daniel Drummond

Hey Muhammad

I replicated the GitHub project as is and it's almost all working fine.
Only when I'm going to update the password that it's not working.
The password is not changed and the output is HTML in Postman.

To make sure I wasn't doing anything wrong, I cloned the GitHub project repository and the same thing happens.

Can you help me find where the problem is?

Collapse
 
drummonddev profile image
Daniel Drummond • Edited

In Postman I passed the generated code and the new password.

Image description

Collapse
 
muhammadfaisal profile image
Muhammad Faisal

Have you tried to add Accept and the value is application/json in Postman Header section?

I met this problem too, and to change the way it show the problem, this value we add to the Header can help

Thread Thread
 
drummonddev profile image
Daniel Drummond

Hi muhammad,

I already passed this option in the header.
But so far I have not been able to solve it, whenever I try to change the password the result is the same. an html

Thread Thread
 
muhammadfaisal profile image
Muhammad Faisal • Edited

Ok, let's solve it one by one to see where it's come from..

So, you're not succeed yet, in the 1st step?

Maybe you get an error that can lead us to the source of the problem?

If you curious, the html response you get, it is just a login page code of Laravel default view..
Try to copy paste it in html code viewer

We need to know what action you did that makes the html error come

Thread Thread
 
drummonddev profile image
Daniel Drummond • Edited

Hi Muhammad

I got the system up and running!!! Couldn't do it without your tip on Accept and some changes to the GitHub codebase. I'll share the working project on GitHub, in case other people have difficulties.

github.com/Drummond-Dev/Laravel-AP...

Thanks for your help

Thread Thread
 
drummonddev profile image
Daniel Drummond

A tip, check how the dates and times of the systems are configured, both in Laravel and MySQL server

Thread Thread
 
muhammadfaisal profile image
Muhammad Faisal

Alright, glad to hear you succeed! :D

Collapse
 
arifnurulhakim profile image
arifnurulhakim

how to fix this?

Collapse
 
titusfrezer profile image
titusfrezer

Great Technique thanks

Collapse
 
marcellopato profile image
Marcello Pato

Your tutorial have a loto of errors, dude!

Collapse
 
soulofjava profile image
soulofjava

Amazing, thank's...