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

146 lines
4.6 KiB
PHP

<?php
namespace com\fwdekker\deathnotifier\user;
use com\fwdekker\deathnotifier\Action;
use com\fwdekker\deathnotifier\Config;
use com\fwdekker\deathnotifier\mailer\Email;
use com\fwdekker\deathnotifier\mailer\EmailQueue;
use com\fwdekker\deathnotifier\Util;
use com\fwdekker\deathnotifier\validation\InvalidTypeException;
use com\fwdekker\deathnotifier\validation\InvalidValueException;
use com\fwdekker\deathnotifier\validation\IsEmailRule;
use com\fwdekker\deathnotifier\validation\IsValidCsrfTokenRule;
use com\fwdekker\deathnotifier\validation\RuleSet;
/**
* Sends a password reset email.
*
* @see SendPasswordResetEmail
*/
class SendPasswordResetAction extends Action
{
/**
* @var UserList the list containing the user to send a password reset email for
*/
private readonly UserList $user_list;
/**
* @var EmailQueue the queue to add the password reset email to
*/
private readonly EmailQueue $email_queue;
/**
* Constructs a new `SendPasswordResetAction`.
*
* @param UserList $user_list the list containing the user to send a password reset email for
* @param EmailQueue $email_queue the queue to add the password reset email to
*/
public function __construct(UserList $user_list, EmailQueue $email_queue)
{
$this->user_list = $user_list;
$this->email_queue = $email_queue;
}
/**
* Sends a password reset email.
*
* 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 send a password reset email for
* @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, or if a password reset email was sent too recently
*/
public function handle(array $inputs): mixed
{
(new RuleSet([
"token" => [new IsValidCsrfTokenRule()],
"email" => [new IsEmailRule()],
]))->check($inputs);
$this->user_list->transaction(function () use ($inputs) {
$user_data = $this->user_list->get_user_by_email($inputs["email"]);
if ($user_data === null)
throw new InvalidValueException("No user with that email address has been registered.");
$minutes_left = Util::minutes_until_interval_elapsed(
$user_data["password_reset_token_timestamp"],
UserList::MINUTES_BETWEEN_PASSWORD_RESETS
);
if ($minutes_left > 0) {
throw new InvalidValueException(
"A password reset email was sent recently. " .
"Please wait $minutes_left more minute(s) before requesting a new email."
);
}
$token = $this->user_list->register_password_reset($inputs["email"]);
$this->email_queue->enqueue([new SendPasswordResetEmail($inputs["email"], $token)]);
});
return null;
}
}
/**
* An email to help a user reset their password.
*
* @see SendPasswordResetAction
*/
class SendPasswordResetEmail extends Email
{
/**
* A string identifying the type of email.
*/
public const TYPE = "reset-password";
/**
* @var string the token to reset the password with
*/
public string $token;
/**
* Constructs a new `ResetPasswordEmail`.
*
* @param string $recipient the intended recipient of this email
* @param string $token the token to reset the password with
*/
public function __construct(string $recipient, string $token)
{
parent::__construct($this::TYPE, $recipient);
$this->token = $token;
}
public function get_subject(): string
{
return "Reset your password";
}
public function get_body(): string
{
$base_path = Config::get("server.base_path");
$verify_path =
"$base_path?action=reset-password&email=" . rawurlencode($this->recipient) . "&token=$this->token";
return
"You requested a password reset link for your Death Notifier account. " .
"You can choose a new password by clicking the link below. " .
"This link expires after " . UserList::MINUTES_VALID_PASSWORD_RESET . " minutes." .
"\n" .
"Reset password: $verify_path" .
"\n\n" .
"If you did not request a new password, you can safely ignore this message." .
"\n\n" .
$base_path;
}
}