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

295 lines
12 KiB
PHP

<?php
namespace com\fwdekker\deathnotifier\user;
use com\fwdekker\deathnotifier\Database;
use PDO;
/**
* A list of trackings, stored in a {@see Database}.
*
* @phpstan-type User array{"uuid": string, "email": string, "email_verification_token": string|null,
* "email_verification_token_timestamp": int, "email_notifications_enabled": int, "password": string,
* "password_last_change": int, "password_reset_token": string|null, "password_reset_token_timestamp": int}
*/
class UserList
{
/**
* 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 Database the database to store users in
*/
private readonly Database $database;
/**
* Constructs a new `UserList`.
*
* @param Database $database the database to store users in
*/
public function __construct(Database $database)
{
$this->database = $database;
}
/**
* Populates the database with the necessary structures for users.
*
* @return void
*/
public function install(): void
{
$conn = $this->database->conn;
$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()));");
}
/**
* Executes {@see $lambda} within a single database transaction.
*
* @param callable(): void $lambda the function to execute within a transaction
* @return void
* @see Database::transaction()
*/
public function transaction(callable $lambda): void
{
$this->database->transaction($lambda);
}
/**
* Registers a new user.
*
* Throws an error if a user with the given email address is already registered.
*
* @param string $email the email address to register the user under
* @param string $password the password to apply to the user
* @return string the email verification token that is generated for the newly-registered user
*/
public function add_user(string $email, string $password): string
{
$stmt = $this->database->conn->prepare("INSERT INTO users (email, password)
VALUES (:email, :password)
RETURNING email_verification_token;");
$stmt->bindValue(":email", $email);
$stmt->bindValue(":password", password_hash($password, PASSWORD_ARGON2ID));
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC)[0]["email_verification_token"];
}
/**
* 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->database->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->database->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->database->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 User|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->database->conn->prepare("SELECT uuid,
email,
email_verification_token,
email_verification_token_timestamp,
email_notifications_enabled,
password,
password_last_change,
password_reset_token,
password_reset_token_timestamp
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 User|null all data of the user with the given email address, or `null` if the user could not be found
*/
public function get_user_by_email(string $email): ?array
{
$stmt = $this->database->conn->prepare("SELECT uuid,
email,
email_verification_token,
email_verification_token_timestamp,
email_notifications_enabled,
password,
password_last_change,
password_reset_token,
password_reset_token_timestamp
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.
*
* Setting the email address to the current value only 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->database->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->database->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->database->conn->prepare("UPDATE users
SET email_notifications_enabled=:enabled
WHERE uuid=:uuid;");
$stmt->bindValue(":enabled", $enabled);
$stmt->bindValue(":uuid", $uuid);
$stmt->execute();
}
/**
* Sets the given 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->database->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_ARGON2ID));
$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->database->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"];
}
}