MAPG-141 add reset password functionality

This commit is contained in:
Bence Pőcze 2020-07-05 00:09:02 +02:00
parent 7539f637b0
commit de1d7338a4
Signed by: bence
GPG Key ID: AA52B11A3269D1C1
6 changed files with 196 additions and 3 deletions

12
mail/password-reset.html Normal file
View File

@ -0,0 +1,12 @@
Hi,
<br><br>
You recently requested password reset on {{APP_NAME}} with this email address ({{EMAIL}}).
To reset the password to your account, please click on the following link:<br>
<a href="{{RESET_LINK}}" title="Reset password">{{RESET_LINK}}</a><br>
(This link expires at {{EXPIRES}}.)
<br><br>
If you did not requested password reset, no further action is required, your account is not touched.
<br><br>
Regards,<br>
{{APP_NAME}}<br>
<a href="{{BASE_URL}}" title="{{APP_NAME}}">{{BASE_URL}}</a>

View File

@ -1,5 +1,6 @@
<?php namespace MapGuesser\Controller; <?php namespace MapGuesser\Controller;
use DateTime;
use MapGuesser\Http\Request; use MapGuesser\Http\Request;
use MapGuesser\Interfaces\Request\IRequest; use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Interfaces\Response\IContent; use MapGuesser\Interfaces\Response\IContent;
@ -8,8 +9,10 @@ use MapGuesser\Mailing\Mail;
use MapGuesser\OAuth\GoogleOAuth; use MapGuesser\OAuth\GoogleOAuth;
use MapGuesser\PersistentData\Model\User; use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\Model\UserConfirmation; use MapGuesser\PersistentData\Model\UserConfirmation;
use MapGuesser\PersistentData\Model\UserPasswordResetter;
use MapGuesser\PersistentData\PersistentDataManager; use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\Repository\UserConfirmationRepository; use MapGuesser\Repository\UserConfirmationRepository;
use MapGuesser\Repository\UserPasswordResetterRepository;
use MapGuesser\Repository\UserRepository; use MapGuesser\Repository\UserRepository;
use MapGuesser\Response\HtmlContent; use MapGuesser\Response\HtmlContent;
use MapGuesser\Response\JsonContent; use MapGuesser\Response\JsonContent;
@ -26,12 +29,15 @@ class LoginController
private UserConfirmationRepository $userConfirmationRepository; private UserConfirmationRepository $userConfirmationRepository;
private UserPasswordResetterRepository $userPasswordResetterRepository;
public function __construct(IRequest $request) public function __construct(IRequest $request)
{ {
$this->request = $request; $this->request = $request;
$this->pdm = new PersistentDataManager(); $this->pdm = new PersistentDataManager();
$this->userRepository = new UserRepository(); $this->userRepository = new UserRepository();
$this->userConfirmationRepository = new UserConfirmationRepository(); $this->userConfirmationRepository = new UserConfirmationRepository();
$this->userPasswordResetterRepository = new UserPasswordResetterRepository();
} }
public function getLoginForm() public function getLoginForm()
@ -97,6 +103,41 @@ class LoginController
return new HtmlContent('login/google_signup', $data); return new HtmlContent('login/google_signup', $data);
} }
public function getRequestPasswordResetForm()
{
if ($this->request->user() !== null) {
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
}
$data = ['email' => $this->request->query('email')];
return new HtmlContent('login/password_reset_request', $data);
}
public function getRequestPasswordResetSuccess(): IContent
{
return new HtmlContent('login/password_reset_request_success');
}
public function getResetPasswordForm()
{
if ($this->request->user() !== null) {
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
}
$token = $this->request->query('token');
$resetter = $this->userPasswordResetterRepository->getByToken($token);
if ($resetter === null || $resetter->getExpiresDate() < new DateTime()) {
$data = ['success' => false];
return new HtmlContent('login/reset_password', $data);
}
$user = $this->userRepository->getById($resetter->getUserId());
$data = ['success' => true, 'token' => $token, 'email' => $user->getEmail()];
return new HtmlContent('login/reset_password', $data);
}
public function login(): IContent public function login(): IContent
{ {
if ($this->request->user() !== null) { if ($this->request->user() !== null) {
@ -127,7 +168,7 @@ class LoginController
} }
if (!$user->checkPassword($this->request->post('password'))) { if (!$user->checkPassword($this->request->post('password'))) {
$data = ['error' => ['errorText' => 'The given password is wrong.']]; $data = ['error' => ['errorText' => 'The given password is wrong. You can <a href="/password/requestReset?email=' . urlencode($user->getEmail()) . '" title="Request password reset">request password reset</a>!']];
return new JsonContent($data); return new JsonContent($data);
} }
@ -196,7 +237,7 @@ class LoginController
if ($user !== null) { if ($user !== null) {
if ($user->getActive()) { if ($user->getActive()) {
if (!$user->checkPassword($this->request->post('password'))) { if (!$user->checkPassword($this->request->post('password'))) {
$data = ['error' => ['errorText' => 'There is a user already registered with the given email address, but the given password is wrong.']]; $data = ['error' => ['errorText' => 'There is a user already registered with the given email address, but the given password is wrong. You can <a href="/password/requestReset?email=' . urlencode($user->getEmail()) . '" title="Request password reset">request password reset</a>!']];
return new JsonContent($data); return new JsonContent($data);
} }
@ -370,6 +411,84 @@ class LoginController
return new HtmlContent('login/cancel', $data); return new HtmlContent('login/cancel', $data);
} }
public function requestPasswordReset(): IContent
{
if ($this->request->user() !== null) {
$data = ['redirect' => ['target' => '/' . \Container::$routeCollection->getRoute('home')->generateLink()]];
return new JsonContent($data);
}
$user = $this->userRepository->getByEmail($this->request->post('email'));
if ($user === null) {
$data = ['error' => ['errorText' => 'No user found with the given email address. You can <a href="/signup" title="Sign up">sign up</a>!']];
return new JsonContent($data);
}
if (!$user->getActive()) {
$data = ['error' => ['errorText' => 'User found with the given email address, but the account is not activated. Please check your email and click on the activation link!']];
return new JsonContent($data);
}
$token = bin2hex(random_bytes(16));
$expires = new DateTime('+1 hour');
$passwordResetter = new UserPasswordResetter();
$passwordResetter->setUser($user);
$passwordResetter->setToken($token);
$passwordResetter->setExpiresDate($expires);
$this->pdm->saveToDb($passwordResetter);
$this->sendPasswordResetEmail($user->getEmail(), $token, $expires);
$data = ['success' => true];
return new JsonContent($data);
}
public function resetPassword(): IContent
{
if ($this->request->user() !== null) {
$data = ['redirect' => ['target' => '/' . \Container::$routeCollection->getRoute('home')->generateLink()]];
return new JsonContent($data);
}
$token = $this->request->query('token');
$resetter = $this->userPasswordResetterRepository->getByToken($token);
if ($resetter === null || $resetter->getExpiresDate() < new DateTime()) {
$data = ['redirect' => ['target' => '/' . \Container::$routeCollection->getRoute('password-reset')->generateLink(['token' => $token])]];
return new JsonContent($data);
}
if (strlen($this->request->post('password')) < 6) {
$data = ['error' => ['errorText' => 'The given password is too short. Please choose a password that is at least 6 characters long!']];
return new JsonContent($data);
}
if ($this->request->post('password') !== $this->request->post('password_confirm')) {
$data = ['error' => ['errorText' => 'The given passwords do not match.']];
return new JsonContent($data);
}
\Container::$dbConnection->startTransaction();
$this->pdm->deleteFromDb($resetter);
$user = $this->userRepository->getById($resetter->getUserId());
$user->setPlainPassword($this->request->post('password'));
$this->pdm->saveToDb($user);
\Container::$dbConnection->commit();
$this->request->setUser($user);
$data = ['success' => true];
return new JsonContent($data);
}
private function sendConfirmationEmail(string $email, string $token): void private function sendConfirmationEmail(string $email, string $token): void
{ {
$mail = new Mail(); $mail = new Mail();
@ -377,7 +496,7 @@ class LoginController
$mail->setSubject('Welcome to ' . $_ENV['APP_NAME'] . ' - Activate your account'); $mail->setSubject('Welcome to ' . $_ENV['APP_NAME'] . ' - Activate your account');
$mail->setBodyFromTemplate('signup', [ $mail->setBodyFromTemplate('signup', [
'EMAIL' => $email, 'EMAIL' => $email,
'ACTIVATE_LINK' => $this->request->getBase() . '/'. \Container::$routeCollection->getRoute('signup.activate')->generateLink(['token' => $token]), 'ACTIVATE_LINK' => $this->request->getBase() . '/' . \Container::$routeCollection->getRoute('signup.activate')->generateLink(['token' => $token]),
'CANCEL_LINK' => $this->request->getBase() . '/' . \Container::$routeCollection->getRoute('signup.cancel')->generateLink(['token' => $token]), 'CANCEL_LINK' => $this->request->getBase() . '/' . \Container::$routeCollection->getRoute('signup.cancel')->generateLink(['token' => $token]),
]); ]);
$mail->send(); $mail->send();
@ -393,4 +512,17 @@ class LoginController
]); ]);
$mail->send(); $mail->send();
} }
private function sendPasswordResetEmail(string $email, string $token, DateTime $expires): void
{
$mail = new Mail();
$mail->addRecipient($email);
$mail->setSubject($_ENV['APP_NAME'] . ' - Password reset');
$mail->setBodyFromTemplate('password-reset', [
'EMAIL' => $email,
'RESET_LINK' => $this->request->getBase() . '/' . \Container::$routeCollection->getRoute('password-reset')->generateLink(['token' => $token]),
'EXPIRES' => $expires->format('Y-m-d H:i:s T')
]);
$mail->send();
}
} }

View File

@ -0,0 +1,14 @@
@extends(templates/layout_normal)
@section(main)
<h2>Request password reset</h2>
<div class="box">
<form id="passwordResetForm" action="/password/requestReset" method="post" data-redirect-on-success="/password/requestReset/success">
<input class="big fullWidth" type="email" name="email" placeholder="Email address" value="<?= isset($email) ? $email : '' ?>" required autofocus>
<p id="passwordResetFormError" class="formError justify marginTop"></p>
<div class="right marginTop">
<button type="submit">Continue</button>
</div>
</form>
</div>
@endsection

View File

@ -0,0 +1,8 @@
@extends(templates/layout_normal)
@section(main)
<h2>Request password reset</h2>
<div class="box">
<p class="justify">Password reset was successfully requested. Please check your email and click on the link to reset your password!</p>
</div>
@endsection

View File

@ -0,0 +1,20 @@
@extends(templates/layout_normal)
@section(main)
<h2>Reset password</h2>
<div class="box">
<?php if ($success) : ?>
<form id="resetPasswordForm" action="/password/reset/<?= $token ?>" method="post" data-redirect-on-success="/">
<input class="big fullWidth" type="email" name="email" placeholder="Email address" value="<?= $email ?>" disabled>
<input class="big fullWidth marginTop" type="password" name="password" placeholder="Password" required minlength="6" autofocus>
<input class="big fullWidth marginTop" type="password" name="password_confirm" placeholder="Password confirmation" required minlength="6">
<p id="resetPasswordFormError" class="formError justify marginTop"></p>
<div class="right">
<button class="marginTop" type="submit">Reset password</button>
</div>
</form>
<?php else: ?>
<p class="error justify">Confirming your identity failed. Please check the link you entered, or retry <a href="/password/requestReset" title="Request password reset">requesting password reset</a>!</p>
<?php endif; ?>
</div>
@endsection

View File

@ -31,6 +31,13 @@ Container::$routeCollection->group('signup', function (MapGuesser\Routing\RouteC
$routeCollection->get('signup.activate', 'activate/{token}', [MapGuesser\Controller\LoginController::class, 'activate']); $routeCollection->get('signup.activate', 'activate/{token}', [MapGuesser\Controller\LoginController::class, 'activate']);
$routeCollection->get('signup.cancel', 'cancel/{token}', [MapGuesser\Controller\LoginController::class, 'cancel']); $routeCollection->get('signup.cancel', 'cancel/{token}', [MapGuesser\Controller\LoginController::class, 'cancel']);
}); });
Container::$routeCollection->group('password', function (MapGuesser\Routing\RouteCollection $routeCollection) {
$routeCollection->get('password-requestReset', 'requestReset', [MapGuesser\Controller\LoginController::class, 'getRequestPasswordResetForm']);
$routeCollection->post('password-requestReset-action', 'requestReset', [MapGuesser\Controller\LoginController::class, 'requestPasswordReset']);
$routeCollection->get('password-requestReset.success', 'requestReset/success', [MapGuesser\Controller\LoginController::class, 'getRequestPasswordResetSuccess']);
$routeCollection->get('password-reset', 'reset/{token}', [MapGuesser\Controller\LoginController::class, 'getResetPasswordForm']);
$routeCollection->post('password-reset.action', 'reset/{token}', [MapGuesser\Controller\LoginController::class, 'resetPassword']);
});
Container::$routeCollection->get('logout', 'logout', [MapGuesser\Controller\LoginController::class, 'logout']); Container::$routeCollection->get('logout', 'logout', [MapGuesser\Controller\LoginController::class, 'logout']);
Container::$routeCollection->group('account', function (MapGuesser\Routing\RouteCollection $routeCollection) { Container::$routeCollection->group('account', function (MapGuesser\Routing\RouteCollection $routeCollection) {
$routeCollection->get('account', '', [MapGuesser\Controller\UserController::class, 'getAccount']); $routeCollection->get('account', '', [MapGuesser\Controller\UserController::class, 'getAccount']);