-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(auth): add password reset endpoints (#89)
Implement password reset endpoints for enabling the "forgot password" functionality. Users can now initiate the password reset process, receive a reset link, and securely update their password. Enhances overall security and user experience. Signed-off-by: Valentin Sickert <[email protected]>
- Loading branch information
Showing
5 changed files
with
265 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
<?php | ||
|
||
namespace App\Http\Controllers; | ||
|
||
use App\Http\Responses\ApiErrorResponse; | ||
use App\Http\Responses\ApiSuccessResponse; | ||
use App\Models\User; | ||
use Illuminate\Auth\Notifications\ResetPassword; | ||
use Illuminate\Http\Request; | ||
use Illuminate\Http\Response; | ||
use Illuminate\Mail\Message; | ||
use Illuminate\Support\Facades\Hash; | ||
use Illuminate\Support\Facades\Password; | ||
|
||
class PasswordResetController extends Controller | ||
{ | ||
/** | ||
* Send a password reset link to the user's email. | ||
* | ||
* @param \Illuminate\Http\Request $request | ||
* @return \App\Http\Responses\ApiSuccessResponse|\App\Http\Responses\ApiErrorResponse | ||
*/ | ||
public function sendLink(Request $request) | ||
{ | ||
$request->validate([ | ||
'email' => ['required', 'email', 'exists:users,email'], | ||
'reset_url' => ['required', 'url'] | ||
]); | ||
|
||
ResetPassword::createUrlUsing(function (User $user, string $token) use ($request) { | ||
return $request->reset_url . '/' . $token . '?email=' . $user->email; | ||
}); | ||
|
||
$status = Password::sendResetLink( | ||
$request->only('email') | ||
); | ||
|
||
return $status === Password::RESET_LINK_SENT | ||
? new ApiSuccessResponse('', Response::HTTP_NO_CONTENT) | ||
: new ApiErrorResponse('Unable to send reset link'); | ||
} | ||
|
||
/** | ||
* Reset the user's password. | ||
* | ||
* @param \Illuminate\Http\Request $request | ||
* @return \App\Http\Responses\ApiSuccessResponse|\App\Http\Responses\ApiErrorResponse | ||
*/ | ||
public function reset(Request $request, string $token) | ||
{ | ||
$request->merge(['token' => $token]); | ||
$request->validate([ | ||
'token' => ['required', 'string'], | ||
'email' => ['required', 'email', 'exists:users,email'], | ||
'password' => ['required', 'min:8', 'confirmed'] | ||
]); | ||
|
||
$status = Password::reset( | ||
$request->only('email', 'password', 'password_confirmation', 'token'), | ||
function (User $user, string $password) { | ||
$user->forceFill([ | ||
'password' => Hash::make($password) | ||
])->save(); | ||
|
||
$user->tokens()->delete(); | ||
} | ||
); | ||
|
||
return $status === Password::PASSWORD_RESET | ||
? new ApiSuccessResponse('', Response::HTTP_NO_CONTENT) | ||
: new ApiErrorResponse('Unable to reset password'); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,11 @@ | ||
<?php | ||
|
||
use App\Http\Controllers\AuthController; | ||
use App\Http\Controllers\PasswordResetController; | ||
use Illuminate\Support\Facades\Route; | ||
|
||
Route::post('/login', [AuthController::class, 'login']); | ||
Route::post('/logout', [AuthController::class, 'logout'])->middleware('auth:sanctum'); | ||
|
||
Route::post('/reset_password', [PasswordResetController::class, 'sendLink'])->name('api.v1.reset-password.email'); | ||
Route::post('/reset_password/{token}', [PasswordResetController::class, 'reset'])->name('api.v1.reset-password.reset'); |
112 changes: 112 additions & 0 deletions
112
tests/Feature/Http/Controllers/PasswordResetControllerTest.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
<?php | ||
|
||
namespace Tests\Feature\Http\Controllers; | ||
|
||
use App\Models\User; | ||
use Illuminate\Foundation\Testing\RefreshDatabase; | ||
use Illuminate\Http\Response; | ||
use Illuminate\Support\Facades\Hash; | ||
use Illuminate\Support\Facades\Password; | ||
use Tests\TestCase; | ||
|
||
class PasswordResetControllerTest extends TestCase | ||
{ | ||
use RefreshDatabase; | ||
|
||
/** | ||
* Test sending a password reset link. | ||
*/ | ||
public function test_send_link(): void | ||
{ | ||
$user = User::factory()->create(); | ||
$email = $user->email; | ||
|
||
$response = $this->postJson('/api/v1/reset_password', ['email' => $email, 'reset_url' => 'http://localhost']); | ||
|
||
$this->assertDatabaseHas('password_reset_tokens', ['email' => $email]); | ||
|
||
$response->assertStatus(Response::HTTP_NO_CONTENT); | ||
} | ||
|
||
/** | ||
* Test sending a password reset link with invalid email. | ||
*/ | ||
public function test_send_link_with_invalid_email(): void | ||
{ | ||
$response = $this->postJson('/api/v1/reset_password', ['email' => '[email protected]', 'reset_url' => 'http://localhost']); | ||
|
||
$this->assertDatabaseMissing('password_reset_tokens', ['email' => '[email protected]']); | ||
|
||
$response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY); | ||
} | ||
|
||
/** | ||
* Test resetting the user's password. | ||
*/ | ||
public function test_reset_password(): void | ||
{ | ||
$user = User::factory()->create(); | ||
$token = Password::createToken($user); | ||
|
||
$response = $this->postJson('/api/v1/reset_password/' . $token, [ | ||
'email' => $user->email, | ||
'password' => 'newpassword', | ||
'password_confirmation' => 'newpassword', | ||
]); | ||
|
||
$response->assertStatus(Response::HTTP_NO_CONTENT); | ||
$this->assertTrue(Hash::check('newpassword', $user->fresh()->password)); | ||
} | ||
|
||
/** | ||
* Test resetting the user's password with invalid token. | ||
*/ | ||
public function test_reset_password_with_invalid_token(): void | ||
{ | ||
$user = User::factory()->create(); | ||
|
||
$response = $this->postJson('/api/v1/reset_password/invalid_token', [ | ||
'email' => $user->email, | ||
'password' => 'newpassword', | ||
'password_confirmation' => 'newpassword', | ||
]); | ||
|
||
$response->assertStatus(Response::HTTP_INTERNAL_SERVER_ERROR); | ||
$this->assertFalse(Hash::check('newpassword', $user->fresh()->password)); | ||
} | ||
|
||
/** | ||
* Test resetting the user's password with invalid email. | ||
*/ | ||
public function test_reset_password_with_invalid_email(): void | ||
{ | ||
$user = User::factory()->create(); | ||
$token = Password::createToken($user); | ||
|
||
$response = $this->postJson('/api/v1/reset_password/' . $token, [ | ||
'email' => '[email protected]', | ||
'password' => 'newpassword', | ||
'password_confirmation' => 'newpassword', | ||
]); | ||
|
||
$response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY); | ||
} | ||
|
||
/** | ||
* Test resetting the user's password with password mismatch. | ||
*/ | ||
public function test_reset_password_with_password_mismatch(): void | ||
{ | ||
$user = User::factory()->create(); | ||
$token = Password::createToken($user); | ||
|
||
$response = $this->postJson('/api/v1/reset_password/' . $token, [ | ||
'email' => $user->email, | ||
'password' => 'newpassword', | ||
'password_confirmation' => 'mismatchedpassword', | ||
]); | ||
|
||
$response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY); | ||
$this->assertFalse(Hash::check('newpassword', $user->fresh()->password)); | ||
} | ||
} |