logger = $logger; $this->db_filename = $db_filename; } /** * Populates the database with the necessary structures for users. * * @return void */ public function install(): void { $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);"); } /** * 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 { // Generate UUID try { $uuid = bin2hex(random_bytes(16)); } catch (Exception $exception) { $this->logger->emergency("Failed to generate UUID.", [$exception]); return new Response("Failed to generate UUID.", false); } // Validate if (!filter_var($email, FILTER_VALIDATE_EMAIL) || strlen($email) > self::MAX_EMAIL_LENGTH) return new Response("Invalid email address.", false); if (strlen($password) < self::MIN_PASSWORD_LENGTH || strlen($password) > self::MAX_PASSWORD_LENGTH) return new Response("Your password should be at least 8 and at most 255 characters long.", false); if ($password !== $password_confirm) return new Response("Passwords do not match.", false); // Connect $conn = Database::connect($this->db_filename); $conn->beginTransaction(); // Check if email address is already in use $stmt = $conn->prepare("SELECT COUNT(*) as count FROM users WHERE email=:email;"); $stmt->bindValue(":email", $email); $stmt->execute(); $result = $stmt->fetch(PDO::FETCH_ASSOC); if ($result["count"] > 0) { $conn->rollBack(); return new Response("Email address already in use.", false); } // Register user $stmt = $conn->prepare("INSERT INTO users (uuid, email, password) VALUES (:uuid, :email, :password);"); $stmt->bindValue(":uuid", $uuid); $stmt->bindValue(":email", $email); $stmt->bindValue(":password", password_hash($password, PASSWORD_DEFAULT)); $stmt->execute(); // Respond $conn->commit(); 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 { $conn = Database::connect($this->db_filename); $stmt = $conn->prepare("DELETE FROM users WHERE uuid=:uuid;"); $stmt->bindValue(":uuid", $uuid); $stmt->execute(); return 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 { if (strlen($email) > self::MAX_EMAIL_LENGTH || strlen($password) > self::MAX_PASSWORD_LENGTH) return [new Response("Incorrect combination of email and password.", false), null]; $conn = Database::connect($this->db_filename); $stmt = $conn->prepare("SELECT uuid, password FROM users WHERE email=:email;"); $stmt->bindValue(":email", $email); $stmt->execute(); $results = $stmt->fetchAll(PDO::FETCH_ASSOC); return (sizeof($results) === 0 || !password_verify($password, $results[0]["password"])) ? [new Response("Incorrect combination of email and password.", false), null] : [new Response(null, true), $results[0]["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 { $conn = Database::connect($this->db_filename); $stmt = $conn->prepare("SELECT * FROM users WHERE uuid=:uuid;"); $stmt->bindValue(":uuid", $uuid); $stmt->execute(); $user = $stmt->fetch(PDO::FETCH_ASSOC); if ($user === false) return new Response("Invalid user.", false); return 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($email, FILTER_VALIDATE_EMAIL) || strlen($email) > self::MAX_EMAIL_LENGTH) return new Response("Invalid email address.", false); // Connect $conn = Database::connect($this->db_filename); $conn->beginTransaction(); // Check if email address is already in use $stmt = $conn->prepare("SELECT COUNT(*) as count FROM users WHERE email=:email;"); $stmt->bindValue(":email", $email); $stmt->execute(); if ($stmt->fetch(PDO::FETCH_ASSOC)["count"] > 0) { $conn->rollBack(); return new Response("Email address already in use.", false); } // Update email address $stmt = $conn->prepare("UPDATE users SET email=:email WHERE uuid=:uuid;"); $stmt->bindValue(":uuid", $uuid); $stmt->bindValue(":email", $email); $stmt->execute(); // Respond $conn->commit(); 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) < self::MIN_PASSWORD_LENGTH || strlen($password_new) > self::MAX_PASSWORD_LENGTH) return new Response("Your password should be at least 8 and at most 255 characters long.", false); if ($password_new !== $password_confirm) return new Response("New passwords do not match.", false); // Connect $conn = Database::connect($this->db_filename); $conn->beginTransaction(); // Validate old password $stmt = $conn->prepare("SELECT password FROM users WHERE uuid=:uuid;"); $stmt->bindValue(":uuid", $uuid); $stmt->execute(); $user = $stmt->fetch(PDO::FETCH_ASSOC); if (!password_verify($password_old, $user["password"])) { $conn->rollBack(); return new Response("Incorrect old password.", false); } // Update password $stmt = $conn->prepare("UPDATE users SET password=:password WHERE uuid=:uuid;"); $stmt->bindValue(":uuid", $uuid); $stmt->bindValue(":password", password_hash($password_new, PASSWORD_DEFAULT)); $stmt->execute(); // Respond $conn->commit(); return new Response(null, true); } }