2 Commits

2 changed files with 111 additions and 0 deletions

View File

@@ -16,4 +16,6 @@ $app->group('/account', function () {
$this->get('/register', 'UserFrosting\Sprinkle\UFTweaks\Controller\AccountController:pageRegister')
->add('checkEnvironment')
->setName('register');
$this->post('/forgot-password', 'UserFrosting\Sprinkle\UFTweaks\Controller\AccountController:forgotPassword');
})->add(new NoCache());

View File

@@ -9,6 +9,7 @@
namespace UserFrosting\Sprinkle\UFTweaks\Controller;
use Carbon\Carbon;
use Illuminate\Database\Capsule\Manager as Capsule;
use Psr\Http\Message\ResponseInterface as Response;
@@ -18,6 +19,9 @@ use UserFrosting\Fortress\RequestDataTransformer;
use UserFrosting\Fortress\RequestSchema;
use UserFrosting\Fortress\ServerSideValidator;
use UserFrosting\Sprinkle\Account\Controller\AccountController as UFAccountController;
use UserFrosting\Sprinkle\Account\Facades\Password;
use UserFrosting\Sprinkle\Core\Mail\EmailRecipient;
use UserFrosting\Sprinkle\Core\Mail\TwigMailMessage;
use UserFrosting\Support\Exception\BadRequestException;
use UserFrosting\Support\Exception\ForbiddenException;
use UserFrosting\Support\Exception\NotFoundException;
@@ -29,6 +33,111 @@ use UserFrosting\Support\Exception\NotFoundException;
*/
class AccountController extends UFAccountController
{
/**
* Processes a request to email a forgotten password reset link to the user.
*
* Processes the request from the form on the "forgot password" page, checking that:
* 1. The rate limit for this type of request is being observed.
* 2. The provided email address belongs to a registered account;
* 3. The submitted data is valid.
* Note that we have removed the requirement that a password reset request not already be in progress.
* This is because we need to allow users to re-request a reset, even if they lose the first reset email.
* This route is "public access".
*
* @todo require additional user information
* @todo prevent password reset requests for root account?
*
* AuthGuard: false
* Route: /account/forgot-password
* Route Name: {none}
* Request type: POST
*
* @param Request $request
* @param Response $response
* @param array $args
*/
public function forgotPassword(Request $request, Response $response, $args)
{
/** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */
$ms = $this->ci->alerts;
/** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
$classMapper = $this->ci->classMapper;
/** @var \UserFrosting\Support\Repository\Repository $config */
$config = $this->ci->config;
// Get POST parameters
$params = $request->getParsedBody();
// Load the request schema
$schema = new RequestSchema('schema://requests/forgot-password.yaml');
// Whitelist and set parameter defaults
$transformer = new RequestDataTransformer($schema);
$data = $transformer->transform($params);
// Validate, and halt on validation errors. Failed validation attempts do not count towards throttling limit.
$validator = new ServerSideValidator($schema, $this->ci->translator);
if (!$validator->validate($data)) {
$ms->addValidationErrors($validator);
return $response->withJson([], 400);
}
// Throttle requests
/** @var \UserFrosting\Sprinkle\Core\Throttle\Throttler $throttler */
$throttler = $this->ci->throttler;
$throttleData = [
'email' => $data['email'],
];
$delay = $throttler->getDelay('password_reset_request', $throttleData);
if ($delay > 0) {
$ms->addMessageTranslated('danger', 'RATE_LIMIT_EXCEEDED', ['delay' => $delay]);
return $response->withJson([], 429);
}
// Load the user, by email address
$user = $classMapper->getClassMapping('user')::where('email', $data['email'])->first();
if ($user) {
if (!$user->flag_verified) {
$ms->addMessageTranslated('danger', 'ACCOUNT.UNVERIFIED');
return $response->withJson([], 400);
}
}
// All checks passed! log events/activities, update user, and send email
// Begin transaction - DB will be rolled back if an exception occurs
Capsule::transaction(function () use ($classMapper, $data, $throttler, $throttleData, $config, $user) {
// Log throttleable event
$throttler->logEvent('password_reset_request', $throttleData);
// Check that the email exists.
// If there is no user with that email address, we should still pretend like we succeeded, to prevent account enumeration
if ($user) {
// Try to generate a new password reset request.
// Use timeout for "reset password"
$passwordReset = $this->ci->repoPasswordReset->create($user, $config['password_reset.timeouts.reset']);
// Create and send email
$message = new TwigMailMessage($this->ci->view, 'mail/password-reset.html.twig');
$message->from($config['address_book.admin'])
->addEmailRecipient(new EmailRecipient($user->email, $user->full_name))
->addParams([
'user' => $user,
'token' => $passwordReset->getToken(),
'request_date' => Carbon::now()->format('Y-m-d H:i:s'),
]);
$this->ci->mailer->send($message);
}
});
}
/**
* Account settings page.
*