From bf6b2a0ffa610ffdd3c5bac29a5eff95f15f4427 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 27 Apr 2024 13:54:09 -0400 Subject: [PATCH] Add single-user handling (#9) - Disallow log on for single-user mode user - Improve CLI header display --- src/cli-start.php | 6 ++- src/lib/Security.php | 19 +++++++--- src/util/user.php | 89 ++++++++++++++++++++++++++------------------ 3 files changed, 70 insertions(+), 44 deletions(-) diff --git a/src/cli-start.php b/src/cli-start.php index 78d6184..b6dda7e 100644 --- a/src/cli-start.php +++ b/src/cli-start.php @@ -22,5 +22,9 @@ function printfn(string $format, mixed ...$values): void { * @param string $title The title to display on the command line */ function cli_title(string $title): void { - printfn("$title | Feed Reader Central v%s" . PHP_EOL, FRC_VERSION); + $appTitle = 'Feed Reader Central ~ v' . FRC_VERSION; + $dashes = ' +' . str_repeat('-', strlen($title) + 2) . '+' . str_repeat('-', strlen($appTitle) + 2) . '+'; + printfn($dashes); + printfn(' | %s | %s |', $title, $appTitle); + printfn($dashes . PHP_EOL); } diff --git a/src/lib/Security.php b/src/lib/Security.php index f7e1af3..7235a03 100644 --- a/src/lib/Security.php +++ b/src/lib/Security.php @@ -20,6 +20,9 @@ class Security { /** @var string The password for the single user with no password */ private const string SINGLE_USER_PASSWORD = 'no-password-required'; + /** @var string The password algorithm to use for our passwords */ + public const string PW_ALGORITHM = PASSWORD_DEFAULT; + /** * Find a user by their ID * @@ -27,7 +30,7 @@ class Security { * @param SQLite3 $db The data connection to use to retrieve the user * @return array|false The user information, or null if the user is not found */ - private static function findUserByEmail(string $email, SQLite3 $db): array|false { + public static function findUserByEmail(string $email, SQLite3 $db): array|false { $query = $db->prepare('SELECT * FROM frc_user WHERE email = :email'); $query->bindValue(':email', $email); $result = $query->execute(); @@ -44,7 +47,7 @@ class Security { public static function addUser(string $email, string $password, SQLite3 $db): void { $query = $db->prepare('INSERT INTO frc_user (email, password) VALUES (:email, :password)'); $query->bindValue(':email', $email); - $query->bindValue(':password', password_hash($password, PASSWORD_DEFAULT)); + $query->bindValue(':password', password_hash($password, self::PW_ALGORITHM)); $query->execute(); } @@ -58,9 +61,9 @@ class Security { */ private static function verifyPassword(array $user, string $password, ?string $returnTo, SQLite3 $db): void { if (password_verify($password, $user['password'])) { - if (password_needs_rehash($user['password'], PASSWORD_DEFAULT)) { + if (password_needs_rehash($user['password'], self::PW_ALGORITHM)) { $rehash = $db->prepare('UPDATE frc_user SET password = :hash WHERE id = :id'); - $rehash->bindValue(':hash', password_hash($password, PASSWORD_DEFAULT)); + $rehash->bindValue(':hash', password_hash($password, self::PW_ALGORITHM)); $rehash->bindValue(':id', $user['id']); $rehash->execute(); } @@ -73,12 +76,16 @@ class Security { /** * Log on a user with e-mail address and password * - * @param string $email The e-mail address for the user + * @param string $email The e-mail address for the user (cannot be the single-user mode user) * @param string $password The password provided by the user * @param string|null $returnTo The URL to which the user should be redirected * @param SQLite3 $db The database connection to use to verify the user's credentials */ public static function logOnUser(string $email, string $password, ?string $returnTo, SQLite3 $db): void { + if ($email == self::SINGLE_USER_EMAIL) { + add_error('Invalid credentials; log on unsuccessful'); + return; + } $user = self::findUserByEmail($email, $db); if ($user) self::verifyPassword($user, $password, $returnTo, $db); add_error('Invalid credentials; log on unsuccessful'); @@ -93,7 +100,7 @@ class Security { */ public static function updatePassword(string $email, string $password, SQLite3 $db): void { $query = $db->prepare('UPDATE frc_user SET password = :password WHERE email = :email'); - $query->bindValue(':password', password_hash($password, PASSWORD_DEFAULT)); + $query->bindValue(':password', password_hash($password, self::PW_ALGORITHM)); $query->bindValue(':email', $email); $query->execute(); } diff --git a/src/util/user.php b/src/util/user.php index 81b8a34..3244963 100644 --- a/src/util/user.php +++ b/src/util/user.php @@ -47,13 +47,17 @@ switch ($argv[1]) { printfn('Missing parameters: delete-user requires e-mail address'); exit(-1); } - delete_user(); + delete_user($argv[2]); break; case 'migrate-single-user': - printfn('TODO: single-user migration'); + if ($argc < 4) { + printfn('Missing parameters: migrate-single-user requires e-mail and password'); + exit(-1); + } + migrate_single_user(); break; case 'remove-single-user': - printfn('TODO: single-user removal'); + delete_user(Security::SINGLE_USER_EMAIL); break; default: printfn('Unrecognized option "%s"', $argv[1]); @@ -70,14 +74,8 @@ function add_user(): void { try { // Ensure there is not already a user with this e-mail address - $existsQuery = $db->prepare('SELECT COUNT(*) FROM frc_user WHERE email = :email'); - $existsQuery->bindValue(':email', $argv[2]); - $existsResult = $existsQuery->execute(); - if (!$existsResult) { - printfn('SQLite error: %s', $db->lastErrorMsg()); - return; - } - if ($existsResult->fetchArray(SQLITE3_NUM)[0] != 0) { + $user = Security::findUserByEmail($argv[2], $db); + if ($user) { printfn('A user with e-mail address "%s" already exists', $argv[2]); return; } @@ -100,14 +98,8 @@ function set_password(): void { try { // Ensure this user exists - $existsQuery = $db->prepare('SELECT COUNT(*) FROM frc_user WHERE email = :email'); - $existsQuery->bindValue(':email', $argv[2]); - $existsResult = $existsQuery->execute(); - if (!$existsResult) { - printfn('SQLite error: %s', $db->lastErrorMsg()); - return; - } - if ($existsResult->fetchArray(SQLITE3_NUM)[0] == 0) { + $user = Security::findUserByEmail($argv[2], $db); + if (!$user) { printfn('No user exists with e-mail address "%s"', $argv[2]); return; } @@ -122,29 +114,25 @@ function set_password(): void { /** * Delete a user + * + * @param string $email The e-mail address of the user to be deleted */ -function delete_user(): void { - global $argv; +function delete_user(string $email): void { $db = Data::getConnection(); try { + $displayUser = $email == Security::SINGLE_USER_EMAIL ? 'single-user mode user' : "user \"$email\""; + // Get the ID for the provided e-mail address - $idQuery = $db->prepare('SELECT id FROM frc_user WHERE email = :email'); - $idQuery->bindValue(':email', $argv[2]); - $idResult = $idQuery->execute(); - if (!$idResult) { - printfn('SQLite error: %s', $db->lastErrorMsg()); - return; - } - $id = $idResult->fetchArray(SQLITE3_NUM); - if (!$id) { - printfn('No user exists with e-mail address "%s"', $argv[2]); + $user = Security::findUserByEmail($email, $db); + if (!$user) { + printfn('No %s exists', $displayUser); return; } $feedCountQuery = $db->prepare('SELECT COUNT(*) FROM feed WHERE user_id = :user'); - $feedCountQuery->bindValue(':user', $id[0]); + $feedCountQuery->bindValue(':user', $user['id']); $feedCountResult = $feedCountQuery->execute(); if (!$feedCountResult) { printfn('SQLite error: %s', $db->lastErrorMsg()); @@ -152,25 +140,52 @@ function delete_user(): void { } $feedCount = $feedCountResult->fetchArray(SQLITE3_NUM); - $proceed = readline("Delete user \"$argv[2]\" and their $feedCount[0] feed(s)? (y/N)" . PHP_EOL); + $proceed = readline("Delete the $displayUser and their $feedCount[0] feed(s)? (y/N)" . PHP_EOL); if (!$proceed || !str_starts_with(strtolower($proceed), 'y')) { printfn('Deletion canceled'); return; } $itemDelete = $db->prepare('DELETE FROM item WHERE feed_id IN (SELECT id FROM feed WHERE user_id = :user)'); - $itemDelete->bindValue(':user', $id[0]); + $itemDelete->bindValue(':user', $user['id']); $itemDelete->execute(); $feedDelete = $db->prepare('DELETE FROM feed WHERE user_id = :user'); - $feedDelete->bindValue(':user', $id[0]); + $feedDelete->bindValue(':user', $user['id']); $feedDelete->execute(); $userDelete = $db->prepare('DELETE FROM frc_user WHERE id = :user'); - $userDelete->bindValue(':user', $id[0]); + $userDelete->bindValue(':user', $user['id']); $userDelete->execute(); - printfn('User "%s" deleted successfully', $argv[2]); + printfn(strtoupper(substr($displayUser, 0, 1)) . substr($displayUser, 1) . ' deleted successfully'); + } finally { + $db->close(); + } +} + +/** + * Change the single-user mode user to a different e-mail address and password + */ +function migrate_single_user(): void { + global $argv; + + $db = Data::getConnection(); + + try { + $single = Security::findUserByEmail(Security::SINGLE_USER_EMAIL, $db); + if (!$single) { + printfn('There is no single-user mode user to be migrated'); + return; + } + + $migrateQuery = $db->prepare('UPDATE frc_user SET email = :email, password = :password WHERE id = :id'); + $migrateQuery->bindValue(':email', $argv[2]); + $migrateQuery->bindValue(':password', password_hash($argv[3], Security::PW_ALGORITHM)); + $migrateQuery->bindValue(':id', $single['id']); + $migrateQuery->execute(); + + printfn('The single user has been moved to "%s", with password "%s"', $argv[2], $argv[3]); } finally { $db->close(); }