366 lines
14 KiB
PHP
366 lines
14 KiB
PHP
<?php
|
|
|
|
namespace php;
|
|
|
|
use Exception;
|
|
use Monolog\Logger;
|
|
use SQLite3;
|
|
|
|
|
|
/**
|
|
* Manages interaction with the database in the context of users.
|
|
*/
|
|
// TODO: Remove duplication in this class
|
|
class UserManager
|
|
{
|
|
/**
|
|
* @var Logger The logger to use for logging.
|
|
*/
|
|
private Logger $logger;
|
|
/**
|
|
* @var string The filename of the database to interact with.
|
|
*/
|
|
private string $db_filename;
|
|
|
|
|
|
/**
|
|
* Constructs a new user manager.
|
|
*
|
|
* @param Logger $logger the logger to use for logging
|
|
* @param string $db_filename the filename of the database to interact with
|
|
*/
|
|
public function __construct(Logger $logger, string $db_filename)
|
|
{
|
|
$this->logger = $logger;
|
|
$this->db_filename = $db_filename;
|
|
}
|
|
|
|
|
|
/**
|
|
* Populates the database with the necessary structures for users.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function install(): void
|
|
{
|
|
$db = new SQLite3($this->db_filename, SQLITE3_OPEN_READWRITE);
|
|
$db->exec("CREATE TABLE users(uuid text primary key not null, email text not null, password text not null);");
|
|
$db->close();
|
|
}
|
|
|
|
/**
|
|
* Registers a new user.
|
|
*
|
|
* @param string $email The user-submitted email address.
|
|
* @param string $password The user-submitted password.
|
|
* @param string $password_confirm The user-submitted password confirmation.
|
|
* @return Response a response with message `null` if the user was registered, or a response with a message
|
|
* explaining what went wrong otherwise
|
|
*/
|
|
public function register(string $email, string $password, string $password_confirm): Response
|
|
{
|
|
// TODO: Limit email address length, and also other lengths!
|
|
// Validate
|
|
if (!filter_var($_POST["email"], FILTER_VALIDATE_EMAIL))
|
|
return new Response("Invalid email address.", false);
|
|
if (strlen($password) < 8)
|
|
return new Response("Your password should be at least 8 characters long.", false);
|
|
if ($password !== $password_confirm)
|
|
return new Response("Passwords do not match.", false);
|
|
|
|
// Open DB
|
|
$db = new SQLite3($this->db_filename, SQLITE3_OPEN_READWRITE);
|
|
$db->exec("BEGIN;");
|
|
|
|
// Check if email address is already in use
|
|
$stmt = $db->prepare("SELECT 1 FROM users WHERE email=:email;");
|
|
if ($stmt === false) {
|
|
$db->exec("COMMIT;");
|
|
$db->close();
|
|
$this->logger->error("Failed to prepare query in 'register'.");
|
|
return new Response("Unexpected database error.", false);
|
|
}
|
|
$stmt->bindValue(":email", $email);
|
|
$result = $stmt->execute();
|
|
if ($result === false) {
|
|
$db->exec("COMMIT;");
|
|
$db->close();
|
|
return new Response("Unexpected database error.", false);
|
|
}
|
|
$email_available = $result->fetchArray(SQLITE3_ASSOC) === false;
|
|
if (!$email_available) {
|
|
$db->exec("COMMIT;");
|
|
$db->close();
|
|
return new Response("Email address already in use.", false);
|
|
}
|
|
|
|
// Generate UUID
|
|
try {
|
|
$uuid = bin2hex(random_bytes(16));
|
|
} catch (Exception) {
|
|
$db->exec("COMMIT;");
|
|
$db->close();
|
|
return new Response("Failed to generate UUID.", false);
|
|
}
|
|
|
|
// Register user
|
|
$stmt = $db->prepare("INSERT INTO users (uuid, email, password) VALUES (:uuid, :email, :password);");
|
|
if ($stmt === false) {
|
|
$db->exec("COMMIT;");
|
|
$db->close();
|
|
$this->logger->error("Failed to prepare query in 'register'.");
|
|
return new Response("Unexpected database error.", false);
|
|
}
|
|
$stmt->bindValue(":uuid", $uuid);
|
|
$stmt->bindValue(":email", $email);
|
|
$stmt->bindValue(":password", password_hash($password, PASSWORD_DEFAULT));
|
|
$user_registered = $stmt->execute() !== false;
|
|
/** @noinspection PhpIfWithCommonPartsInspection Easier to read this way */
|
|
if (!$user_registered) {
|
|
$db->exec("COMMIT;");
|
|
$db->close();
|
|
return new Response("Unexpected database error.", false);
|
|
}
|
|
|
|
// Respond
|
|
$db->exec("COMMIT;");
|
|
$db->close();
|
|
|
|
return new Response(null, true);
|
|
}
|
|
|
|
/**
|
|
* Deletes the user with the given UUID.
|
|
*
|
|
* @param string $uuid the UUID of the user to delete
|
|
* @return Response a response with message `null` if the user was deleted, or a response with a message explaining
|
|
* what went wrong otherwise
|
|
*/
|
|
public function delete(string $uuid): Response
|
|
{
|
|
$db = new SQLite3($this->db_filename, SQLITE3_OPEN_READWRITE);
|
|
$stmt = $db->prepare("DELETE FROM users WHERE uuid=:uuid;");
|
|
if ($stmt === false) {
|
|
$db->close();
|
|
$this->logger->error("Failed to prepare query in 'delete'.");
|
|
return new Response("Unexpected database error.", false);
|
|
}
|
|
$stmt->bindValue(":uuid", $uuid);
|
|
$result = $stmt->execute();
|
|
$db->close();
|
|
|
|
return $result === false
|
|
? new Response("Failed to delete user.", false)
|
|
: new Response(null, true);
|
|
}
|
|
|
|
/**
|
|
* Validates a login attempt with the given email address and password.
|
|
*
|
|
* @param string $email the email address of the user whose password should be checked
|
|
* @param string $password the password to check against the specified user
|
|
* @return array{Response, ?string} the first element is a response with message `null` if the login was successful,
|
|
* or a response with a message explaining what went wrong otherwise; the second element is the UUID of the user
|
|
* that was logged in as, or `null` if the login should not be performed
|
|
*/
|
|
public function check_login(string $email, string $password): array
|
|
{
|
|
$db = new SQLite3($this->db_filename, SQLITE3_OPEN_READONLY);
|
|
$stmt = $db->prepare("SELECT uuid, password FROM users WHERE email=:email;");
|
|
if ($stmt === false) {
|
|
$db->close();
|
|
$this->logger->error("Failed to prepare query in 'check_login'.");
|
|
return [new Response("Unexpected database error.", false), null];
|
|
}
|
|
$stmt->bindValue(":email", $email);
|
|
$result = $stmt->execute();
|
|
if ($result === false) {
|
|
$db->close();
|
|
$this->logger->error("Failed to execute query in 'check_login'.");
|
|
return [new Response("Unexpected database error.", false), null];
|
|
}
|
|
$user = $result->fetchArray(SQLITE3_ASSOC);
|
|
$db->close();
|
|
|
|
return $user === false || !password_verify($password, $user["password"])
|
|
? [new Response("Incorrect combination of email and password.", false), null]
|
|
: [new Response(null, true), $user["uuid"]];
|
|
}
|
|
|
|
/**
|
|
* Returns the user with the given UUID.
|
|
*
|
|
* @param string $uuid the UUID of the user to return
|
|
* @return Response the user with the given UUID, or a response with an explanation what went wrong otherwise
|
|
*/
|
|
public function get_user_data(string $uuid): Response
|
|
{
|
|
$db = new SQLite3($this->db_filename, SQLITE3_OPEN_READONLY);
|
|
$stmt = $db->prepare("SELECT * FROM users WHERE uuid=:uuid;");
|
|
if ($stmt === false) {
|
|
$db->close();
|
|
$this->logger->error("Failed to prepare query in 'get_user_data'.");
|
|
return new Response("Unexpected database error.", false);
|
|
}
|
|
$stmt->bindValue(":uuid", $uuid);
|
|
$result = $stmt->execute();
|
|
if ($result === false) {
|
|
$db->close();
|
|
$this->logger->error("Failed to execute query in 'get_user_data'.");
|
|
return new Response("Unexpected database error.", false);
|
|
}
|
|
$user = $result->fetchArray(SQLITE3_ASSOC);
|
|
$db->close();
|
|
|
|
return $user === false
|
|
? new Response("User does not exist.", false)
|
|
: new Response($user, true);
|
|
}
|
|
|
|
/**
|
|
* Updates the indicated user's email address.
|
|
*
|
|
* @param string $uuid the UUID of the user whose email address should be updated
|
|
* @param string $email the new email address
|
|
* @return Response a response with message `null` if the email address was updated, or a response with a message
|
|
* explaining what went wrong otherwise
|
|
*/
|
|
public function set_email(string $uuid, string $email): Response
|
|
{
|
|
// Validate
|
|
if (!filter_var($_POST["email"], FILTER_VALIDATE_EMAIL))
|
|
return new Response("Invalid email address.", false);
|
|
|
|
// Open DB
|
|
$db = new SQLite3($this->db_filename, SQLITE3_OPEN_READWRITE);
|
|
$db->exec("BEGIN;");
|
|
|
|
// Check if email address is already in use
|
|
$stmt = $db->prepare("SELECT 1 FROM users WHERE email=:email;");
|
|
if ($stmt === false) {
|
|
$db->exec("COMMIT;");
|
|
$db->close();
|
|
$this->logger->error("Failed to prepare query in 'set_email'.");
|
|
return new Response("Unexpected database error.", false);
|
|
}
|
|
$stmt->bindValue(":email", $email);
|
|
$result = $stmt->execute();
|
|
if ($result === false) {
|
|
$db->exec("COMMIT;");
|
|
$db->close();
|
|
$this->logger->error("Failed to execute query in 'set_email'.");
|
|
return new Response("Email address already in use.", false);
|
|
}
|
|
$email_available = $result->fetchArray(SQLITE3_ASSOC) === false;
|
|
if (!$email_available) {
|
|
$db->exec("COMMIT;");
|
|
$db->close();
|
|
return new Response("Email address already in use.", false);
|
|
}
|
|
|
|
// Update email address
|
|
$stmt = $db->prepare("UPDATE users SET email=:email WHERE uuid=:uuid;");
|
|
if ($stmt === false) {
|
|
$db->exec("COMMIT;");
|
|
$db->close();
|
|
$this->logger->error("Failed to prepare query in 'set_email'.");
|
|
return new Response("Unexpected database error.", false);
|
|
}
|
|
$stmt->bindValue(":uuid", $uuid);
|
|
$stmt->bindValue(":email", $email);
|
|
$email_updated = $stmt->execute() !== false;
|
|
/** @noinspection PhpIfWithCommonPartsInspection Easier to read this way */
|
|
if (!$email_updated) {
|
|
$db->exec("COMMIT;");
|
|
$db->close();
|
|
$this->logger->error(
|
|
"Failed to update email address after validation.",
|
|
["uuid" => $uuid, "email" => $email]
|
|
);
|
|
return new Response("Unexpected database error.", false);
|
|
}
|
|
|
|
// Respond
|
|
$db->exec("COMMIT;");
|
|
$db->close();
|
|
|
|
return new Response(null, true);
|
|
}
|
|
|
|
/**
|
|
* Updates the indicated user's password.
|
|
*
|
|
* @param string $uuid the UUID of the user whose password should be updated
|
|
* @param string $password_old the old password
|
|
* @param string $password_new the new password
|
|
* @param string $password_confirm the confirmation of the new password
|
|
* @return Response a response with message `null` if the password was updated, or a response with a message
|
|
* explaining what went wrong otherwise
|
|
*/
|
|
public function set_password(string $uuid, string $password_old, string $password_new,
|
|
string $password_confirm): Response
|
|
{
|
|
// Validate
|
|
if (strlen($password_new) < 8)
|
|
return new Response("Your password should be at least 8 characters long.", false);
|
|
if ($password_new !== $password_confirm)
|
|
return new Response("New passwords do not match.", false);
|
|
|
|
// Open DB
|
|
$db = new SQLite3($this->db_filename, SQLITE3_OPEN_READWRITE);
|
|
$db->exec("BEGIN;");
|
|
|
|
// Validate old password
|
|
$stmt = $db->prepare("SELECT * FROM users WHERE uuid=:uuid;");
|
|
if ($stmt === false) {
|
|
$db->exec("COMMIT;");
|
|
$db->close();
|
|
$this->logger->error("Failed to prepare query in 'set_password'.");
|
|
return new Response("Unexpected database error.", false);
|
|
}
|
|
$stmt->bindValue(":uuid", $uuid);
|
|
$result = $stmt->execute();
|
|
if ($result === false) {
|
|
$db->exec("COMMIT;");
|
|
$db->close();
|
|
$this->logger->error("Failed to execute query in 'set_password'.");
|
|
return new Response("Invalid user.", false);
|
|
}
|
|
$user = $result->fetchArray(SQLITE3_ASSOC);
|
|
if ($user === false) {
|
|
$db->exec("COMMIT;");
|
|
$db->close();
|
|
return new Response("Invalid user.", false);
|
|
}
|
|
if (!password_verify($password_old, $user["password"])) {
|
|
$db->exec("COMMIT;");
|
|
$db->close();
|
|
return new Response("Incorrect old password.", false);
|
|
}
|
|
|
|
// Update password
|
|
$stmt = $db->prepare("UPDATE users SET password=:password WHERE uuid=:uuid;");
|
|
if ($stmt === false) {
|
|
$db->exec("COMMIT;");
|
|
$db->close();
|
|
$this->logger->error("Failed to prepare query in 'set_password'.");
|
|
return new Response("Unexpected database error.", false);
|
|
}
|
|
$stmt->bindValue(":uuid", $uuid);
|
|
$stmt->bindValue(":password", password_hash($password_new, PASSWORD_DEFAULT));
|
|
$password_updated = $stmt->execute() !== false;
|
|
/** @noinspection PhpIfWithCommonPartsInspection Easier to read this way */
|
|
if (!$password_updated) {
|
|
$db->exec("COMMIT;");
|
|
$db->close();
|
|
$this->logger->error("Failed to update password after validation.", ["uuid" => $uuid]);
|
|
return new Response("Unexpected database error.", false);
|
|
}
|
|
|
|
$db->exec("COMMIT;");
|
|
$db->close();
|
|
|
|
return new Response(null, true);
|
|
}
|
|
}
|