* @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 $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 $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(); } } }