death-notifier/src/main/php/UserManager.php

318 lines
12 KiB
PHP
Raw Normal View History

2022-08-12 20:46:43 +02:00
<?php
2022-08-14 16:49:52 +02:00
namespace php;
2022-08-12 20:46:43 +02:00
use Exception;
2022-08-14 15:52:47 +02:00
use Monolog\Logger;
2022-08-14 18:31:55 +02:00
use PDO;
2022-08-12 20:46:43 +02:00
/**
* Manages interaction with the database in the context of users.
*/
2022-08-14 15:52:47 +02:00
// TODO: Remove duplication in this class
2022-08-12 20:46:43 +02:00
class UserManager
{
2022-08-14 17:54:55 +02:00
/**
* The maximum length of an email address.
*/
private const MAX_EMAIL_LENGTH = 254;
/**
* The minimum length of a password;
*/
private const MIN_PASSWORD_LENGTH = 8;
/**
* The maximum length of a password.
*/
private const MAX_PASSWORD_LENGTH = 255;
2022-08-14 15:52:47 +02:00
/**
* @var Logger The logger to use for logging.
*/
private Logger $logger;
2022-08-12 20:46:43 +02:00
/**
* @var string The filename of the database to interact with.
*/
private string $db_filename;
/**
* Constructs a new user manager.
*
2022-08-14 15:52:47 +02:00
* @param Logger $logger the logger to use for logging
2022-08-12 20:46:43 +02:00
* @param string $db_filename the filename of the database to interact with
*/
2022-08-14 15:52:47 +02:00
public function __construct(Logger $logger, string $db_filename)
2022-08-12 20:46:43 +02:00
{
2022-08-14 15:52:47 +02:00
$this->logger = $logger;
2022-08-12 20:46:43 +02:00
$this->db_filename = $db_filename;
}
/**
* Populates the database with the necessary structures for users.
*
* @return void
*/
public function install(): void
{
2022-08-14 18:31:55 +02:00
$conn = Database::connect($this->db_filename);
$conn->exec("CREATE TABLE users(uuid text primary key not null, email text not null, password text not null);");
}
2022-08-12 20:46:43 +02:00
/**
* 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
{
2022-08-14 18:31:55 +02:00
// Generate UUID
try {
$uuid = bin2hex(random_bytes(16));
} catch (Exception $exception) {
$this->logger->emergency("Failed to generate UUID.", [$exception]);
return new Response(payload: ["target" => null, "message" => "Failed to generate UUID."], satisfied: false);
2022-08-14 18:31:55 +02:00
}
2022-08-12 20:46:43 +02:00
// Validate
2022-08-14 17:54:55 +02:00
if (!filter_var($email, FILTER_VALIDATE_EMAIL) || strlen($email) > self::MAX_EMAIL_LENGTH)
return new Response(
payload: ["target" => "email", "message" => "Invalid email address."],
satisfied: false
);
2022-08-14 17:54:55 +02:00
if (strlen($password) < self::MIN_PASSWORD_LENGTH || strlen($password) > self::MAX_PASSWORD_LENGTH)
return new Response(
payload: [
"target" => "password",
"message" => "Your password should be at least 8 and at most 255 characters long."
],
satisfied: false
);
2022-08-14 14:14:25 +02:00
if ($password !== $password_confirm)
return new Response(
payload: ["target" => "passwordConfirm", "message" => "Passwords do not match."],
satisfied: false
);
2022-08-12 20:46:43 +02:00
2022-08-14 18:31:55 +02:00
// Connect
$conn = Database::connect($this->db_filename);
$conn->beginTransaction();
2022-08-12 20:46:43 +02:00
// Check if email address is already in use
2022-08-14 18:31:55 +02:00
$stmt = $conn->prepare("SELECT COUNT(*) as count FROM users WHERE email=:email;");
2022-08-12 20:46:43 +02:00
$stmt->bindValue(":email", $email);
2022-08-14 18:31:55 +02:00
$stmt->execute();
$result = $stmt->fetch(PDO::FETCH_ASSOC);
if ($result["count"] > 0) {
$conn->rollBack();
return new Response(
payload: ["target" => "email", "message" => "Email address already in use."],
satisfied: false
);
2022-08-12 20:46:43 +02:00
}
// Register user
2022-08-14 18:31:55 +02:00
$stmt = $conn->prepare("INSERT INTO users (uuid, email, password) VALUES (:uuid, :email, :password);");
2022-08-12 20:46:43 +02:00
$stmt->bindValue(":uuid", $uuid);
$stmt->bindValue(":email", $email);
$stmt->bindValue(":password", password_hash($password, PASSWORD_DEFAULT));
2022-08-14 18:31:55 +02:00
$stmt->execute();
2022-08-12 20:46:43 +02:00
// Respond
2022-08-14 18:31:55 +02:00
$conn->commit();
return new Response(payload: null, satisfied: true);
2022-08-12 20:46:43 +02:00
}
/**
* 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
{
2022-08-14 18:31:55 +02:00
$conn = Database::connect($this->db_filename);
$stmt = $conn->prepare("DELETE FROM users WHERE uuid=:uuid;");
2022-08-12 20:46:43 +02:00
$stmt->bindValue(":uuid", $uuid);
2022-08-14 18:31:55 +02:00
$stmt->execute();
2022-08-12 20:46:43 +02:00
return new Response(payload: null, satisfied: true);
2022-08-12 20:46:43 +02:00
}
/**
* 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
2022-08-12 20:46:43 +02:00
*/
public function check_login(string $email, string $password): array
{
2022-08-19 17:23:24 +02:00
if (!filter_var($email, FILTER_VALIDATE_EMAIL) || strlen($email) > self::MAX_EMAIL_LENGTH)
return [
new Response(
payload: ["target" => "email", "message" => "Invalid email address."],
satisfied: false
),
null
];
if (strlen($password) > self::MAX_PASSWORD_LENGTH)
return [
new Response(
payload: ["target" => "password", "message" => "Incorrect combination of email and password."],
satisfied: false
),
null
];
2022-08-14 17:54:55 +02:00
2022-08-14 18:31:55 +02:00
$conn = Database::connect($this->db_filename);
$stmt = $conn->prepare("SELECT uuid, password FROM users WHERE email=:email;");
$stmt->bindValue(":email", $email);
2022-08-14 18:31:55 +02:00
$stmt->execute();
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
2022-08-12 20:46:43 +02:00
2022-08-14 18:31:55 +02:00
return (sizeof($results) === 0 || !password_verify($password, $results[0]["password"]))
? [
new Response(
payload: ["target" => "password", "message" => "Incorrect combination of email and password."],
satisfied: false
),
null
]
: [new Response(payload: null, satisfied: true), $results[0]["uuid"]];
2022-08-12 20:46:43 +02:00
}
/**
* 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
2022-08-12 20:46:43 +02:00
*/
public function get_user_data(string $uuid): Response
2022-08-12 20:46:43 +02:00
{
2022-08-14 18:31:55 +02:00
$conn = Database::connect($this->db_filename);
$stmt = $conn->prepare("SELECT * FROM users WHERE uuid=:uuid;");
2022-08-12 20:46:43 +02:00
$stmt->bindValue(":uuid", $uuid);
2022-08-14 18:31:55 +02:00
$stmt->execute();
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if ($user === false)
return new Response(payload: ["target" => "uuid", "message" => "Invalid user."], satisfied: false);
2022-08-12 20:46:43 +02:00
return new Response(payload: $user, satisfied: true);
2022-08-12 20:46:43 +02:00
}
/**
* 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
2022-08-14 17:54:55 +02:00
if (!filter_var($email, FILTER_VALIDATE_EMAIL) || strlen($email) > self::MAX_EMAIL_LENGTH)
return new Response(
payload: ["target" => "email", "message" => "Invalid email address."],
satisfied: false
);
2022-08-12 20:46:43 +02:00
2022-08-14 18:31:55 +02:00
// Connect
$conn = Database::connect($this->db_filename);
$conn->beginTransaction();
2022-08-12 20:46:43 +02:00
// Check if email address is already in use
2022-08-14 18:31:55 +02:00
$stmt = $conn->prepare("SELECT COUNT(*) as count FROM users WHERE email=:email;");
2022-08-12 20:46:43 +02:00
$stmt->bindValue(":email", $email);
2022-08-14 18:31:55 +02:00
$stmt->execute();
if ($stmt->fetch(PDO::FETCH_ASSOC)["count"] > 0) {
$conn->rollBack();
return new Response(
payload: ["target" => "email", "message" => "Email address already in use."],
satisfied: false
);
2022-08-12 20:46:43 +02:00
}
// Update email address
2022-08-14 18:31:55 +02:00
$stmt = $conn->prepare("UPDATE users SET email=:email WHERE uuid=:uuid;");
2022-08-12 20:46:43 +02:00
$stmt->bindValue(":uuid", $uuid);
$stmt->bindValue(":email", $email);
2022-08-14 18:31:55 +02:00
$stmt->execute();
2022-08-12 20:46:43 +02:00
// Respond
2022-08-14 18:31:55 +02:00
$conn->commit();
return new Response(payload: null, satisfied: true);
2022-08-12 20:46:43 +02:00
}
/**
* 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
2022-08-12 20:46:43 +02:00
* @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
2022-08-12 20:46:43 +02:00
{
// Validate
if (strlen($password_old) > self::MAX_PASSWORD_LENGTH)
return new Response(
payload: ["target" => "passwordOld", "message" => "Incorrect old password."],
satisfied: false
);
2022-08-14 17:54:55 +02:00
if (strlen($password_new) < self::MIN_PASSWORD_LENGTH || strlen($password_new) > self::MAX_PASSWORD_LENGTH)
return new Response(
payload: [
"target" => "passwordNew",
"message" => "Your password should be at least 8 and at most 255 characters long."
],
satisfied: false
);
2022-08-14 14:14:25 +02:00
if ($password_new !== $password_confirm)
return new Response(
payload: ["target" => "passwordConfirm", "message" => "New passwords do not match."],
satisfied: false
);
2022-08-12 20:46:43 +02:00
2022-08-14 18:31:55 +02:00
// Connect
$conn = Database::connect($this->db_filename);
$conn->beginTransaction();
2022-08-12 20:46:43 +02:00
// Validate old password
2022-08-14 18:31:55 +02:00
$stmt = $conn->prepare("SELECT password FROM users WHERE uuid=:uuid;");
$stmt->bindValue(":uuid", $uuid);
2022-08-14 18:31:55 +02:00
$stmt->execute();
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if (!password_verify($password_old, $user["password"])) {
2022-08-14 18:31:55 +02:00
$conn->rollBack();
return new Response(
payload: ["target" => "passwordOld", "message" => "Incorrect old password."],
satisfied: false
);
}
2022-08-12 20:46:43 +02:00
// Update password
2022-08-14 18:31:55 +02:00
$stmt = $conn->prepare("UPDATE users SET password=:password WHERE uuid=:uuid;");
2022-08-12 20:46:43 +02:00
$stmt->bindValue(":uuid", $uuid);
$stmt->bindValue(":password", password_hash($password_new, PASSWORD_DEFAULT));
2022-08-14 18:31:55 +02:00
$stmt->execute();
2022-08-12 20:46:43 +02:00
2022-08-14 18:31:55 +02:00
// Respond
$conn->commit();
return new Response(payload: null, satisfied: true);
2022-08-12 20:46:43 +02:00
}
}