Skip to content

Commit 79730b3

Browse files
authored
Password resets (#54)
* Add reset link below login form * Add view for resetting password * Add logic for sending password reset email * Add view for resetting password * Add logic for resetting password * Add tests for `ResetPasswordController` * Update ResetPasswordController.php
1 parent eac67d0 commit 79730b3

File tree

10 files changed

+369
-2
lines changed

10 files changed

+369
-2
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ yarn-error.log
1919
/.fleet
2020
/.idea
2121
/.vscode
22+
.phpunit.cache/
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
namespace App\Http\Controllers;
4+
5+
use App\Http\Requests\ResetPassword\ResetPasswordStore;
6+
use App\Http\Requests\ResetPassword\ResetPasswordUpdate;
7+
use App\Models\User;
8+
use Illuminate\Auth\Events\PasswordReset;
9+
use Illuminate\Http\Request;
10+
use Illuminate\Support\Facades\Password;
11+
use Illuminate\Support\Str;
12+
use Illuminate\Validation\ValidationException;
13+
14+
class ResetPasswordController extends Controller
15+
{
16+
public function show(Request $request)
17+
{
18+
return \inertia('ResetPassword/Show');
19+
}
20+
21+
public function store(ResetPasswordStore $request)
22+
{
23+
$status = Password::sendResetLink($request->only('email'));
24+
25+
if ($status !== Password::RESET_LINK_SENT) {
26+
throw ValidationException::withMessages([
27+
'reset_link' => \__($status),
28+
]);
29+
}
30+
31+
\session()->flash('message', \__('passwords.sent'));
32+
33+
return \redirect()->route('login');
34+
}
35+
36+
public function edit(Request $request, string $token)
37+
{
38+
return \inertia('ResetPassword/Edit', [
39+
'token' => $token,
40+
'email' => $request->string('email'),
41+
]);
42+
}
43+
44+
public function update(ResetPasswordUpdate $request)
45+
{
46+
$status = Password::reset($request->only('token', 'email', 'password', 'token'), function (User $user, string $password) {
47+
$user->forceFill([
48+
'password' => $password,
49+
])->setRememberToken(Str::random(60));
50+
51+
$user->save();
52+
53+
\event(new PasswordReset($user));
54+
});
55+
56+
if ($status !== Password::PASSWORD_RESET) {
57+
throw ValidationException::withMessages([
58+
'reset' => __($status),
59+
]);
60+
}
61+
62+
\session()->flash('message', \__('passwords.reset'));
63+
64+
return \redirect()->route('login');
65+
}
66+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace App\Http\Requests\ResetPassword;
4+
5+
use Illuminate\Foundation\Http\FormRequest;
6+
7+
class ResetPasswordStore extends FormRequest
8+
{
9+
public function rules()
10+
{
11+
return [
12+
'email' => ['required', 'email', 'exists:users'],
13+
];
14+
}
15+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
namespace App\Http\Requests\ResetPassword;
4+
5+
use Illuminate\Foundation\Http\FormRequest;
6+
use Illuminate\Validation\Rules\Password;
7+
8+
class ResetPasswordUpdate extends FormRequest
9+
{
10+
public function rules()
11+
{
12+
return [
13+
'email' => ['required', 'email', 'exists:users'],
14+
'token' => ['required'],
15+
'password_confirmation' => ['required', 'same:password'],
16+
'password' => [
17+
'required',
18+
Password::min(6)
19+
->mixedCase()
20+
->numbers()
21+
->symbols()
22+
->uncompromised(),
23+
],
24+
];
25+
}
26+
}

resources/js/Pages/Login/Show.vue

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<template>
2+
23
<Head :title="title" />
34

45
<div class="mx-auto max-w-2xl">
@@ -69,8 +70,17 @@
6970
</div>
7071
</form>
7172

72-
<div class="mt-5 sm:mt-7">
73+
<div class="mt-6 lg:mt-10">
7374
<p class="text-center text-slate-800">
75+
<Link
76+
class="underline hover:no-underline"
77+
:href="route('password')"
78+
>
79+
Forgot your password?
80+
</Link>
81+
</p>
82+
83+
<p class="text-center text-slate-800 mt-3">
7484
<Link
7585
class="underline hover:no-underline"
7686
:href="route('register')"

resources/js/Pages/Register/Show.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@
100100
</div>
101101
</form>
102102

103-
<div class="mt-5 sm:mt-7">
103+
<div class="mt-6 lg:mt-10">
104104
<p class="text-center text-slate-800">
105105
<Link
106106
class="underline hover:no-underline"
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<template>
2+
3+
<Head :title="title" />
4+
5+
<div class="mx-auto max-w-2xl">
6+
<h1
7+
v-text="title"
8+
class="lg:text-4xl text-3xl font-medium text-slate-800 text-center lg:mb-8 mb-4"
9+
></h1>
10+
11+
<div class="bg-white rounded-2xl lg:p-10 p-6 border border-slate-200">
12+
<form @submit.prevent="submitResetPasswordForm">
13+
<div class="grid grid-cols-1 gap-5 sm:gap-7 sm:grid-cols-6">
14+
<div class="col-span-full">
15+
<label
16+
class="label"
17+
for="password"
18+
>
19+
Password
20+
</label>
21+
<input
22+
id="password"
23+
type="password"
24+
class="input"
25+
required
26+
v-model="resetPasswordForm.password"
27+
/>
28+
</div>
29+
30+
<div class="col-span-full">
31+
<label
32+
class="label"
33+
for="password-confirmation"
34+
>
35+
Confirm Password
36+
</label>
37+
<input
38+
id="password-confirmation"
39+
type="password"
40+
class="input"
41+
required
42+
v-model="resetPasswordForm.password_confirmation"
43+
/>
44+
</div>
45+
46+
<div class="col-span-full">
47+
<Button
48+
text="Reset Password"
49+
class="w-full text-center justify-center"
50+
:disabled="resetPasswordForm.processing"
51+
/>
52+
</div>
53+
</div>
54+
</form>
55+
</div>
56+
</div>
57+
</template>
58+
59+
<script>
60+
import { useForm } from "@inertiajs/vue3";
61+
62+
export default {
63+
props: {
64+
email: String,
65+
token: String,
66+
},
67+
68+
data() {
69+
return {
70+
title: "Reset Password",
71+
resetPasswordForm: useForm({
72+
email: this.email,
73+
password: "",
74+
password_confirmation: "",
75+
token: this.token,
76+
}),
77+
};
78+
},
79+
80+
methods: {
81+
submitResetPasswordForm() {
82+
this.resetPasswordForm.patch(route("password.update"));
83+
},
84+
},
85+
};
86+
</script>
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<template>
2+
3+
<Head :title="title" />
4+
5+
<div class="mx-auto max-w-2xl">
6+
<h1
7+
v-text="title"
8+
class="lg:text-4xl text-3xl font-medium text-slate-800 text-center lg:mb-8 mb-4"
9+
></h1>
10+
11+
<div class="bg-white rounded-2xl lg:p-10 p-6 border border-slate-200">
12+
<form @submit.prevent="submitForgotPasswordForm">
13+
<div class="grid grid-cols-1 gap-5 sm:gap-7 sm:grid-cols-6">
14+
<div class="col-span-full">
15+
<label
16+
class="label"
17+
for="email"
18+
>
19+
Email
20+
</label>
21+
<input
22+
id="email"
23+
type="email"
24+
required
25+
class="input"
26+
v-model="forgotPasswordForm.email"
27+
/>
28+
</div>
29+
30+
<div class="col-span-full">
31+
<Button
32+
text="Email Password Reset Link"
33+
class="w-full text-center justify-center"
34+
:disabled="forgotPasswordForm.processing"
35+
/>
36+
</div>
37+
</div>
38+
</form>
39+
40+
<div class="mt-6 lg:mt-10">
41+
<p class="text-center text-slate-800">
42+
<Link
43+
class="underline hover:no-underline"
44+
:href="route('login')"
45+
>
46+
Login
47+
</Link>
48+
</p>
49+
</div>
50+
</div>
51+
</div>
52+
</template>
53+
54+
<script>
55+
import { useForm } from "@inertiajs/vue3";
56+
57+
export default {
58+
data() {
59+
return {
60+
title: "Forgot Password",
61+
forgotPasswordForm: useForm({
62+
email: "",
63+
}),
64+
};
65+
},
66+
67+
methods: {
68+
submitForgotPasswordForm() {
69+
this.forgotPasswordForm.post(route("password.store"));
70+
},
71+
},
72+
};
73+
</script>

routes/web.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@
1616
Route::post('login', 'store')->name('login.store');
1717
});
1818

19+
Route::controller(App\Http\Controllers\ResetPasswordController::class)
20+
->group(function () {
21+
Route::get('forgot-password', 'show')->name('password');
22+
Route::post('forgot-password', 'store')->name('password.store');
23+
Route::get('reset-password/{token}', 'edit')->name('password.reset');
24+
Route::patch('reset-password', 'update')->name('password.update');
25+
});
26+
1927
Route::post('logout', App\Http\Controllers\LogoutController::class)
2028
->middleware(['auth'])
2129
->name('logout');

0 commit comments

Comments
 (0)