diff --git a/Layout/default/UserPasswordReset/forgot-password-sent.php b/Layout/default/UserPasswordReset/forgot-password-sent.php new file mode 100644 index 000000000..984847279 --- /dev/null +++ b/Layout/default/UserPasswordReset/forgot-password-sent.php @@ -0,0 +1,41 @@ + + + + + + <?= MFAPPNAME_FULL ?> | Link versendet + + + + + + + + + +
+ +
+ +
+
+ + \ No newline at end of file diff --git a/Layout/default/UserPasswordReset/forgot-password.php b/Layout/default/UserPasswordReset/forgot-password.php new file mode 100644 index 000000000..41ad09c2e --- /dev/null +++ b/Layout/default/UserPasswordReset/forgot-password.php @@ -0,0 +1,48 @@ + + + + + + <?= MFAPPNAME_FULL ?> | Passwort vergessen + + + + + + + + +
+ +
+ +
+
+ + \ No newline at end of file diff --git a/Layout/default/UserPasswordReset/reset-password.php b/Layout/default/UserPasswordReset/reset-password.php new file mode 100644 index 000000000..226766d6d --- /dev/null +++ b/Layout/default/UserPasswordReset/reset-password.php @@ -0,0 +1,57 @@ + + + + + + <?= MFAPPNAME_FULL ?> | Passwort zurücksetzen + + + + + + + + +
+ +
+ +
+
+ + \ No newline at end of file diff --git a/Layout/default/mfLogin/Index.php b/Layout/default/mfLogin/Index.php index 6db44b77d..cfdaeaff0 100644 --- a/Layout/default/mfLogin/Index.php +++ b/Layout/default/mfLogin/Index.php @@ -19,7 +19,7 @@ if ($requesttype == "2fa" || $requesttype == "false2fa") {
-
@@ -87,6 +87,7 @@ if ($requesttype == "2fa" || $requesttype == "false2fa") { .cursor-pointer { cursor: pointer; } + .noselect { -webkit-touch-callout: none; /* iOS Safari */ -webkit-user-select: none; /* Safari */ @@ -134,6 +135,9 @@ if ($requesttype == "2fa" || $requesttype == "false2fa") {
@@ -166,6 +170,13 @@ if ($requesttype == "2fa" || $requesttype == "false2fa") { $('#TwofactorCode').remove(); $('form').submit(); }); + $(document).ready(function () { + if ($('#TwofactorCode').length > 0) { + $('#TwofactorCode').focus(); + } else { + $('#mfUsername').focus(); + } + }) diff --git a/application/UserPasswordReset/UserPasswordResetController.php b/application/UserPasswordReset/UserPasswordResetController.php new file mode 100644 index 000000000..974e85a11 --- /dev/null +++ b/application/UserPasswordReset/UserPasswordResetController.php @@ -0,0 +1,108 @@ +layout()->setTemplate("UserPasswordReset/forgot-password"); + } + + protected function sendResetLinkAction($request) + { + $username = $this->db()->escape($request['Username']); + $res = $this->db()->select(MFUSERTABLE, "*", "username='$username' OR email='$username'"); + + if ($this->db()->num_rows($res)) { + $user = $this->db()->fetch_object($res); + $token = bin2hex(random_bytes(32)); + $expires = time() + 3600; + + $this->db()->update(MFUSERTABLE, array( + 'password_reset_token' => $token, + 'password_reset_expires' => $expires + ), "id=" . (int)$user->id); + + $resetLink = "https://".MFAPPNAME.".xinon.at/UserPasswordReset/resetPassword?token=" . $token; + + $email = new Emailnotification(); + $email->setTo($user->email); + $email->setFrom('noreply@xinon.at','XINON No-Reply'); + $email->setSubject("Passwort zurücksetzen für " . MFAPPNAME_FULL); + + $textBody = "Hallo " . $user->username . ",\n\n" . + "Klicke auf den folgenden Link, um dein Passwort zurückzusetzen. Der Link ist eine Stunde gültig.\n\n" . + $resetLink . "\n\n" . + "Wenn du diese Anfrage nicht gestellt hast, ignoriere diese E-Mail.\n"; + + $htmlBody = "

Passwort zurücksetzen

" . + "

Hallo " . $user->username . ",

" . + "

Klicke auf den folgenden Link, um dein Passwort zurückzusetzen. Der Link ist eine Stunde gültig.

" . + '

Passwort jetzt zurücksetzen

' . + "

Wenn du diese Anfrage nicht gestellt hast, ignoriere diese E-Mail bitte.

"; + + $email->setBody($textBody); + $email->setHtmlBody($htmlBody); + $email->send(); + } + $this->layout()->setTemplate("UserPasswordReset/forgot-password-sent"); + } + + protected function resetPasswordAction($request) + { + if (empty($request['token'])) { + self::redirect("mfLogin", "index"); + return; + } + + $token = $this->db()->escape($request['token']); + $res = $this->db()->select(MFUSERTABLE, "*", "password_reset_token='$token' AND password_reset_expires > " . time()); + + if (!$this->db()->num_rows($res)) { + $this->layout()->setTemplate("mfLogin/Index"); + return; + } + + $this->layout()->set("token", $token); + $this->layout()->setTemplate("UserPasswordReset/reset-password"); + } + + protected function updatePasswordAction($request) + { + $token = $this->db()->escape($request['token']); + $password = $request['Password']; + $password_confirm = $request['Password_confirm']; + + if (empty($token) || empty($password) || $password !== $password_confirm) { + $error="Passwörter stimmen nicht überein oder die Anfrage ist ungültig."; + $this->layout()->set("error",$error,); + + $this->layout()->set("token", $token); + $this->layout()->setTemplate("UserPasswordReset/reset-password"); + return; + } + + $res = $this->db()->select(MFUSERTABLE, "*", "password_reset_token='$token' AND password_reset_expires > " . time()); + + if (!$this->db()->num_rows($res)) { + $this->layout()->setTemplate("mfLogin/Index"); + return; + } + + $user = $this->db()->fetch_object($res); + $new_hash = mfLoginController::generatePasswordHash($password); + + $this->db()->update(MFUSERTABLE, array( + 'password' => $new_hash, + 'password_reset_token' => NULL, + 'password_reset_expires' => NULL + ), "id=" . (int)$user->id); + + $this->layout()->setFlash("Dein Passwort wurde erfolgreich geändert. Du kannst dich jetzt einloggen.", "success"); + self::redirect("mfLogin", "index"); + } +} \ No newline at end of file diff --git a/db/migrations/20250805145608_worker_add_password_reset.php b/db/migrations/20250805145608_worker_add_password_reset.php new file mode 100644 index 000000000..3c2a8b156 --- /dev/null +++ b/db/migrations/20250805145608_worker_add_password_reset.php @@ -0,0 +1,33 @@ +getEnvironment() == "thetool") { + $table = $this->table("Worker"); + $table->addColumn("password_reset_token", "string", ["null" => true, "after" => "twofactorrequired"]); + $table->addColumn("password_reset_expires", "integer", ["null" => true, "default" => null, "after" => "password_reset_token"]); + $table->update(); + } + + if($this->getEnvironment() == "addressdb") { + + } + } + + public function down(): void + { + if($this->getEnvironment() == "thetool") { + $this->table("Worker")->removeColumn("password_reset_token")->save(); + $this->table("Worker")->removeColumn("password_reset_expires")->save(); + } + + if($this->getEnvironment() == "addressdb") { + + } + } +}