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)
+
Password reset was successfully requested. Please check your email and click on the link to reset your password!
+Confirming your identity failed. Please check the link you entered, or retry requesting password reset!
+ +