MAPG-141 add reset password functionality
This commit is contained in:
parent
7539f637b0
commit
de1d7338a4
12
mail/password-reset.html
Normal file
12
mail/password-reset.html
Normal 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>
|
@ -1,5 +1,6 @@
|
||||
<?php namespace MapGuesser\Controller;
|
||||
|
||||
use DateTime;
|
||||
use MapGuesser\Http\Request;
|
||||
use MapGuesser\Interfaces\Request\IRequest;
|
||||
use MapGuesser\Interfaces\Response\IContent;
|
||||
@ -8,8 +9,10 @@ use MapGuesser\Mailing\Mail;
|
||||
use MapGuesser\OAuth\GoogleOAuth;
|
||||
use MapGuesser\PersistentData\Model\User;
|
||||
use MapGuesser\PersistentData\Model\UserConfirmation;
|
||||
use MapGuesser\PersistentData\Model\UserPasswordResetter;
|
||||
use MapGuesser\PersistentData\PersistentDataManager;
|
||||
use MapGuesser\Repository\UserConfirmationRepository;
|
||||
use MapGuesser\Repository\UserPasswordResetterRepository;
|
||||
use MapGuesser\Repository\UserRepository;
|
||||
use MapGuesser\Response\HtmlContent;
|
||||
use MapGuesser\Response\JsonContent;
|
||||
@ -26,12 +29,15 @@ class LoginController
|
||||
|
||||
private UserConfirmationRepository $userConfirmationRepository;
|
||||
|
||||
private UserPasswordResetterRepository $userPasswordResetterRepository;
|
||||
|
||||
public function __construct(IRequest $request)
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->pdm = new PersistentDataManager();
|
||||
$this->userRepository = new UserRepository();
|
||||
$this->userConfirmationRepository = new UserConfirmationRepository();
|
||||
$this->userPasswordResetterRepository = new UserPasswordResetterRepository();
|
||||
}
|
||||
|
||||
public function getLoginForm()
|
||||
@ -97,6 +103,41 @@ class LoginController
|
||||
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
|
||||
{
|
||||
if ($this->request->user() !== null) {
|
||||
@ -127,7 +168,7 @@ class LoginController
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@ -196,7 +237,7 @@ class LoginController
|
||||
if ($user !== null) {
|
||||
if ($user->getActive()) {
|
||||
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);
|
||||
}
|
||||
|
||||
@ -370,6 +411,84 @@ class LoginController
|
||||
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
|
||||
{
|
||||
$mail = new Mail();
|
||||
@ -377,7 +496,7 @@ class LoginController
|
||||
$mail->setSubject('Welcome to ' . $_ENV['APP_NAME'] . ' - Activate your account');
|
||||
$mail->setBodyFromTemplate('signup', [
|
||||
'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]),
|
||||
]);
|
||||
$mail->send();
|
||||
@ -393,4 +512,17 @@ class LoginController
|
||||
]);
|
||||
$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();
|
||||
}
|
||||
}
|
||||
|
14
views/login/password_reset_request.php
Normal file
14
views/login/password_reset_request.php
Normal 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
|
8
views/login/password_reset_request_success.php
Normal file
8
views/login/password_reset_request_success.php
Normal 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
|
20
views/login/reset_password.php
Normal file
20
views/login/reset_password.php
Normal 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
|
7
web.php
7
web.php
@ -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.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->group('account', function (MapGuesser\Routing\RouteCollection $routeCollection) {
|
||||
$routeCollection->get('account', '', [MapGuesser\Controller\UserController::class, 'getAccount']);
|
||||
|
Loading…
Reference in New Issue
Block a user