Cleanup login/reset functionality, address security issue with 2FA pathways
This commit is contained in:
parent
eade81f89b
commit
c3e462ab2f
11 changed files with 158 additions and 39 deletions
|
@ -106,6 +106,10 @@ abstract class AbstractLoginController extends Controller
|
||||||
$this->getField($request->input('user')) => $request->input('user'),
|
$this->getField($request->input('user')) => $request->input('user'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if ($request->route()->named('auth.checkpoint')) {
|
||||||
|
throw new DisplayException(trans('auth.checkpoint_failed'));
|
||||||
|
}
|
||||||
|
|
||||||
throw new DisplayException(trans('auth.failed'));
|
throw new DisplayException(trans('auth.failed'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
namespace Pterodactyl\Http\Controllers\Auth;
|
namespace Pterodactyl\Http\Controllers\Auth;
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Support\Facades\Password;
|
use Illuminate\Support\Facades\Password;
|
||||||
use Pterodactyl\Http\Controllers\Controller;
|
use Pterodactyl\Http\Controllers\Controller;
|
||||||
use Pterodactyl\Events\Auth\FailedPasswordReset;
|
use Pterodactyl\Events\Auth\FailedPasswordReset;
|
||||||
|
@ -18,9 +18,9 @@ class ForgotPasswordController extends Controller
|
||||||
*
|
*
|
||||||
* @param \Illuminate\Http\Request
|
* @param \Illuminate\Http\Request
|
||||||
* @param string $response
|
* @param string $response
|
||||||
* @return \Illuminate\Http\RedirectResponse
|
* @return \Illuminate\Http\JsonResponse
|
||||||
*/
|
*/
|
||||||
protected function sendResetLinkFailedResponse(Request $request, $response): RedirectResponse
|
protected function sendResetLinkFailedResponse(Request $request, $response): JsonResponse
|
||||||
{
|
{
|
||||||
// As noted in #358 we will return success even if it failed
|
// As noted in #358 we will return success even if it failed
|
||||||
// to avoid pointing out that an account does or does not
|
// to avoid pointing out that an account does or does not
|
||||||
|
@ -34,9 +34,9 @@ class ForgotPasswordController extends Controller
|
||||||
* Get the response for a successful password reset link.
|
* Get the response for a successful password reset link.
|
||||||
*
|
*
|
||||||
* @param string $response
|
* @param string $response
|
||||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse
|
* @return \Illuminate\Http\JsonResponse
|
||||||
*/
|
*/
|
||||||
protected function sendResetLinkResponse($response)
|
protected function sendResetLinkResponse($response): JsonResponse
|
||||||
{
|
{
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'status' => trans($response),
|
'status' => trans($response),
|
||||||
|
|
|
@ -10,9 +10,8 @@ class LoginCheckpointController extends AbstractLoginController
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Handle a login where the user is required to provide a TOTP authentication
|
* Handle a login where the user is required to provide a TOTP authentication
|
||||||
* token. In order to add additional layers of security, users are not
|
* token. Once a user has reached this stage it is assumed that they have already
|
||||||
* informed of an incorrect password until this stage, forcing them to
|
* provided a valid username and password.
|
||||||
* provide a token on each login attempt.
|
|
||||||
*
|
*
|
||||||
* @param \Pterodactyl\Http\Requests\Auth\LoginCheckpointRequest $request
|
* @param \Pterodactyl\Http\Requests\Auth\LoginCheckpointRequest $request
|
||||||
* @return \Illuminate\Http\JsonResponse
|
* @return \Illuminate\Http\JsonResponse
|
||||||
|
@ -28,7 +27,7 @@ class LoginCheckpointController extends AbstractLoginController
|
||||||
return $this->sendFailedLoginResponse($request);
|
return $this->sendFailedLoginResponse($request);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! array_get($cache, 'valid_credentials') || array_get($cache, 'request_ip') !== $request->ip()) {
|
if (array_get($cache, 'request_ip') !== $request->ip()) {
|
||||||
return $this->sendFailedLoginResponse($request, $user);
|
return $this->sendFailedLoginResponse($request, $user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,7 +39,7 @@ class LoginCheckpointController extends AbstractLoginController
|
||||||
return $this->sendFailedLoginResponse($request, $user);
|
return $this->sendFailedLoginResponse($request, $user);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->authManager->guard()->login($user, true);
|
$this->auth->guard()->login($user, true);
|
||||||
|
|
||||||
return $this->sendLoginResponse($request);
|
return $this->sendLoginResponse($request);
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,22 +33,26 @@ class LoginController extends AbstractLoginController
|
||||||
return $this->sendFailedLoginResponse($request);
|
return $this->sendFailedLoginResponse($request);
|
||||||
}
|
}
|
||||||
|
|
||||||
$validCredentials = password_verify($request->input('password'), $user->password);
|
// Ensure that the account is using a valid username and password before trying to
|
||||||
|
// continue. Previously this was handled in the 2FA checkpoint, however that has
|
||||||
|
// a flaw in which you can discover if an account exists simply by seeing if you
|
||||||
|
// can proceede to the next step in the login process.
|
||||||
|
if (! password_verify($request->input('password'), $user->password)) {
|
||||||
|
return $this->sendFailedLoginResponse($request, $user);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the user is using 2FA we do not actually log them in at this step, we return
|
||||||
|
// a one-time token to link the 2FA credentials to this account via the UI.
|
||||||
if ($user->use_totp) {
|
if ($user->use_totp) {
|
||||||
$token = str_random(128);
|
$token = str_random(128);
|
||||||
$this->cache->put($token, [
|
$this->cache->put($token, [
|
||||||
'user_id' => $user->id,
|
'user_id' => $user->id,
|
||||||
'valid_credentials' => $validCredentials,
|
|
||||||
'request_ip' => $request->ip(),
|
'request_ip' => $request->ip(),
|
||||||
], 5);
|
], 5);
|
||||||
|
|
||||||
return response()->json(['complete' => false, 'token' => $token]);
|
return response()->json(['complete' => false, 'token' => $token]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $validCredentials) {
|
|
||||||
return $this->sendFailedLoginResponse($request, $user);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->auth->guard()->login($user, true);
|
$this->auth->guard()->login($user, true);
|
||||||
|
|
||||||
return response()->json(['complete' => true]);
|
return response()->json(['complete' => true]);
|
||||||
|
|
|
@ -1,9 +1,19 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<form class="bg-white shadow-lg rounded-lg pt-10 px-8 pb-6 mb-4 animate fadein" method="post">
|
<div class="pb-4" v-for="error in errors">
|
||||||
|
<div class="p-2 bg-red-dark border-red-darker border items-center text-red-lightest leading-normal rounded flex lg:inline-flex w-full text-sm"
|
||||||
|
role="alert">
|
||||||
|
<span class="flex rounded-full bg-red uppercase px-2 py-1 text-xs font-bold mr-3 leading-none">Error</span>
|
||||||
|
<span class="mr-2 text-left flex-auto">{{ error }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form class="bg-white shadow-lg rounded-lg pt-10 px-8 pb-6 mb-4 animate fadein" method="post"
|
||||||
|
v-on:submit.prevent="submitForm"
|
||||||
|
>
|
||||||
<div class="flex flex-wrap -mx-3 mb-6">
|
<div class="flex flex-wrap -mx-3 mb-6">
|
||||||
<div class="input-open">
|
<div class="input-open">
|
||||||
<input class="input" id="grid-email" type="email" aria-labelledby="grid-email" ref="email" required
|
<input class="input" id="grid-email" type="email" aria-labelledby="grid-email" ref="email" required
|
||||||
|
v-bind:readonly="showSpinner"
|
||||||
v-bind:value="email"
|
v-bind:value="email"
|
||||||
v-on:input="updateEmail($event)"
|
v-on:input="updateEmail($event)"
|
||||||
/>
|
/>
|
||||||
|
@ -12,9 +22,11 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<csrf/>
|
<button class="btn btn-blue btn-jumbo" type="submit" v-bind:disabled="submitDisabled">
|
||||||
<button class="btn btn-blue btn-jumbo" type="submit">
|
<span class="spinner white" v-bind:class="{ hidden: ! showSpinner }"> </span>
|
||||||
{{ $t('auth.recover_account') }}
|
<span v-bind:class="{ hidden: showSpinner }">
|
||||||
|
{{ $t('auth.recover_account') }}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="pt-6 text-center">
|
<div class="pt-6 text-center">
|
||||||
|
@ -27,10 +39,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Csrf from "../forms/CSRF";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {Csrf},
|
|
||||||
name: 'forgot-password',
|
name: 'forgot-password',
|
||||||
props: {
|
props: {
|
||||||
email: {type: String, required: true},
|
email: {type: String, required: true},
|
||||||
|
@ -41,11 +50,43 @@
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
X_CSRF_TOKEN: window.X_CSRF_TOKEN,
|
X_CSRF_TOKEN: window.X_CSRF_TOKEN,
|
||||||
|
errors: [],
|
||||||
|
submitDisabled: false,
|
||||||
|
showSpinner: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
updateEmail: function (event) {
|
updateEmail: function (event) {
|
||||||
|
this.$data.submitDisabled = false;
|
||||||
this.$emit('update-email', event.target.value);
|
this.$emit('update-email', event.target.value);
|
||||||
|
},
|
||||||
|
|
||||||
|
submitForm: function () {
|
||||||
|
const self = this;
|
||||||
|
this.$data.submitDisabled = true;
|
||||||
|
this.$data.showSpinner = true;
|
||||||
|
this.$data.errors = [];
|
||||||
|
|
||||||
|
window.axios.post(this.route('auth.forgot-password.send-link'), {
|
||||||
|
email: this.$props.email,
|
||||||
|
})
|
||||||
|
.then(function (response) {
|
||||||
|
self.$data.submitDisabled = false;
|
||||||
|
self.$data.showSpinner = false;
|
||||||
|
self.flash({message: response.data.status, variant: 'success'});
|
||||||
|
self.$router.push({name: 'login'});
|
||||||
|
})
|
||||||
|
.catch(function (err) {
|
||||||
|
self.$data.showSpinner = false;
|
||||||
|
if (!err.response) {
|
||||||
|
return console.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = err.response;
|
||||||
|
if (response.data && _.isObject(response.data.errors)) {
|
||||||
|
self.$data.errors.push(response.data.errors[0].detail);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,7 @@
|
||||||
<div>
|
<div>
|
||||||
<flash-message variant="danger" />
|
<flash-message variant="danger" />
|
||||||
<flash-message variant="success" />
|
<flash-message variant="success" />
|
||||||
<flash-message variant="warning" />
|
<div class="pb-4" v-if="errors && errors.length === 1">
|
||||||
<flash-message variant="info" />
|
|
||||||
<div class="py-4" v-if="errors && errors.length === 1">
|
|
||||||
<div class="p-2 bg-red-dark border-red-darker border items-center text-red-lightest leading-normal rounded flex lg:inline-flex w-full text-sm"
|
<div class="p-2 bg-red-dark border-red-darker border items-center text-red-lightest leading-normal rounded flex lg:inline-flex w-full text-sm"
|
||||||
role="alert">
|
role="alert">
|
||||||
<span class="flex rounded-full bg-red uppercase px-2 py-1 text-xs font-bold mr-3 leading-none">Error</span>
|
<span class="flex rounded-full bg-red uppercase px-2 py-1 text-xs font-bold mr-3 leading-none">Error</span>
|
||||||
|
@ -12,13 +10,12 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form class="bg-white shadow-lg rounded-lg pt-10 px-8 pb-6 mb-4 animate fadein" method="post"
|
<form class="bg-white shadow-lg rounded-lg pt-10 px-8 pb-6 mb-4 animate fadein" method="post"
|
||||||
v-on:submit.prevent="handleLogin"
|
v-on:submit.prevent="submitForm"
|
||||||
>
|
>
|
||||||
<div class="flex flex-wrap -mx-3 mb-6">
|
<div class="flex flex-wrap -mx-3 mb-6">
|
||||||
<div class="input-open">
|
<div class="input-open">
|
||||||
<input class="input" id="grid-username" type="text" name="user" aria-labelledby="grid-username"
|
<input class="input" id="grid-username" type="text" name="user" aria-labelledby="grid-username" required
|
||||||
ref="email"
|
ref="email"
|
||||||
required
|
|
||||||
v-bind:value="user.email"
|
v-bind:value="user.email"
|
||||||
v-on:input="updateEmail($event)"
|
v-on:input="updateEmail($event)"
|
||||||
/>
|
/>
|
||||||
|
@ -28,6 +25,7 @@
|
||||||
<div class="flex flex-wrap -mx-3 mb-6">
|
<div class="flex flex-wrap -mx-3 mb-6">
|
||||||
<div class="input-open">
|
<div class="input-open">
|
||||||
<input class="input" id="grid-password" type="password" name="password"
|
<input class="input" id="grid-password" type="password" name="password"
|
||||||
|
ref="password"
|
||||||
aria-labelledby="grid-password" required
|
aria-labelledby="grid-password" required
|
||||||
v-model="user.password"
|
v-model="user.password"
|
||||||
/>
|
/>
|
||||||
|
@ -35,8 +33,11 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button class="btn btn-blue btn-jumbo" type="submit">
|
<button class="btn btn-blue btn-jumbo" type="submit" v-bind:disabled="showSpinner">
|
||||||
{{ $t('auth.sign_in') }}
|
<span class="spinner white" v-bind:class="{ hidden: ! showSpinner }"> </span>
|
||||||
|
<span v-bind:class="{ hidden: showSpinner }">
|
||||||
|
{{ $t('auth.sign_in') }}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="pt-6 text-center">
|
<div class="pt-6 text-center">
|
||||||
|
@ -67,6 +68,7 @@
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
errors: [],
|
errors: [],
|
||||||
|
showSpinner: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted: function () {
|
mounted: function () {
|
||||||
|
@ -75,8 +77,9 @@
|
||||||
methods: {
|
methods: {
|
||||||
// Handle a login request eminating from the form. If 2FA is required the
|
// Handle a login request eminating from the form. If 2FA is required the
|
||||||
// user will be presented with the 2FA modal window.
|
// user will be presented with the 2FA modal window.
|
||||||
handleLogin: function () {
|
submitForm: function () {
|
||||||
const self = this;
|
const self = this;
|
||||||
|
this.$data.showSpinner = true;
|
||||||
|
|
||||||
axios.post(this.route('auth.login'), {
|
axios.post(this.route('auth.login'), {
|
||||||
user: this.$props.user.email,
|
user: this.$props.user.email,
|
||||||
|
@ -88,17 +91,20 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
self.$props.user.password = '';
|
self.$props.user.password = '';
|
||||||
|
self.$data.showSpinner = false;
|
||||||
self.$router.push({name: 'checkpoint', query: {token: response.data.token}});
|
self.$router.push({name: 'checkpoint', query: {token: response.data.token}});
|
||||||
})
|
})
|
||||||
.catch(function (err) {
|
.catch(function (err) {
|
||||||
self.$props.user.password = '';
|
self.$props.user.password = '';
|
||||||
|
self.$data.showSpinner = false;
|
||||||
if (!err.response) {
|
if (!err.response) {
|
||||||
return console.error(err);
|
return console.error(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = err.response;
|
const response = err.response;
|
||||||
if (response.data && _.isObject(response.data.errors)) {
|
if (response.data && _.isObject(response.data.errors)) {
|
||||||
self.$data.errors.push(response.data.errors[0].detail);
|
self.$data.errors = [response.data.errors[0].detail];
|
||||||
|
self.$refs.password.focus();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
&.btn-blue {
|
&.btn-blue {
|
||||||
@apply .bg-blue .border-blue-dark .border .text-white;
|
@apply .bg-blue .border-blue-dark .border .text-white;
|
||||||
|
|
||||||
&:hover {
|
&:hover:enabled {
|
||||||
@apply .bg-blue-dark .border-blue-darker;
|
@apply .bg-blue-dark .border-blue-darker;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,4 +18,9 @@
|
||||||
&.btn-jumbo {
|
&.btn-jumbo {
|
||||||
@apply .p-4 .w-full .uppercase .tracking-wide .text-sm;
|
@apply .p-4 .w-full .uppercase .tracking-wide .text-sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.55;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
52
resources/assets/pterodactyl/styles/components/spinners.css
Normal file
52
resources/assets/pterodactyl/styles/components/spinners.css
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
.spinner {
|
||||||
|
color: transparent;
|
||||||
|
pointer-events: none;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
animation: spinners--spin 500ms infinite linear;
|
||||||
|
border-radius: 9999px;
|
||||||
|
@apply .border-2 .border-grey-light;
|
||||||
|
border-top-color: transparent !important;
|
||||||
|
border-right-color: transparent !important;
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
left: calc(50% - (1em / 2));
|
||||||
|
top: calc(50% - (1em / 2));
|
||||||
|
position: absolute !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Speeds
|
||||||
|
*/
|
||||||
|
&.spin-slow:after {
|
||||||
|
animation: spinners--spin 1200ms infinite linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spinner Colors
|
||||||
|
*/
|
||||||
|
&.blue:after {
|
||||||
|
@apply .border-blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.white:after {
|
||||||
|
@apply .border-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.spinner-thick:after {
|
||||||
|
@apply .border-4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spinners--spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,6 +10,7 @@
|
||||||
@import "components/animations.css";
|
@import "components/animations.css";
|
||||||
@import "components/authentication.css";
|
@import "components/authentication.css";
|
||||||
@import "components/buttons.css";
|
@import "components/buttons.css";
|
||||||
|
@import "components/spinners.css";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tailwind Utilities
|
* Tailwind Utilities
|
||||||
|
|
|
@ -19,7 +19,8 @@ return [
|
||||||
'reset_password_text' => 'Reset your account password.',
|
'reset_password_text' => 'Reset your account password.',
|
||||||
'reset_password' => 'Reset Account Password',
|
'reset_password' => 'Reset Account Password',
|
||||||
'email_sent' => 'An email has been sent to you with further instructions for resetting your password.',
|
'email_sent' => 'An email has been sent to you with further instructions for resetting your password.',
|
||||||
'failed' => 'The credentials provided do not match those we have on record, or the 2FA token provided was invalid.',
|
'failed' => 'No account matching those credentials could be found.',
|
||||||
|
'checkpoint_failed' => 'The two-factor authentication token was invalid.',
|
||||||
'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
|
'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
|
||||||
'password_requirements' => 'Passwords must contain at least one uppercase, lowercase, and numeric character and must be at least 8 characters in length.',
|
'password_requirements' => 'Passwords must contain at least one uppercase, lowercase, and numeric character and must be at least 8 characters in length.',
|
||||||
'request_reset' => 'Locate Account',
|
'request_reset' => 'Locate Account',
|
||||||
|
|
|
@ -9,14 +9,20 @@
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
Route::group(['middleware' => 'guest'], function () {
|
Route::group(['middleware' => 'guest'], function () {
|
||||||
|
// Login specific routes
|
||||||
Route::get('/login', 'LoginController@showLoginForm')->name('auth.login');
|
Route::get('/login', 'LoginController@showLoginForm')->name('auth.login');
|
||||||
Route::get('/password/reset/{token}', 'ResetPasswordController@showResetForm')->name('auth.reset');
|
|
||||||
|
|
||||||
Route::post('/login', 'LoginController@login')->middleware('recaptcha');
|
Route::post('/login', 'LoginController@login')->middleware('recaptcha');
|
||||||
Route::post('/login/checkpoint', 'LoginCheckpointController@index')->name('auth.checkpoint');
|
Route::post('/login/checkpoint', 'LoginCheckpointController@index')->name('auth.checkpoint');
|
||||||
Route::post('/password', 'ForgotPasswordController@sendResetLinkEmail')->middleware('recaptcha');
|
|
||||||
|
// Forgot password route. A post to this endpoint will trigger an
|
||||||
|
// email to be sent containing a reset token.
|
||||||
|
Route::post('/password', 'ForgotPasswordController@sendResetLinkEmail')->name('auth.forgot-password.send-link')->middleware('recaptcha');
|
||||||
|
|
||||||
|
// Password reset routes. This endpoint is hit after going through
|
||||||
|
// the forgot password routes to acquire a token (or after an account
|
||||||
|
// is created).
|
||||||
|
Route::get('/password/reset/{token}', 'ResetPasswordController@showResetForm')->name('auth.reset-password');
|
||||||
Route::post('/password/reset', 'ResetPasswordController@reset')->name('auth.reset.post')->middleware('recaptcha');
|
Route::post('/password/reset', 'ResetPasswordController@reset')->name('auth.reset.post')->middleware('recaptcha');
|
||||||
Route::post('/password/reset/{token}', 'ForgotPasswordController@sendResetLinkEmail')->middleware('recaptcha');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
Loading…
Reference in a new issue