Add single-user handling (#9)
- Disallow log on for single-user mode user - Improve CLI header display
This commit is contained in:
		
							parent
							
								
									c1790b58fd
								
							
						
					
					
						commit
						bf6b2a0ffa
					
				@ -22,5 +22,9 @@ function printfn(string $format, mixed ...$values): void {
 | 
				
			|||||||
 * @param string $title The title to display on the command line
 | 
					 * @param string $title The title to display on the command line
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
function cli_title(string $title): void {
 | 
					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);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -20,6 +20,9 @@ class Security {
 | 
				
			|||||||
    /** @var string The password for the single user with no password */
 | 
					    /** @var string The password for the single user with no password */
 | 
				
			||||||
    private const string SINGLE_USER_PASSWORD = 'no-password-required';
 | 
					    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
 | 
					     * Find a user by their ID
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
@ -27,7 +30,7 @@ class Security {
 | 
				
			|||||||
     * @param SQLite3 $db The data connection to use to retrieve the user
 | 
					     * @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
 | 
					     * @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 = $db->prepare('SELECT * FROM frc_user WHERE email = :email');
 | 
				
			||||||
        $query->bindValue(':email', $email);
 | 
					        $query->bindValue(':email', $email);
 | 
				
			||||||
        $result = $query->execute();
 | 
					        $result = $query->execute();
 | 
				
			||||||
@ -44,7 +47,7 @@ class Security {
 | 
				
			|||||||
    public static function addUser(string $email, string $password, SQLite3 $db): void {
 | 
					    public static function addUser(string $email, string $password, SQLite3 $db): void {
 | 
				
			||||||
        $query = $db->prepare('INSERT INTO frc_user (email, password) VALUES (:email, :password)');
 | 
					        $query = $db->prepare('INSERT INTO frc_user (email, password) VALUES (:email, :password)');
 | 
				
			||||||
        $query->bindValue(':email',    $email);
 | 
					        $query->bindValue(':email',    $email);
 | 
				
			||||||
        $query->bindValue(':password', password_hash($password, PASSWORD_DEFAULT));
 | 
					        $query->bindValue(':password', password_hash($password, self::PW_ALGORITHM));
 | 
				
			||||||
        $query->execute();
 | 
					        $query->execute();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -58,9 +61,9 @@ class Security {
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    private static function verifyPassword(array $user, string $password, ?string $returnTo, SQLite3 $db): void {
 | 
					    private static function verifyPassword(array $user, string $password, ?string $returnTo, SQLite3 $db): void {
 | 
				
			||||||
        if (password_verify($password, $user['password'])) {
 | 
					        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 = $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->bindValue(':id',   $user['id']);
 | 
				
			||||||
                $rehash->execute();
 | 
					                $rehash->execute();
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@ -73,12 +76,16 @@ class Security {
 | 
				
			|||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Log on a user with e-mail address and password
 | 
					     * 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 $password The password provided by the user
 | 
				
			||||||
     * @param string|null $returnTo The URL to which the user should be redirected
 | 
					     * @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
 | 
					     * @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 {
 | 
					    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);
 | 
					        $user = self::findUserByEmail($email, $db);
 | 
				
			||||||
        if ($user) self::verifyPassword($user, $password, $returnTo, $db);
 | 
					        if ($user) self::verifyPassword($user, $password, $returnTo, $db);
 | 
				
			||||||
        add_error('Invalid credentials; log on unsuccessful');
 | 
					        add_error('Invalid credentials; log on unsuccessful');
 | 
				
			||||||
@ -93,7 +100,7 @@ class Security {
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    public static function updatePassword(string $email, string $password, SQLite3 $db): void {
 | 
					    public static function updatePassword(string $email, string $password, SQLite3 $db): void {
 | 
				
			||||||
        $query = $db->prepare('UPDATE frc_user SET password = :password WHERE email = :email');
 | 
					        $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->bindValue(':email',    $email);
 | 
				
			||||||
        $query->execute();
 | 
					        $query->execute();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -47,13 +47,17 @@ switch ($argv[1]) {
 | 
				
			|||||||
            printfn('Missing parameters: delete-user requires e-mail address');
 | 
					            printfn('Missing parameters: delete-user requires e-mail address');
 | 
				
			||||||
            exit(-1);
 | 
					            exit(-1);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        delete_user();
 | 
					        delete_user($argv[2]);
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
    case 'migrate-single-user':
 | 
					    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;
 | 
					        break;
 | 
				
			||||||
    case 'remove-single-user':
 | 
					    case 'remove-single-user':
 | 
				
			||||||
        printfn('TODO: single-user removal');
 | 
					        delete_user(Security::SINGLE_USER_EMAIL);
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
    default:
 | 
					    default:
 | 
				
			||||||
        printfn('Unrecognized option "%s"', $argv[1]);
 | 
					        printfn('Unrecognized option "%s"', $argv[1]);
 | 
				
			||||||
@ -70,14 +74,8 @@ function add_user(): void {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
        // Ensure there is not already a user with this e-mail address
 | 
					        // Ensure there is not already a user with this e-mail address
 | 
				
			||||||
        $existsQuery = $db->prepare('SELECT COUNT(*) FROM frc_user WHERE email = :email');
 | 
					        $user = Security::findUserByEmail($argv[2], $db);
 | 
				
			||||||
        $existsQuery->bindValue(':email', $argv[2]);
 | 
					        if ($user) {
 | 
				
			||||||
        $existsResult = $existsQuery->execute();
 | 
					 | 
				
			||||||
        if (!$existsResult) {
 | 
					 | 
				
			||||||
            printfn('SQLite error: %s', $db->lastErrorMsg());
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if ($existsResult->fetchArray(SQLITE3_NUM)[0] != 0) {
 | 
					 | 
				
			||||||
            printfn('A user with e-mail address "%s" already exists', $argv[2]);
 | 
					            printfn('A user with e-mail address "%s" already exists', $argv[2]);
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -100,14 +98,8 @@ function set_password(): void {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
        // Ensure this user exists
 | 
					        // Ensure this user exists
 | 
				
			||||||
        $existsQuery = $db->prepare('SELECT COUNT(*) FROM frc_user WHERE email = :email');
 | 
					        $user = Security::findUserByEmail($argv[2], $db);
 | 
				
			||||||
        $existsQuery->bindValue(':email', $argv[2]);
 | 
					        if (!$user) {
 | 
				
			||||||
        $existsResult = $existsQuery->execute();
 | 
					 | 
				
			||||||
        if (!$existsResult) {
 | 
					 | 
				
			||||||
            printfn('SQLite error: %s', $db->lastErrorMsg());
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if ($existsResult->fetchArray(SQLITE3_NUM)[0] == 0) {
 | 
					 | 
				
			||||||
            printfn('No user exists with e-mail address "%s"', $argv[2]);
 | 
					            printfn('No user exists with e-mail address "%s"', $argv[2]);
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -122,29 +114,25 @@ function set_password(): void {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Delete a user
 | 
					 * Delete a user
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param string $email The e-mail address of the user to be deleted
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
function delete_user(): void {
 | 
					function delete_user(string $email): void {
 | 
				
			||||||
    global $argv;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $db = Data::getConnection();
 | 
					    $db = Data::getConnection();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
 | 
					        $displayUser = $email == Security::SINGLE_USER_EMAIL ? 'single-user mode user' : "user \"$email\"";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Get the ID for the provided e-mail address
 | 
					        // Get the ID for the provided e-mail address
 | 
				
			||||||
        $idQuery = $db->prepare('SELECT id FROM frc_user WHERE email = :email');
 | 
					        $user = Security::findUserByEmail($email, $db);
 | 
				
			||||||
        $idQuery->bindValue(':email', $argv[2]);
 | 
					        if (!$user) {
 | 
				
			||||||
        $idResult = $idQuery->execute();
 | 
					            printfn('No %s exists', $displayUser);
 | 
				
			||||||
        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]);
 | 
					 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $feedCountQuery = $db->prepare('SELECT COUNT(*) FROM feed WHERE user_id = :user');
 | 
					        $feedCountQuery = $db->prepare('SELECT COUNT(*) FROM feed WHERE user_id = :user');
 | 
				
			||||||
        $feedCountQuery->bindValue(':user', $id[0]);
 | 
					        $feedCountQuery->bindValue(':user', $user['id']);
 | 
				
			||||||
        $feedCountResult = $feedCountQuery->execute();
 | 
					        $feedCountResult = $feedCountQuery->execute();
 | 
				
			||||||
        if (!$feedCountResult) {
 | 
					        if (!$feedCountResult) {
 | 
				
			||||||
            printfn('SQLite error: %s', $db->lastErrorMsg());
 | 
					            printfn('SQLite error: %s', $db->lastErrorMsg());
 | 
				
			||||||
@ -152,25 +140,52 @@ function delete_user(): void {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
        $feedCount = $feedCountResult->fetchArray(SQLITE3_NUM);
 | 
					        $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')) {
 | 
					        if (!$proceed || !str_starts_with(strtolower($proceed), 'y')) {
 | 
				
			||||||
            printfn('Deletion canceled');
 | 
					            printfn('Deletion canceled');
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $itemDelete = $db->prepare('DELETE FROM item WHERE feed_id IN (SELECT id FROM feed WHERE user_id = :user)');
 | 
					        $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();
 | 
					        $itemDelete->execute();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $feedDelete = $db->prepare('DELETE FROM feed WHERE user_id = :user');
 | 
					        $feedDelete = $db->prepare('DELETE FROM feed WHERE user_id = :user');
 | 
				
			||||||
        $feedDelete->bindValue(':user', $id[0]);
 | 
					        $feedDelete->bindValue(':user', $user['id']);
 | 
				
			||||||
        $feedDelete->execute();
 | 
					        $feedDelete->execute();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $userDelete = $db->prepare('DELETE FROM frc_user WHERE id = :user');
 | 
					        $userDelete = $db->prepare('DELETE FROM frc_user WHERE id = :user');
 | 
				
			||||||
        $userDelete->bindValue(':user', $id[0]);
 | 
					        $userDelete->bindValue(':user', $user['id']);
 | 
				
			||||||
        $userDelete->execute();
 | 
					        $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 {
 | 
					    } finally {
 | 
				
			||||||
        $db->close();
 | 
					        $db->close();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user