death-notifier/src/main/php/com/fwdekker/deathnotifier/user/ResetPasswordAction.php

134 lines
4.3 KiB
PHP

<?php
namespace com\fwdekker\deathnotifier\user;
use com\fwdekker\deathnotifier\Action;
use com\fwdekker\deathnotifier\Config;
use com\fwdekker\deathnotifier\IllegalStateError;
use com\fwdekker\deathnotifier\mailer\Email;
use com\fwdekker\deathnotifier\mailer\EmailQueue;
use com\fwdekker\deathnotifier\validation\HasStringLengthRule;
use com\fwdekker\deathnotifier\validation\InvalidTypeException;
use com\fwdekker\deathnotifier\validation\InvalidValueException;
use com\fwdekker\deathnotifier\validation\IsEmailRule;
use com\fwdekker\deathnotifier\validation\IsStringRule;
use com\fwdekker\deathnotifier\validation\IsValidCsrfTokenRule;
use com\fwdekker\deathnotifier\validation\RuleSet;
/**
* Resets the user's password after they forgot it.
*
* @see ResetPasswordEmail
*/
class ResetPasswordAction extends Action
{
/**
* @var UserList the list to reset the password in
*/
private readonly UserList $user_list;
/**
* @var EmailQueue the queue to add a notification to
*/
private readonly EmailQueue $email_queue;
/**
* Constructs a new `ResetPasswordAction`.
*
* @param UserList $user_list the list to reset the password in
* @param EmailQueue $email_queue the queue to add a notification to
*/
public function __construct(UserList $user_list, EmailQueue $email_queue)
{
$this->user_list = $user_list;
$this->email_queue = $email_queue;
}
/**
* Resets the user's password after they forgot it.
*
* Requires that a valid CSRF token is present. Does not require the user to be logged in or out.
*
* @param array<int|string, mixed> $inputs `"token": string`: a valid CSRF token, `"email": string`: the email
* address of the account to reset the password of, `"password": string`: the new password to set,
* `"reset_token": string`: the token to reset the password with
* @return null
* @throws InvalidTypeException if any of the inputs has the incorrect type
* @throws InvalidValueException if no valid CSRF token is present, if no account with the given email address
* exists, if the password is too short or too long, if the reset token is invalid, or if the reset token has
* expired
*/
public function handle(array $inputs): mixed
{
(new RuleSet([
"token" => [new IsValidCsrfTokenRule()],
"email" => [new IsEmailRule()],
"password" => [new HasStringLengthRule(UserList::MIN_PASSWORD_LENGTH, UserList::MAX_PASSWORD_LENGTH)],
"reset_token" => [new IsStringRule()],
]))->check($inputs);
$this->user_list->transaction(function () use ($inputs) {
ValidatePasswordResetTokenAction::validate_token(
$this->user_list,
$inputs["email"],
$inputs["reset_token"]
);
$user_data = $this->user_list->get_user_by_email($inputs["email"]);
if ($user_data === null)
throw new IllegalStateError("User data is `null` despite previous validation.");
$this->user_list->set_password($user_data["uuid"], $inputs["password"]);
$this->email_queue->enqueue([new ResetPasswordEmail($inputs["email"])]);
});
return null;
}
}
/**
* An email informing a user that their password has been reset.
*
* @see ResetPasswordAction
*/
class ResetPasswordEmail extends Email
{
/**
* A string identifying the type of email.
*/
public const TYPE = "reset-password";
/**
* Constructs a new `ChangedPasswordEmail`.
*
* @param string $recipient the intended recipient of this email
*/
public function __construct(string $recipient)
{
parent::__construct($this::TYPE, $recipient);
}
public function get_subject(): string
{
return "Your password has been reset";
}
public function get_body(): string
{
$base_path = Config::get("server.base_path");
return
"You requested a password reset and changed the password of your Death Notifier account." .
"\n\n" .
"If you did not change the password of your account, go to the Death Notifier website and use the forgot " .
"password option to change your password back." .
"\n\n" .
$base_path;
}
}