diff options
author | Jonas Kohl | 2024-09-19 11:28:12 +0200 |
---|---|---|
committer | Jonas Kohl | 2024-09-19 11:28:12 +0200 |
commit | 35db6e71fc7196af1757be17d2a3919246476683 (patch) | |
tree | 07e7c6ddd3260cdbe7e662f434f1407a37c832d1 /src/index.php | |
parent | 01454544896827113e49db0d2848b5aab6ce14ae (diff) |
Add password reset
Diffstat (limited to 'src/index.php')
-rw-r--r-- | src/index.php | 152 |
1 files changed, 151 insertions, 1 deletions
diff --git a/src/index.php b/src/index.php index 801cc07..62874e6 100644 --- a/src/index.php +++ b/src/index.php @@ -15,6 +15,7 @@ use mystic\forum\orm\UserPermissions; use mystic\forum\utils\FileUtils; use mystic\forum\utils\RequestUtils; use mystic\forum\utils\ValidationUtils; +use Symfony\Component\Mailer\Exception\TransportException; use Symfony\Component\Mailer\Transport; use Symfony\Component\Mime\Address; use Symfony\Component\Mime\Email; @@ -70,6 +71,41 @@ function generateCaptchaText(): string { return $phrase; } +function generatePasswordResetLink(Database &$db, User &$user): ?string { + $token = $db->generateId(20); + + $user->passwordResetToken = $token; + $user->passwordResetTokenCreated = new \DateTimeImmutable(); + + if (!$db->update($user)) { + return null; + } + + return env("PUBLIC_URL") . "?_action=pwreset&token=" . urlencode($user->passwordResetToken) . "&sig=" . urlencode(base64_encode(hash("sha256", env("SECRET") . $user->passwordResetToken . $user->id . $user->passwordHash, true))); +} + +function decodePasswordResetLink(Database &$db, string $token, string $signature): ?User { + $user = new User(); + $user->passwordResetToken = $token; + + if (!$db->fetchWhere($user, "password_reset_token")) { + return null; + } + + $then = $user->passwordResetTokenCreated; + $now = new \DateTimeImmutable(); + if (($now->getTimestamp() - $then->getTimestamp()) >= 3600) { + return null; + } + + $expectedSignature = base64_encode(hash("sha256", env("SECRET") . $user->passwordResetToken . $user->id . $user->passwordHash, true)); + if ($expectedSignature !== $signature) { + return null; + } + + return $user; +} + function getThemeAndLangInfo(): array { $availableThemes = []; $currentTheme = $_GET["theme"] ?? $_COOKIE["theme"] ?? env("MYSTIC_FORUM_THEME") ?? "default"; @@ -385,7 +421,7 @@ if ($_action === "auth") { "forum_copyright" => (env("MYSTIC_FORUM_COPYRIGHT") ?? env("MYSTIC_FORUM_TITLE") ?? "Forum") ] )) - ->subject("Please activate your account") + ->subject(__("Please activate your account")) ); $db->insert($user); @@ -1311,6 +1347,120 @@ if ($_action === "auth") { $next = $_POST["next"] ?? "."; setcookie("lang", $lang, time()+60*60*24*30); header("Location: $next"); +} elseif ($_action === "pwreset") { + if ($currentUser) { + header("Location: ."); + exit; + } + + if (RequestUtils::isRequestMethod("POST")) { + $token = $_GET["token"] ?? null; + $signature = $_GET["sig"] ?? null; + + if ($token !== null && $signature !== null) { + RequestUtils::setFormErrorDestination("?_action=pwreset&token=" . urlencode($token) . "&sig=" . urlencode($signature)); + $formId = "pwnew"; + $newPassword = RequestUtils::getRequiredField("new_password", $formId); + $retypePassword = RequestUtils::getRequiredField("retype_password", $formId); + $resetUser = decodePasswordResetLink($db, $token, $signature); + + if ($resetUser === null) { + http_response_code(400); + msg_error(__("The password reset link is either invalid or it expired"), true); + exit; + } + + if ($newPassword !== $retypePassword) { + RequestUtils::triggerFormError(__("New passwords don't match"), $formId); + } + + if (strlen($newPassword) < 8) { + RequestUtils::triggerFormError(__("Password too short! Your password must consist of 8 or more characters"), $formId); + } + + $resetUser->passwordHash = password_hash($newPassword, PASSWORD_DEFAULT); + $resetUser->passwordResetToken = null; + $resetUser->passwordResetTokenCreated = null; + + if (!$db->update($resetUser)) { + RequestUtils::triggerFormError(__("Failed to update password"), $formId); + } + + msg_info(__("Password reset successfully!"), true); + } else { + $formId = "pwreset"; + $email = RequestUtils::getRequiredField("email", $formId); + + $user = new User(); + $user->email = $email; + + if ($db->fetchWhere($user, "email")) { + try { + Transport::fromDsn(env("MAILER_DSN"))->send( + (new Email()) + ->from(env("MAILER_FROM")) + ->to(new Address($user->email, $user->displayName)) + ->text(__( + "Hello, %user_display_name%!\n" . + "\n" . + "a password reset has been requested successfully! Please click the link below to set a new password:\n" . + "%reset_link%\n" . + "\n" . + "If this wasn't you, you can safely ignore this email. The link will only be valid for one hour.\n" . + "\n" . + "Kind regards,\n" . + "%forum_copyright%", + params: [ + "forum_title" => (env("MYSTIC_FORUM_TITLE") ?? "Forum"), + "user_display_name" => $user->displayName, + "reset_link" => generatePasswordResetLink($db, $user), + "forum_copyright" => (env("MYSTIC_FORUM_COPYRIGHT") ?? env("MYSTIC_FORUM_TITLE") ?? "Forum") + ] + )) + ->subject(__("Forgot your password? No problem!")) + ); + } catch (TransportException $_) { + // fail silently + } + } else { + // don't make the delay difference too obvious + usleep(random_int(900, 4500) * 1000); + + // ideally, at some point we would just want to queue up the email + // and send it asynchronously, but this'll have to do for now + } + + msg_info(__("If an account exists with the given email address, we will have sent a password reset link to that email address."), true); + } + } else { + $token = $_GET["token"] ?? null; + $signature = $_GET["sig"] ?? null; + + if ($token !== null && $signature !== null) { + $resetUser = decodePasswordResetLink($db, $token, $signature); + if ($resetUser === null) { + http_response_code(400); + msg_error(__("The password reset link is either invalid or it expired"), true); + exit; + } + + _view("template_start", [ "_title" => __("Reset password") ]); + _view("template_navigation_start"); + _view("template_navigation_end"); + _view("form_new_password", [ + "token" => $token, + "signature" => $signature, + ]); + _view("template_end", [...getThemeAndLangInfo()]); + } else { + _view("template_start", [ "_title" => __("Reset password") ]); + _view("template_navigation_start"); + _view("template_navigation", ["user" => null]); + _view("template_navigation_end"); + _view("form_password_reset"); + _view("template_end", [...getThemeAndLangInfo()]); + } + } } elseif ($_action === "ctheme") { // options $enableLogging = true; |