From de1d7338a48bac5e9b80a65aebdf74073db32f6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=91cze=20Bence?= Date: Sun, 5 Jul 2020 00:09:02 +0200 Subject: [PATCH] MAPG-141 add reset password functionality --- mail/password-reset.html | 12 ++ src/Controller/LoginController.php | 138 +++++++++++++++++- views/login/password_reset_request.php | 14 ++ .../login/password_reset_request_success.php | 8 + views/login/reset_password.php | 20 +++ web.php | 7 + 6 files changed, 196 insertions(+), 3 deletions(-) create mode 100644 mail/password-reset.html create mode 100644 views/login/password_reset_request.php create mode 100644 views/login/password_reset_request_success.php create mode 100644 views/login/reset_password.php diff --git a/mail/password-reset.html b/mail/password-reset.html new file mode 100644 index 0000000..bf5b60d --- /dev/null +++ b/mail/password-reset.html @@ -0,0 +1,12 @@ +Hi, +

+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:
+{{RESET_LINK}}
+(This link expires at {{EXPIRES}}.) +

+If you did not requested password reset, no further action is required, your account is not touched. +

+Regards,
+{{APP_NAME}}
+{{BASE_URL}} diff --git a/src/Controller/LoginController.php b/src/Controller/LoginController.php index fb5573a..bc130c7 100644 --- a/src/Controller/LoginController.php +++ b/src/Controller/LoginController.php @@ -1,5 +1,6 @@ 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 request password reset!']]; 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 request password reset!']]; 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 sign up!']]; + 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(); + } } diff --git a/views/login/password_reset_request.php b/views/login/password_reset_request.php new file mode 100644 index 0000000..4f0b862 --- /dev/null +++ b/views/login/password_reset_request.php @@ -0,0 +1,14 @@ +@extends(templates/layout_normal) + +@section(main) +

Request password reset

+
+
+ +

+
+ +
+
+
+@endsection diff --git a/views/login/password_reset_request_success.php b/views/login/password_reset_request_success.php new file mode 100644 index 0000000..4e2eb1e --- /dev/null +++ b/views/login/password_reset_request_success.php @@ -0,0 +1,8 @@ +@extends(templates/layout_normal) + +@section(main) +

Request password reset

+
+

Password reset was successfully requested. Please check your email and click on the link to reset your password!

+
+@endsection diff --git a/views/login/reset_password.php b/views/login/reset_password.php new file mode 100644 index 0000000..ad8750a --- /dev/null +++ b/views/login/reset_password.php @@ -0,0 +1,20 @@ +@extends(templates/layout_normal) + +@section(main) +

Reset password

+
+ +
+ + + +

+
+ +
+
+ +

Confirming your identity failed. Please check the link you entered, or retry requesting password reset!

+ +
+@endsection diff --git a/web.php b/web.php index 6df0859..5576441 100644 --- a/web.php +++ b/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']);