295 lines
12 KiB
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"];
|
|
}
|
|
}
|