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

272 lines
9.9 KiB
PHP

<?php
namespace com\fwdekker\deathnotifier\user;
use com\fwdekker\deathnotifier\Database;
use com\fwdekker\deathnotifier\LoggerUtil;
use com\fwdekker\deathnotifier\mailer\MailManager;
use com\fwdekker\deathnotifier\Response;
use Monolog\Logger;
use PDO;
/**
* Manages interaction with the database in the context of users.
*/
class UserManager
{
/**
* The minimum length of a password;
*/
public const MIN_PASSWORD_LENGTH = 8;
/**
* The maximum length of a password.
*/
public const MAX_PASSWORD_LENGTH = 64;
/**
* The minimum number of minutes between two verification emails.
*/
public const MINUTES_BETWEEN_VERIFICATION_EMAILS = 5;
/**
* The maximum number of minutes after which a validation email expires.
*/
public const MINUTES_VALID_VERIFICATION = 60;
/**
* The minimum number of minutes between two password reset emails.
*/
public const MINUTES_BETWEEN_PASSWORD_RESETS = 5;
/**
* The maximum number of minutes after which a password reset email expires.
*/
public const MINUTES_VALID_PASSWORD_RESET = 60;
/**
* @var Logger the logger to use for logging
*/
private Logger $logger; // @phpstan-ignore-line Unused, but useful for debugging
/**
* @var PDO the database connection to interact with
*/
private PDO $conn;
/**
* @var MailManager the mailer to send emails with
*/
private MailManager $mailer;
/**
* Constructs a new user manager.
*
* @param PDO $conn the database connection to interact with
* @param MailManager $mailer the mailer to send emails with
*/
public function __construct(PDO $conn, MailManager $mailer)
{
$this->logger = LoggerUtil::with_name($this::class);
$this->conn = $conn;
$this->mailer = $mailer;
}
/**
* Populates the database with the necessary structures for users.
*
* @return void
*/
public function install(): void
{
$this->conn->exec("CREATE TABLE users(uuid TEXT NOT NULL UNIQUE PRIMARY KEY DEFAULT(lower(hex(randomblob(16)))),
email TEXT NOT NULL UNIQUE,
email_verification_token TEXT DEFAULT(lower(hex(randomblob(16)))),
email_verification_token_timestamp INT NOT NULL DEFAULT(unixepoch()),
email_notifications_enabled INT NOT NULL DEFAULT(1),
password TEXT NOT NULL,
password_last_change INT NOT NULL DEFAULT(unixepoch()),
password_reset_token TEXT DEFAULT(null),
password_reset_token_timestamp INT NOT NULL DEFAULT(unixepoch()));");
}
/**
* Registers a new user.
*
* Throws an exception if a user with the given email address is already registered.
*
* @param string $email the user's email address
* @param string $password the user's password
* @return string the user's UUID
*/
public function add_user(string $email, string $password): string
{
$stmt = $this->conn->prepare("INSERT INTO users (email, password)
VALUES (:email, :password)
RETURNING uuid;");
$stmt->bindValue(":email", $email);
$stmt->bindValue(":password", password_hash($password, PASSWORD_BCRYPT));
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC)[0]["uuid"];
}
/**
* Returns `true` if and only if a user exists with the given UUID.
*
* @param string $uuid the UUID to check existence of
* @return bool `true` if and only if a user exists with the given UUID
*/
public function has_user_with_uuid(string $uuid): bool
{
$stmt = $this->conn->prepare("SELECT EXISTS(SELECT 1 FROM users WHERE uuid=:uuid);");
$stmt->bindValue(":uuid", $uuid);
$stmt->execute();
return $stmt->fetch()[0] === 1;
}
/**
* Returns `true` if and only if a user exists with the given email address.
*
* @param string $email the email address to check existence of
* @return bool `true` if and only if a user exists with the given email address
*/
public function has_user_with_email(string $email): bool
{
$stmt = $this->conn->prepare("SELECT EXISTS(SELECT 1 FROM users WHERE email=:email);");
$stmt->bindValue(":email", $email);
$stmt->execute();
return $stmt->fetch()[0] === 1;
}
/**
* Removes the user with the given UUID.
*
* @param string $uuid the UUID of the user to remove
* @return void
*/
public function remove_user_by_uuid(string $uuid): void
{
$stmt = $this->conn->prepare("DELETE FROM users WHERE uuid=:uuid;");
$stmt->bindValue(":uuid", $uuid);
$stmt->execute();
}
/**
* Returns all data of the user with the given UUID.
*
* @param string $uuid the UUID of the user to return the data of
* @return array<string, mixed>|null all data of the user with the given UUID, or `null` if the user could not be
* found
*/
public function get_user_by_uuid(string $uuid): ?array
{
$stmt = $this->conn->prepare("SELECT * FROM users WHERE uuid=:uuid;");
$stmt->bindValue(":uuid", $uuid);
$stmt->execute();
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
return empty($results) ? null : $results[0];
}
/**
* Returns all data of the user with the given email address.
*
* @param string $email the email address of the user to return the data of
* @return array<string, mixed>|null all data of the user with the given email address
*/
public function get_user_by_email(string $email): ?array
{
$stmt = $this->conn->prepare("SELECT * FROM users WHERE email=:email;");
$stmt->bindValue(":email", $email);
$stmt->execute();
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
return empty($results) ? null : $results[0];
}
/**
* Updates the email address of the user with the given UUID, and resets the email verification token.
*
* Settings the email address to the current value resets the email verification token.
*
* @param string $uuid the UUID of the user whose email address should be updated
* @param string $email the new email address
* @return string the verification token for the updated email address
*/
public function set_email(string $uuid, string $email): string
{
$stmt = $this->conn->prepare("UPDATE users
SET email=:email,
email_verification_token=lower(hex(randomblob(16))),
email_verification_token_timestamp=unixepoch()
WHERE uuid=:uuid
RETURNING email_verification_token;");
$stmt->bindValue(":uuid", $uuid);
$stmt->bindValue(":email", $email);
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC)[0]["email_verification_token"];
}
/**
* Sets the user's email address as verified.
*
* @param string $uuid the UUID of the user whose email address has been verified
* @return void
*/
public function set_email_verified(string $uuid): void
{
$stmt = $this->conn->prepare("UPDATE users SET email_verification_token=null WHERE uuid=:uuid;");
$stmt->bindValue(":uuid", $uuid);
$stmt->execute();
}
/**
* Sets whether email notifications are enabled for the given user.
*
* @param string $uuid the UUID of the user whose notifications should be enabled or disabled
* @param bool $enabled `true` if notifications should be enabled, `false` if notifications should be disabled
* @return void
*/
public function set_notifications_enabled(string $uuid, bool $enabled): void
{
$stmt = $this->conn->prepare("UPDATE users SET email_notifications_enabled=:enabled WHERE uuid=:uuid;");
$stmt->bindValue(":enabled", $enabled);
$stmt->bindValue(":uuid", $uuid);
$stmt->execute();
}
/**
* Sets the indicated user's password.
*
* @param string $uuid the UUID of the user whose password should be changed
* @param string $password the new password
* @return void
*/
public function set_password(string $uuid, string $password): void
{
$stmt = $this->conn->prepare("UPDATE users
SET password=:password, password_last_change=unixepoch(),
password_reset_token=null
WHERE uuid=:uuid;");
$stmt->bindValue(":uuid", $uuid);
$stmt->bindValue(":password", password_hash($password, PASSWORD_BCRYPT));
$stmt->execute();
}
/**
* Generates and registers a password reset token for the user with the given email address.
*
* @param string $email the address for which a password reset token should be generated
* @return string the generate password reset token
*/
public function register_password_reset(string $email): string
{
$stmt = $this->conn->prepare("UPDATE users
SET password_reset_token=lower(hex(randomblob(16))),
password_reset_token_timestamp=unixepoch()
WHERE email=:email
RETURNING password_reset_token;");
$stmt->bindValue(":email", $email);
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC)[0]["password_reset_token"];
}
}