feed-reader-central/src/lib/Security.php
Daniel J. Summers d06249aecd beta4 changes (#26)
These changes are mostly in underlying libraries; however, this now uses the [inspired by F#](https://git.bitbadger.solutions/bit-badger/inspired-by-fsharp) library to handle the feed parsing pipeline and optional return values

Reviewed-on: #26
2024-08-06 23:20:17 +00:00

150 lines
5.2 KiB
PHP

<?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
namespace FeedReaderCentral;
use BitBadger\InspiredByFSharp\Option;
use BitBadger\PDODocument\{DocumentException, Field, Patch};
/**
* Security functions
*/
readonly class Security
{
/** @var int Run as a single user requiring no password */
public const SingleUserMode = 0;
/** @var int Run as a single user requiring a password */
public const SingleUserPasswordMode = 1;
/** @var int Require users to provide e-mail address and password */
public const MultiUserMode = 2;
/**
* @var int Run as a single user requiring no password
* @deprecated Use Security::SingleUserMode instead
*/
public const SINGLE_USER = 0;
/**
* @var int Run as a single user requiring a password
* @deprecated Use Security::SingleUserPasswordMode instead
*/
public const SINGLE_USER_WITH_PASSWORD = 1;
/**
* @var int Require users to provide e-mail address and password
* @deprecated Use Security::MultiUserMode instead
*/
public const MULTI_USER = 2;
/** @var string The e-mail address for the single user */
public const SingleUserEmail = 'solouser@example.com';
/** @var string The password for the single user with no password */
public const SingleUserPassword = 'no-password-required';
/** @var string The password algorithm to use for our passwords */
public const PasswordAlgorithm = PASSWORD_DEFAULT;
/** Prevent instances of this class */
private function __construct() {}
/**
* Verify a user's password
*
* @param User $user The user information retrieved from the database
* @param string $password The password provided by the user
* @param Option<string> $returnTo The URL to which the user should be redirected
* @throws DocumentException if any is encountered
*/
private static function verifyPassword(User $user, string $password, Option $returnTo): void
{
if (password_verify($password, $user->password)) {
if (password_needs_rehash($user->password, self::PasswordAlgorithm)) {
Patch::byId(Table::User, $user->id, ['password' => password_hash($password, self::PasswordAlgorithm)]);
}
$_SESSION[Key::UserId] = $user->id;
$_SESSION[Key::UserEmail] = $user->email;
frc_redirect($returnTo->getOrDefault('/'));
}
}
/**
* Log on a user with e-mail address and password
*
* @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 Option<string> $returnTo The URL to which the user should be redirected
* @throws DocumentException If any is encountered
*/
public static function logOnUser(string $email, string $password, Option $returnTo): void
{
if (SECURITY_MODEL === self::SingleUserPasswordMode) {
$dbEmail = self::SingleUserEmail;
} else {
if ($email === self::SingleUserEmail) {
add_error('Invalid credentials; log on unsuccessful');
return;
}
$dbEmail = $email;
}
User::findByEmail($dbEmail)->iter(fn(User $it) => self::verifyPassword($it, $password, $returnTo));
add_error('Invalid credentials; log on unsuccessful');
}
/**
* Update the password for the given user
*
* @param string $email The e-mail address of the user whose password should be updated
* @param string $password The new password for this user
* @throws DocumentException If any is encountered
*/
public static function updatePassword(string $email, string $password): void
{
Patch::byFields(Table::User, [Field::EQ('email', $email)],
['password' => password_hash($password, self::PasswordAlgorithm)]);
}
/**
* Log on the single user
*
* @throws DocumentException If any is encountered
*/
private static function logOnSingleUser(): void
{
$user = User::findByEmail(self::SingleUserEmail)->getOrCall(function () {
User::add(self::SingleUserEmail, self::SingleUserPassword);
return User::findByEmail(self::SingleUserEmail)->get();
});
self::verifyPassword($user, self::SingleUserPassword, $_GET['returnTo']);
}
/**
* Verify that user is logged on
*
* @param bool $redirectIfAnonymous Whether to redirect the request if there is no user logged on
* @throws DocumentException If any is encountered
*/
public static function verifyUser(bool $redirectIfAnonymous = true): void
{
if (key_exists(Key::UserId, $_SESSION)) return;
if (SECURITY_MODEL === self::SingleUserMode) self::logOnSingleUser();
if (SECURITY_MODEL !== self::SingleUserPasswordMode && SECURITY_MODEL != self::MultiUserMode) {
die('Unrecognized security model (' . SECURITY_MODEL . ')');
}
if ($redirectIfAnonymous) {
header("Location: /user/log-on?returnTo={$_SERVER['REQUEST_URI']}", true, 307);
die();
}
}
}