<?php

namespace Mnv\Modules\User;

use Mnv\Core\Auth\Status;
use Mnv\Core\Utilities\Base64\Base64;
use Mnv\Core\Utilities\Cookie\Cookie;
use Mnv\Core\Utilities\Cookie\Session;

use Mnv\Core\Auth\Errors\UserPhoneRequiredError;
use Mnv\Core\Auth\Exceptions\InvalidPhoneException;
use Mnv\Core\Auth\Exceptions\PhoneNotPaidException;
use Mnv\Core\Auth\Exceptions\ValidatePasswordException;
use Mnv\Core\Auth\Errors\PhoneRequiredError;
use Mnv\Core\Auth\Errors\UsernameRequiredError;
use Mnv\Core\Auth\Exceptions\UnknownIdException;
use Mnv\Core\Auth\Exceptions\EmailNotPaidException;
use Mnv\Core\Auth\Exceptions\PhoneNotVerifiedException;

use Mnv\Core\Auth\Exceptions\NotLoggedInException;
use Mnv\Core\Auth\Exceptions\InvalidEmailException;
use Mnv\Core\Auth\Exceptions\TokenExpiredException;
use Mnv\Core\Auth\Exceptions\UserRoleExistsException;
use Mnv\Core\Auth\Exceptions\InvalidPasswordException;
use Mnv\Core\Auth\Exceptions\TooManyRequestsException;
use Mnv\Core\Auth\Exceptions\UnknownUsernameException;
use Mnv\Core\Auth\Exceptions\EmailNotVerifiedException;
use Mnv\Core\Auth\Exceptions\AttemptCancelledException;
use Mnv\Core\Auth\Exceptions\UserAlreadyExistsException;
use Mnv\Core\Auth\Exceptions\AmbiguousUsernameException;
use Mnv\Core\Auth\Exceptions\DuplicateUsernameException;
use Mnv\Core\Auth\Exceptions\InvalidSelectorTokenPairException;

use Mnv\Core\Auth\Errors\AuthError;
use Mnv\Core\Auth\Errors\DatabaseError;
use Mnv\Core\Auth\Exceptions\AuthException;
use Mnv\Core\Auth\Errors\MissingCallbackError;
use Mnv\Core\Auth\Errors\HeadersAlreadySentError;
use Mnv\Core\Auth\Exceptions\ResetDisabledException;
use Mnv\Core\Auth\Errors\EmailOrUsernameRequiredError;
use Mnv\Core\Auth\Exceptions\ConfirmationRequestNotFound;

use Mnv\Core\Uploads\ImageSizes;
use Mnv\Core\Database\Throwable\Error;
use Mnv\Core\Database\Throwable\IntegrityConstraintViolationException;
use Mnv\Core\Validations\ValidateEmail;
use Mnv\Core\Validations\ValidatePhone;
use Mnv\Models\Users\UserTypes;


/** Компонент, который предоставляет все функции и утилиты для безопасной аутентификации отдельных пользователей. */
final class Auth extends UserManager
{

    private string $table = 'users';
    private string $table_images = 'user_images';
    private string $table_remembered = 'users_remembered';

	const COOKIE_PREFIXES = [Cookie::PREFIX_SECURE, Cookie::PREFIX_HOST];
	const COOKIE_CONTENT_SEPARATOR = '~';

    /** @var string текущий IP-адрес пользователя */
    private $ipAddress;
    /** @var bool должно ли регулирование быть включено (например, в производстве) или отключено (например, во время разработки) */
    private $throttling;
    /** @var int интервал в секундах, по истечении которого необходимо повторно синхронизировать данные сеанса с его официальным источником в базе данных */
    private $sessionResyncInterval;
    /** @var string имя файла cookie, используемого для функции "запомнить меня" */
    private $rememberCookieName;

	/**
	 * @param string|null $ipAddress (optional) IP-адрес, который следует использовать вместо настройки по умолчанию (если есть), например когда за прокси
	 * @param bool|null $throttling (optional) должно ли регулирование быть включено (например, в производстве) или отключено (например, во время разработки)
	 * @param int|null $sessionResyncInterval (optional) интервал в секундах, по истечении которого необходимо повторно синхронизировать данные сеанса с его официальным источником в базе данных
	 */
	public function __construct($ipAddress = null, $throttling = null, $sessionResyncInterval = null)
	{

		$this->ipAddress = !empty($ipAddress) ? $ipAddress : (isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null);
		$this->throttling = isset($throttling) ? (bool) $throttling : true;
		$this->sessionResyncInterval = isset($sessionResyncInterval) ? ((int) $sessionResyncInterval) : (60 * 5);
		$this->rememberCookieName = self::createRememberCookieName();

		$this->initSessionIfNecessary();
		$this->enhanceHttpSecurity();

		$this->processRememberDirective();
		$this->resyncSessionIfNecessary();
	}

	/** Инициализирует сеанс и устанавливает правильную конфигурацию */
	private function initSessionIfNecessary()
    {
		if (\session_status() === \PHP_SESSION_NONE) {
			// use cookies to store session IDs
			\ini_set('session.use_cookies', 1);
			// use cookies only (do not send session IDs in URLs)
			\ini_set('session.use_only_cookies', 1);
			// do not send session IDs in URLs
			\ini_set('session.use_trans_sid', 0);

			// start the session (requests a cookie to be written on the client)
			@Session::start();
		}
	}

	/** Повышает безопасность приложения по протоколу HTTP(S), задавая определенные заголовки. */
	private function enhanceHttpSecurity()
	{
		// remove exposure of PHP version (at least where possible)
		\header_remove('X-Powered-By');

		// if the user is signed in
		if ($this->isLoggedIn()) {
			// prevent clickjacking
			\header('X-Frame-Options: sameorigin');
			// prevent content sniffing (MIME sniffing)
			\header('X-Content-Type-Options: nosniff');

			// disable caching of potentially sensitive data
			\header('Cache-Control: no-store, no-cache, must-revalidate', true);
			\header('Expires: Thu, 19 Nov 1981 00:00:00 GMT', true);
			\header('Pragma: no-cache', true);
		}
	}

	/** Проверяет наличие установленной директивы "запомнить меня" и обрабатывает автоматический вход (при необходимости) */
	private function processRememberDirective()
	{
		// if the user is not signed in yet
		if (!$this->isLoggedIn()) {
			// if there is currently no cookie for the 'remember me' feature
			if (!isset($_COOKIE[$this->rememberCookieName])) {
				// if an old cookie for that feature from versions v1.x.x to v6.x.x has been found
				if (isset($_COOKIE['auth_remember'])) {
					// use the value from that old cookie instead
					$_COOKIE[$this->rememberCookieName] = $_COOKIE['auth_remember'];
				}
			}

			// if a remember cookie is set
			if (isset($_COOKIE[$this->rememberCookieName])) {
				// assume the cookie and its contents to be invalid until proven otherwise
				$valid = false;

				// split the cookie's content into selector and token
				$parts = \explode(self::COOKIE_CONTENT_SEPARATOR, $_COOKIE[$this->rememberCookieName], 2);

				// if both selector and token were found
				if (!empty($parts[0]) && !empty($parts[1])) {
                    try {
                        $rememberData = connect()->table('users_remembered AS ur')
                            ->join('users AS u','ur.user', '=', 'u.userId')
                            ->select('ur.user, ur.token, ur.expires, u.email, u.fullName, u.firstName, u.lastName, u.loginName, u.status, u.accessLevel, u.force_logout')
                            ->where('ur.selector', $parts[0])->get('array');
                    }
					catch (Error $e) {
						throw new DatabaseError($e->getMessage());
					}

					if (!empty($rememberData)) {
						if ($rememberData['expires'] >= \time()) {
							if (\password_verify($parts[1], $rememberData['token'])) {
								// cookie и его содержимое теперь доказали свою действительность
								$valid = true;

								$this->onLoginUserSuccessful($rememberData['user'], $rememberData['fullName'], $rememberData['firstName'], $rememberData['lastName'], $rememberData['email'], $rememberData['loginName'], $rememberData['status'], $rememberData['accessLevel'], $rememberData['force_logout'], true);
							}
						}
					}
				}

				// if the cookie or its contents have been invalid
				if (!$valid) {
					// mark the cookie as such to prevent any further futile attempts
					$this->setRememberCookie('', '', \time() + 60 * 60 * 24 * 365.25);
				}
			}
		}
	}

    private function resyncSessionIfNecessary()
	{
		// если пользователь вошел в систему
		if ($this->isLoggedIn()) {
			// следующее поле сеанса, возможно, не было инициализировано для сеансов, которые уже существовали до введения этой функции
			if (!isset($_SESSION['siteUser'][self::SESSION_USER_LAST_RESYNC])) {
				$_SESSION['siteUser'][self::SESSION_USER_LAST_RESYNC] = 0;
			}

			// если пришло время для повторной синхронизации
			if (($_SESSION['siteUser'][self::SESSION_USER_LAST_RESYNC] + $this->sessionResyncInterval) <= \time()) {
				// снова получить достоверные данные из базы данных
				try {
                    $authoritativeData = connect($this->table)
                        ->select('email, fullName, loginName, status, accessLevel, force_logout')
                        ->where('userId', $this->getUserId())->get('array');
				}
				catch (Error $e) {
					throw new DatabaseError($e->getMessage());
				}

				// если данные пользователя были найдены
				if (!empty($authoritativeData)) {
					// следующее поле сеанса, возможно, не было инициализировано для сеансов, которые уже существовали до введения этой функции
					if (!isset($_SESSION['siteUser'][self::SESSION_USER_FORCE_LOGOUT])) {
						$_SESSION['siteUser'][self::SESSION_USER_FORCE_LOGOUT] = 0;
					}

					// если счетчик, отслеживающий принудительные выходы из системы, был увеличен
					if ($authoritativeData['force_logout'] > $_SESSION['siteUser'][self::SESSION_USER_FORCE_LOGOUT]) {
						// пользователь должен выйти из системы
						$this->logOutUser();
					}
					// если счетчик, отслеживающий принудительные выходы из системы, остался неизменным
					else {
						// данные сеанса необходимо обновить
						$_SESSION['siteUser'][self::SESSION_USER_EMAIL]     = $authoritativeData['email'];
						$_SESSION['siteUser'][self::SESSION_USER_USERNAME]  = $authoritativeData['loginName'];
						$_SESSION['siteUser'][self::SESSION_USER_STATUS]    = (int) $authoritativeData['status'];
						$_SESSION['siteUser'][self::SESSION_USER_ROLES]     = (int) $authoritativeData['accessLevel'];

						// помните, что мы только что выполнили необходимую повторную синхронизацию
						$_SESSION['siteUser'][self::SESSION_USER_LAST_RESYNC] = \time();
					}
				}
				// если данные для пользователя не найдены
				else {
					// их учетная запись могла быть удалена, поэтому они должны выйти из системы
					$this->logOutUser();
				}
			}
		}
	}

	/**
	 * Зарегистрировать пользователя
	 * Если вы хотите, чтобы учетная запись пользователя была активирована по умолчанию, передайте `null` в качестве callback
	 * Если вы хотите заставить пользователя сначала подтвердить свой адрес электронной почты, передайте анонимную функцию в качестве callback
	 * Функция callback должна иметь следующую подпись: `function ($selector, $token)`
	 * Обе части информации должны быть отправлены пользователю, обычно встроены в ссылку
	 * Когда пользователь захочет подтвердить свой адрес электронной почты в качестве следующего шага, снова потребуются обе части
	 *
	 * @param string $email the email address to register
	 * @param string $password the password for the new account
	 * @param string|null $loginName (optional) the loginName that will be displayed
	 * @param callable|null $callback (optional) the function that sends the confirmation email to the user
	 * @return int the ID of the user that has been created (if any)
	 * @throws InvalidEmailException if the email address was invalid
	 * @throws InvalidPasswordException if the password was invalid
	 * @throws UserAlreadyExistsException if a user with the specified email address already exists
	 * @throws TooManyRequestsException if the number of allowed attempts/requests has been exceeded
	 * @throws AuthError if an internal problem occurred (do *not* catch)
	 *
	 * @see confirmEmail
	 * @see confirmEmailAndSignIn
	 */
	public function register($email, $password, $loginName = null, callable $callback = null)
    {
		$this->throttle(['enumerateUsers', $this->getIpAddress()], 1, (60 * 60), 75);
		$this->throttle(['createNewAccount', $this->getIpAddress()], 1, (60 * 60 * 12), 5, true);

        $newUserId = $this->createUserInternal(false, $email, $password, $loginName, $callback);

		$this->throttle(['createNewAccount', $this->getIpAddress()], 1, (60 * 60 * 12), 5, false);

		return $newUserId;
	}

	/**
	 * Зарегистрировать пользователя, гарантируя, что имя пользователя уникально
     * Если вы хотите, чтобы учетная запись пользователя была активирована по умолчанию, передайте `null` в качестве callback
     * Если вы хотите, чтобы пользователь сначала подтвердил `verify` свой адрес электронной почты, передайте анонимную функцию в качестве обратного вызова `callback`.
     * Функция callback должна иметь следующую подпись: `function ($selector, $token)`
	 * Обе части информации должны быть отправлены пользователю, как правило, встроены в ссылку
	 * Когда пользователь хочет подтвердить свой адрес электронной почты в качестве следующего шага, снова потребуются обе части.
	 *
	 * @param string $unique the unique field to register
	 * @param array $user the fields new account
	 * @param string|null $role (optional) the loginName that will be displayed
	 * @param callable|null $callback (optional) the function that sends the confirmation email to the user
	 * @return int the ID of the user that has been created (if any)
	 * @throws InvalidEmailException if the email address was invalid
	 * @throws InvalidPasswordException if the password was invalid
	 * @throws ValidatePasswordException if the password was invalid
	 * @throws UserAlreadyExistsException if a user with the specified email address already exists
	 * @throws DuplicateUsernameException if the specified loginName wasn't unique
	 * @throws TooManyRequestsException if the number of allowed attempts/requests has been exceeded
	 * @throws AuthError if an internal problem occurred (do *not* catch)
	 *
	 * @see confirmEmail
	 * @see confirmEmailAndSignIn
	 */
	public function registerWithUniqueUsername($unique, $user, $role, $confirmType, $confirm, callable $callback = null)
    {
		$this->throttle(['enumerateUsers', $this->getIpAddress()], 1000, (60 * 60), 75);
		$this->throttle(['createNewAccount', $this->getIpAddress()], 1000, (60 * 60), 75, true, 1);

        $newUserId = $this->createSiteUserInternal($unique, true, $user, $role, $confirmType, $confirm, $callback);

		$this->throttle(['createNewAccount', $this->getIpAddress()], 1000, (60 * 60), 75, false, 1);

		return $newUserId;
	}


	/**
	 * Попытки войти в систему пользователя с их email address and password
	 *
	 * @param string $email the user's email address
	 * @param string $password the user's password
	 * @param int|null $rememberDuration (optional) the duration in seconds to keep the user logged in ("remember me"), e.g. `60 * 60 * 24 * 365.25` for one year
	 * @param callable|null $onBeforeSuccess (optional) a function that receives the user's ID as its single parameter and is executed before successful authentication; must return `true` to proceed or `false` to cancel
	 * @throws InvalidEmailException if the email address was invalid or could not be found
	 * @throws InvalidPasswordException if the password was invalid
	 * @throws EmailNotVerifiedException if the email address has not been verified yet via confirmation email
	 * @throws AttemptCancelledException if the attempt has been cancelled by the supplied callback that is executed before success
	 * @throws TooManyRequestsException if the number of allowed attempts/requests has been exceeded
	 * @throws AuthError if an internal problem occurred (do *not* catch)
	 */
	public function login($email, $password, $rememberDuration = null, callable $onBeforeSuccess = null)
    {
		$this->throttle([ 'attemptToLogin', 'email', $email ], 1000, (60 * 60 * 24), 500, true);

		$this->authenticateUserInternal($password, $email, null, $rememberDuration, $onBeforeSuccess);
	}

	/**
	 * Попытки войти в систему пользователя с их loginName and password
	 *
	 * При использовании этого метода для аутентификации пользователей следует убедиться, что имена пользователей уникальны.
	 *
	 * Consistently using {@see registerWithUniqueUsername} instead of {@see register} can be helpful
	 *
	 * @param string $loginName логин пользователя
	 * @param string $password пароль пользователя
	 * @param int|null $rememberDuration (необязательно) продолжительность в секундах, в течение которой пользователь будет оставаться в системе («запомнить меня»), например. `60 * 60 * 24 * 365,25` в течение одного года
	 * @param callable|null $onBeforeSuccess (необязательно) функция, которая получает идентификатор пользователя в качестве единственного параметра и выполняется перед успешной аутентификацией; должен вернуть `true`, чтобы продолжить, или `false`, чтобы отменить
	 * @throws UnknownUsernameException if the specified loginName does not exist
	 * @throws AmbiguousUsernameException if the specified loginName is ambiguous, i.e. there are multiple users with that name
	 * @throws InvalidPasswordException if the password was invalid
	 * @throws EmailNotVerifiedException if the email address has not been verified yet via confirmation email
	 * @throws AttemptCancelledException if the attempt has been cancelled by the supplied callback that is executed before success
	 * @throws TooManyRequestsException if the number of allowed attempts/requests has been exceeded
	 * @throws AuthError if an internal problem occurred (do *not* catch)
	 */
	public function loginWithUsername($loginName, $password, $rememberDuration = null, callable $onBeforeSuccess = null)
    {
		$this->throttle(['attemptToLogin', 'loginName', $loginName], 500, (60 * 60 * 24), null, true);

		$this->authenticateUserInternal($password, null, $loginName, $rememberDuration, $onBeforeSuccess);
	}

    /**
     * Попытки войти в систему пользователя с их loginName and password
     *
     * При использовании этого метода для аутентификации пользователей следует убедиться, что имена пользователей уникальны.
     *
     * Consistently using {@see registerWithUniquePhone} instead of {@see register} can be helpful
     *
     * @param string $phone логин пользователя
     * @param string $password пароль пользователя
     * @param int $confirmSms  подтверждение
     * @param int|null $rememberDuration (необязательно) продолжительность в секундах, в течение которой пользователь будет оставаться в системе («запомнить меня»), например. `60 * 60 * 24 * 365,25` в течение одного года
     * @param callable|null $callback (необязательно) функция, которая получает идентификатор пользователя в качестве единственного параметра и выполняется перед успешной аутентификацией; должен вернуть `true`, чтобы продолжить, или `false`, чтобы отменить
     *
     * @throws AttemptCancelledException if the attempt has been cancelled by the supplied callback that is executed before success
     * @throws AuthError if an internal problem occurred (do *not* catch)
     * @throws DatabaseError
     * @throws IntegrityConstraintViolationException
     * @throws InvalidPasswordException if the password was invalid
     * @throws ValidatePasswordException if the password was invalid
     * @throws InvalidPhoneException если телефон был недействителен или его не удалось найти
     * @throws PhoneNotVerifiedException if the phone has not been verified yet via confirmation phone
     * @throws TooManyRequestsException if the number of allowed attempts/requests has been exceeded
     * @throws UnknownIdException пользователь с таким ID не найден
     * @throws UserPhoneRequiredError
     */
    public function loginWithPhone($phone, $password, $confirmSms = null, $rememberDuration = null, callable $callback = null)
    {
        $this->throttle(['attemptToPhone', 'phone', $phone], 500, (60 * 60 * 24), null, true);

        $this->authenticateExistingUserByPhone($password, $phone, $confirmSms, $rememberDuration, $callback);
    }

	/**
	 * Попытки снова подтвердить пароль текущего пользователя, вошедшего в систему
	 *
	 * Whenever you want to confirm the user's identity again, e.g. before
	 * the user is allowed to perform some "dangerous" action, you should
	 * use this method to confirm that the user is who they claim to be.
	 *
	 * For example, when a user has been remembered by a long-lived cookie
	 * and thus {@see isRemembered} returns `true`, this means that the
	 * user has not entered their password for quite some time anymore.
	 *
	 * @param string $password the user's password
	 * @return bool whether the supplied password has been correct
	 * @throws NotLoggedInException if the user is not currently signed in
	 * @throws TooManyRequestsException if the number of allowed attempts/requests has been exceeded
	 * @throws ValidatePasswordException if the number of allowed attempts/requests has been exceeded
	 * @throws AuthError if an internal problem occurred (do *not* catch)
	 */
	public function reconfirmPassword($password)
    {
		if ($this->isLoggedIn()) {
			try {
				$password = self::validatePassword($password);
			}
			catch (InvalidPasswordException $e) {
				return false;
			}

			$this->throttle(['reconfirmPassword', $this->getIpAddress()], 3, (60 * 60), 4, true);

			try {
                $expectedHash = connect($this->table)->select('password')->where('userId', $this->getUserId())->get();
			}
			catch (Error $e) {
				throw new DatabaseError($e->getMessage());
			}

			if (!empty($expectedHash->password)) {
				$validated = \password_verify($password, $expectedHash->password);
				if (!$validated) {
					$this->throttle(['reconfirmPassword', $this->getIpAddress()], 3, (60 * 60), 4, false);
				}

				return $validated;
			} else {
				throw new NotLoggedInException();
			}
		} else {
			throw new NotLoggedInException();
		}
	}

    /**
     * Выполняет выход пользователя из системы
     *
     * @throws AuthError if an internal problem occurred (do *not* catch)
     */
    public function logOutUser() {
        // если user вошел в систему
        if ($this->isLoggedIn()) {
            // получить любую локально существующую директиву remember
            $rememberDirectiveSelector = $this->getRememberDirectiveSelector();

            // если такая директива remember существует
            if (isset($rememberDirectiveSelector)) {
                // удалить директиву local remember
                $this->deleteRememberDirectiveForUserById($this->getUserId(), $rememberDirectiveSelector);
            }

            unset($_SESSION['siteUser']);
        }
    }

	/**
	 * Выполняет выход пользователя из всех остальных сеансов (кроме текущего)
	 *
	 * @throws NotLoggedInException if the user is not currently signed in
	 * @throws AuthError if an internal problem occurred (do *not* catch)
	 */
	public function logOutEverywhereElse()
	{
		if (!$this->isLoggedIn()) {
			throw new NotLoggedInException();
		}

		// определить дату истечения срока действия любой локально существующей директивы запоминания
		$previousRememberDirectiveExpiry = $this->getRememberDirectiveExpiry();

		// запланировать принудительный выход из системы во всех сеансах
		$this->forceLogoutForUserById($this->getUserId());

		// следующее поле сеанса могло быть не инициализировано для сеансов, которые уже существовали до введения этой функции.
		if (!isset($_SESSION['siteUser'][self::SESSION_USER_FORCE_LOGOUT])) {
			$_SESSION['siteUser'][self::SESSION_USER_FORCE_LOGOUT] = 0;
		}

		// убедитесь, что мы просто пропустим или проигнорируем следующий принудительный выход из системы (который мы только что вызвали) в текущем сеансе
		$_SESSION['siteUser'][self::SESSION_USER_FORCE_LOGOUT]++;

		// повторно сгенерировать идентификатор сеанса, чтобы предотвратить атаки фиксации сеанса (запрашивает запись файла cookie на клиенте)
		Session::regenerate(true);

		// если ранее существовала директива запоминания
		if (isset($previousRememberDirectiveExpiry)) {
			// восстановить директиву со старой датой истечения срока действия, но с новыми учетными данными
			$this->createRememberDirective($this->getUserId(), $previousRememberDirectiveExpiry - \time());
		}
	}

	/**
	 * Выходит из системы во всех sessions
	 *
	 * @throws NotLoggedInException if the user is not currently signed in
	 * @throws AuthError if an internal problem occurred (do *not* catch)
	 */
	public function logOutEverywhere()
	{
		if (!$this->isLoggedIn()) {
			throw new NotLoggedInException();
		}

		// запланировать принудительный выход из системы во всех sessions
		$this->forceLogoutForUserById($this->getUserId());
		// и немедленно примените выход из системы локально
		$this->logOutUser();
	}

	/**
	 * Destroys all session data
	 *
	 * @throws AuthError if an internal problem occurred (do *not* catch)
	 */
	public function destroySession() {
		// remove all session variables without exception
		$_SESSION = [];
		// delete the session cookie
		$this->deleteSessionCookie();
		// let PHP destroy the session
		\session_destroy();
	}

	/**
	 * Создает новую директиву, удерживающую пользователя в системе ("remember me")
	 *
	 * @param int $userId the user ID to keep signed in
	 * @param int $duration the duration in seconds
	 * @throws AuthError if an internal problem occurred (do *not* catch)
	 */
	private function createRememberDirective($userId, $duration)
	{
		$selector = self::createRandomString(24);
		$token = self::createRandomString(32);
		$tokenHashed = \password_hash($token, \PASSWORD_DEFAULT);
		$expires = \time() + ((int) $duration);

		try {
            connect($this->table_remembered)->insert([
                'user'      => (int) $userId,
                'selector'  => $selector,
                'token'     => $tokenHashed,
                'expires'   => $expires
            ]);
		}
		catch (Error $e) {
			throw new DatabaseError($e->getMessage());
		}

		$this->setRememberCookie($selector, $token, $expires);
	}

	protected function deleteRememberDirectiveForUserById($userId, $selector = null)
	{
		parent::deleteRememberDirectiveForUserById($userId, $selector);

		$this->setRememberCookie(null, null, \time() - 3600);
	}

	/**
	 * Устанавливает или обновляет файл cookie, который управляет token "remember me" token
	 *
	 * @param string|null $selector the selector from the selector/token pair
	 * @param string|null $token the token from the selector/token pair
	 * @param int $expires the UNIX time in seconds which the token should expire at
	 * @throws AuthError if an internal problem occurred (do *not* catch)
	 */
	private function setRememberCookie($selector, $token, $expires)
	{
		$params = \session_get_cookie_params();

		if (isset($selector) && isset($token)) {
			$content = $selector . self::COOKIE_CONTENT_SEPARATOR . $token;
		}
		else {
			$content = '';
		}

		// save the cookie with the selector and token (requests a cookie to be written on the client)
		$cookie = new Cookie($this->rememberCookieName);
		$cookie->setValue($content);
		$cookie->setExpiryTime($expires);
		$cookie->setPath($params['path']);
		$cookie->setDomain($params['domain']);
		$cookie->setHttpOnly($params['httponly']);
		$cookie->setSecureOnly($params['secure']);
		$result = $cookie->save();

		if ($result === false) {
			throw new HeadersAlreadySentError();
		}

		// if we've been deleting the cookie above
		if (!isset($selector) || !isset($token)) {
			// attempt to delete a potential old cookie from versions v1.x.x to v6.x.x as well (requests a cookie to be written on the client)
			$cookie = new Cookie('auth_remember');
			$cookie->setPath((!empty($params['path'])) ? $params['path'] : '/');
			$cookie->setDomain($params['domain']);
			$cookie->setHttpOnly($params['httponly']);
			$cookie->setSecureOnly($params['secure']);
			$cookie->delete();
		}
	}

    /**
     * @throws IntegrityConstraintViolationException
     * @throws DatabaseError
     */
    protected function onLoginUserSuccessful($userId, $fullName, $firstName, $lastName, $email, $loginName, $status, $roles, $forceLogout, $remembered)
	{
		// обновить метку времени последнего входа пользователя
		try {
            connect($this->table)->where('userId', $userId)->update(['last_login' => \time()]);
		}
		catch (Error $e) {
			throw new DatabaseError($e->getMessage());
		}

		parent::onLoginUserSuccessful($userId, $fullName, $firstName, $lastName, $email, $loginName, $status, $roles, $forceLogout, $remembered);
	}

	/**
	 * Deletes the session cookie on the client / Удаляет session cookie на клиенте
	 *
	 * @throws AuthError if an internal problem occurred (do *not* catch)
	 */
	private function deleteSessionCookie() {
		$params = \session_get_cookie_params();

		// ask for the session cookie to be deleted (requests a cookie to be written on the client)
		$cookie = new Cookie(\session_name());
		$cookie->setPath($params['path']);
		$cookie->setDomain($params['domain']);
		$cookie->setHttpOnly($params['httponly']);
		$cookie->setSecureOnly($params['secure']);
		$result = $cookie->delete();

		if ($result === false) {
			throw new HeadersAlreadySentError();
		}
	}

	/**
	 * Подтверждает адрес электронной почты (и активирует учетную запись), указав правильную пару селектор / токен
	 * Confirms an email address (and activates the account) by supplying the correct selector/token pair
	 *
	 * Пара селектор/токен должна быть сгенерирована ранее при регистрации новой учетной записи.
	 * The selector/token pair must have been generated previously by registering a new account
	 *
	 * @param string $selector the selector from the selector/token pair
	 * @param string $token the token from the selector/token pair
	 * @return string[] an array with the old email address (if any) at index zero and the new email address (which has just been verified) at index one
	 * @throws InvalidSelectorTokenPairException if either the selector or the token was not correct
	 * @throws TokenExpiredException if the token has already expired
	 * @throws UserAlreadyExistsException if an attempt has been made to change the email address to a (now) occupied address
	 * @throws TooManyRequestsException if the number of allowed attempts/requests has been exceeded
	 * @throws AuthError if an internal problem occurred (do *not* catch)
	 */
	public function confirmEmail($selector, $token)
	{
		$this->throttle(['confirmEmail', $this->getIpAddress()], 5, (60 * 60), 10);
		$this->throttle(['confirmEmail', 'selector', $selector], 3, (60 * 60), 10);
		$this->throttle(['confirmEmail', 'token', $token], 3, (60 * 60), 10);

		try {
            $confirmationData = connect()->table('users_confirmations', true)
                ->join($this->table, 'users.userId', '=', 'users_confirmations.user_id', '', true)
                ->select('users_confirmations.id, users_confirmations.user_id, users_confirmations.email AS new_email, users_confirmations.token, users_confirmations.expires, users.email AS old_email')
                ->where('users_confirmations.selector','=', $selector)->get('array');
		}
		catch (Error $e) {
			throw new DatabaseError($e->getMessage());
		}

		if (!empty($confirmationData)) {
			if (\password_verify($token, $confirmationData['token'])) {
				if ($confirmationData['expires'] >= \time()) {
					// аннулировать любые потенциальные невыполненные запросы на сброс пароля
					try {
                        connect()->table('users_resets')->where('user','=', $confirmationData['user_id'])->delete();
					}
					catch (Error $e) {
						throw new DatabaseError($e->getMessage());
					}

                    // отметьте адрес `email` как подтвержденный (и, возможно, обновите его на новый указанный `email`)
					try {
                        connect($this->table)->where('userId', $confirmationData['user_id'])->update(['email' => $confirmationData['new_email'], 'verified' => 1, 'status' => Status::NORMAL]);
					}
					catch (IntegrityConstraintViolationException $e) {
						throw new UserAlreadyExistsException();
					}
					catch (Error $e) {
						throw new DatabaseError($e->getMessage());
					}

					// if the user is currently signed in
					if ($this->isLoggedIn()) {
						// if the user has just confirmed an email address for their own account
						if ($this->getUserId() === $confirmationData['user_id']) {
							// immediately update the email address in the current session as well
							$_SESSION['siteUser'][self::SESSION_USER_EMAIL] = $confirmationData['new_email'];
						}
					}

					// consume the token just being used for confirmation
					try {
                        connect()->table('users_confirmations')->where('id','=', $confirmationData['id'])->delete();
					}
					catch (Error $e) {
						throw new DatabaseError($e->getMessage());
					}

					// if the email address has not been changed but simply been verified
					if ($confirmationData['old_email'] === $confirmationData['new_email']) {
						// the output should not contain any previous email address
						$confirmationData['old_email'] = null;
					}

					return [
						$confirmationData['old_email'],
						$confirmationData['new_email']
					];
				}
				else {
					throw new TokenExpiredException();
				}
			}
			else {
				throw new InvalidSelectorTokenPairException();
			}
		}
		else {
			throw new InvalidSelectorTokenPairException();
		}
	}

	/**
	 * Подтверждает адрес электронной почты и активирует учетную запись, указав правильную пару селектор/токен
	 * Confirms an email address and activates the account by supplying the correct selector/token pair
	 *
	 * Пара селектор / токен должна быть сгенерирована ранее при регистрации новой учетной записи.
	 * The selector/token pair must have been generated previously by registering a new account
	 *
	 * Пользователь будет автоматически авторизован, если эта операция прошла успешно.
	 * The user will be automatically signed in if this operation is successful
	 *
	 * @param string $selector the selector from the selector/token pair
	 * @param string $token the token from the selector/token pair
	 * @param int|null $rememberDuration (optional) the duration in seconds to keep the user logged in ("remember me"), e.g. `60 * 60 * 24 * 365.25` for one year
	 * @return string[] an array with the old email address (if any) at index zero and the new email address (which has just been verified) at index one
	 * @throws InvalidSelectorTokenPairException if either the selector or the token was not correct
	 * @throws TokenExpiredException if the token has already expired
	 * @throws UserAlreadyExistsException if an attempt has been made to change the email address to a (now) occupied address
	 * @throws TooManyRequestsException if the number of allowed attempts/requests has been exceeded
	 * @throws AuthError if an internal problem occurred (do *not* catch)
	 */
	public function confirmEmailAndSignIn($selector, $token, $rememberDuration = null)
	{
		$emailBeforeAndAfter = $this->confirmEmail($selector, $token);

		if (!$this->isLoggedIn()) {
			if ($emailBeforeAndAfter[1] !== null) {
				$emailBeforeAndAfter[1] = ValidateEmail::fromString($emailBeforeAndAfter[1]);

				$userData = $this->getUserDataBy('email', $emailBeforeAndAfter[1], ['userId', 'email', 'fullName', 'firstName', 'lastName', 'loginName', 'status', 'accessLevel', 'force_logout']);

				$this->onLoginUserSuccessful($userData['userId'], $userData['fullName'], $userData['firstName'], $userData['lastName'], $userData['email'], $userData['loginName'], $userData['status'], $userData['accessLevel'], $userData['force_logout'], true);

				if ($rememberDuration !== null) {
					$this->createRememberDirective($userData['userId'], $rememberDuration);
				}
			}
		}

		return $emailBeforeAndAfter;
	}

	/**
	 * Изменяет текущий пароль пользователя, вошедшего в систему, при этом для проверки требуется старый пароль.
	 * Changes the currently signed-in user's password while requiring the old password for verification
	 *
	 * @param string $oldPassword the old password to verify account ownership
	 * @param string $newPassword the new password that should be set
	 * @throws NotLoggedInException if the user is not currently signed in
	 * @throws InvalidPasswordException if either the old password has been wrong or the desired new one has been invalid
	 * @throws TooManyRequestsException if the number of allowed attempts/requests has been exceeded
	 * @throws AuthError if an internal problem occurred (do *not* catch)
	 */
	public function changePassword($oldPassword, $newPassword)
	{
		if ($this->reconfirmPassword($oldPassword)) {
			$this->changePasswordWithoutOldPassword($newPassword);
		}
		else {
			throw new InvalidPasswordException();
		}
	}

	/**
	 * Изменяет пароль текущего авторизованного пользователя, не требуя старый пароль для проверки
	 * Changes the currently signed-in user's password without requiring the old password for verification
	 *
	 * @param string $newPassword the new password that should be set
	 * @throws NotLoggedInException if the user is not currently signed in
	 * @throws ValidatePasswordException if the desired new password has been invalid
	 * @throws AuthError if an internal problem occurred (do *not* catch)
	 */
	public function changePasswordWithoutOldPassword($newPassword)
	{
		if ($this->isLoggedIn()) {
			$newPassword = self::validatePassword($newPassword);
			$this->updatePasswordInternal($this->getUserId(), $newPassword);

			try {
				$this->logOutEverywhereElse();
			}
			catch (NotLoggedInException $ignored) {}
		}
		else {
			throw new NotLoggedInException();
		}
	}

	/**
	 * Попытки изменить адрес электронной почты текущего пользователя, вошедшего в систему (что требует подтверждения)
	 * Функция обратного вызова должна иметь следующую подпись:`function ($selector, $token)`
	 * Обе части информации должны быть отправлены пользователю, как правило, встроены в ссылку.
	 * Когда пользователь хочет подтвердить свой адрес электронной почты в качестве следующего шага, снова потребуются обе части.
	 *
	 * @param string $newEmail the desired new email address
	 * @param callable $callback the function that sends the confirmation email to the user
	 * @throws InvalidEmailException if the desired new email address is invalid
	 * @throws UserAlreadyExistsException if a user with the desired new email address already exists
	 * @throws EmailNotVerifiedException if the current (old) email address has not been verified yet
	 * @throws NotLoggedInException if the user is not currently signed in
	 * @throws TooManyRequestsException if the number of allowed attempts/requests has been exceeded
	 * @throws AuthError if an internal problem occurred (do *not* catch)
	 *
	 * @see confirmEmail
	 * @see confirmEmailAndSignIn
	 */
	public function changeEmail($newEmail, callable $callback)
	{
		if ($this->isLoggedIn()) {
			$newEmail = ValidateEmail::fromString($newEmail);

			$this->throttle(['enumerateUsers', $this->getIpAddress()], 1, (60 * 60), 75);

			try {
                $existingUsersWithNewEmail = connect($this->table)->count('*', 'count')->where('email', $newEmail)->get();
			}
			catch (Error $e) {
				throw new DatabaseError($e->getMessage());
			}

			if ((int) $existingUsersWithNewEmail->count !== 0) {
				throw new UserAlreadyExistsException();
			}

			try {
                $user = connect($this->table)->select('verified')->where('userId', $this->getUserId())->get();
			}
			catch (Error $e) {
				throw new DatabaseError($e->getMessage());
			}

			// ensure that at least the current (old) email address has been verified before proceeding
			if ((int) $user->verified !== 1) {
				throw new EmailNotVerifiedException();
			}

			$this->throttle(['requestEmailChange', 'userId', $this->getUserId()], 1, (60 * 60 * 24));
			$this->throttle(['requestEmailChange', $this->getIpAddress()], 1, (60 * 60 * 24), 3);

			$this->createConfirmationRequest($this->getUserId(), $newEmail, $callback);
		}
		else {
			throw new NotLoggedInException();
		}
	}

	/**
	 * Попытки повторно отправить более ранний запрос подтверждения для пользователя с указанным адресом электронной почты
	 * Функция обратного вызова должна иметь следующую подпись: `function ($selector, $token)`
	 * Обе части информации должны быть отправлены пользователю, как правило, встроены в ссылку.
	 * Когда пользователь хочет подтвердить свой адрес электронной почты в качестве следующего шага, снова потребуются обе части.
	 *
	 * @param string $email the email address of the user to re-send the confirmation request for
	 * @param callable $callback the function that sends the confirmation request to the user
	 * @throws ConfirmationRequestNotFound if no previous request has been found that could be re-sent
	 * @throws TooManyRequestsException if the number of allowed attempts/requests has been exceeded
	 */
	public function resendConfirmationForEmail($email, callable $callback)
	{
		$this->throttle(['enumerateUsers', $this->getIpAddress()], 1, (60 * 60), 75);

		$this->resendConfirmationForColumnValue('email', $email, $callback);
	}

	/**
	 * Попытки повторно отправить более ранний запрос подтверждения для пользователя с указанным идентификатором
	 * Функция обратного вызова должна иметь следующую подпись: `function ($selector, $token)`
	 * Обе части информации должны быть отправлены пользователю, обычно встроенные в ссылку.
	 * Когда пользователь хочет подтвердить свой адрес электронной почты в качестве следующего шага, обе части потребуются снова.
	 *
	 * @param int $userId the ID of the user to re-send the confirmation request for
	 * @param callable $callback the function that sends the confirmation request to the user
	 * @throws ConfirmationRequestNotFound if no previous request has been found that could be re-sent
	 * @throws TooManyRequestsException if the number of allowed attempts/requests has been exceeded
	 */
	public function resendConfirmationForUserId($userId, callable $callback)
    {
		$this->resendConfirmationForColumnValue('user_id', $userId, $callback);
	}

	/**
	 * Попытки повторно отправить более ранний запрос подтверждения
	 * Функция обратного вызова должна иметь следующую подпись: `function ($selector, $token)`
	 * Обе части информации должны быть отправлены пользователю, обычно встроенные в ссылку.
	 * Когда пользователь хочет подтвердить свой адрес электронной почты в качестве следующего шага, обе части потребуются снова.
	 *
	 * Вы никогда не должны передавать ненадежный ввод в параметр, который принимает имя столбца.
	 *
	 * @param string $columnName the name of the column to filter by
	 * @param mixed $columnValue the value to look for in the selected column
	 * @param callable $callback the function that sends the confirmation request to the user
	 * @throws ConfirmationRequestNotFound if no previous request has been found that could be re-sent
	 * @throws TooManyRequestsException if the number of allowed attempts/requests has been exceeded
	 * @throws AuthError if an internal problem occurred (do *not* catch)
	 */
	private function resendConfirmationForColumnValue($columnName, $columnValue, callable $callback)
    {
		try {
            $latestAttempt = connect()->table('users_confirmations')->select('user_id, email')->where($columnName,'=', $columnValue)->orderBy('id')->get('array');
		}
		catch (Error $e) {
			throw new DatabaseError($e->getMessage());
		}

		if ($latestAttempt === null) {
			throw new ConfirmationRequestNotFound();
		}

		$this->throttle(['resendConfirmation', 'userId', $latestAttempt['user_id']], 1, (60 * 60 * 6));
		$this->throttle(['resendConfirmation', $this->getIpAddress()], 4, (60 * 60 * 24 * 7), 2);

		$this->createConfirmationRequest($latestAttempt['user_id'], $latestAttempt['email'], $callback);
	}

    /**
     * Инициирует запрос на сброс пароля для пользователя с указанным `email`.
     * Функция обратного вызова должна иметь следующую подпись: `function ($selector, $token)`
     * Обе части информации должны быть отправлены пользователю, обычно встроенные в ссылку.
     * Когда пользователь захочет перейти ко второму шагу сброса пароля, снова потребуются обе части.
     *
     * @param string $email the email address of the user who wants to request the password reset
     * @param callable $callback the function that sends the password reset information to the user
     * @param int|null $requestExpiresAfter (optional) the interval in seconds after which the request should expire
     * @param int|null $maxOpenRequests (optional) the maximum number of unexpired and unused requests per user
     * @throws InvalidEmailException if the email address was invalid or could not be found
     * @throws EmailNotVerifiedException if the email address has not been verified yet via confirmation email
     * @throws ResetDisabledException if the user has explicitly disabled password resets for their account
     * @throws TooManyRequestsException if the number of allowed attempts/requests has been exceeded
     * @throws AuthError if an internal problem occurred (do *not* catch)
     * @throws EmailNotPaidException
     *
     * @see canResetPasswordOrThrow
     * @see canResetPassword
     * @see resetPassword
     * @see resetPasswordAndSignIn
     */
	public function forgotPassword($email, callable $callback, $requestExpiresAfter = null, $maxOpenRequests = null)
    {
		$email = ValidateEmail::fromString($email);

		$this->throttle(['enumerateUsers', $this->getIpAddress()], 1, (60 * 60), 75);

        $requestExpiresAfter = ($requestExpiresAfter === null) ? 60 * 60 * 6 : (int) $requestExpiresAfter;
        $maxOpenRequests = ($maxOpenRequests === null) ? 2 : (int) $maxOpenRequests;

		$userData = $this->getUserDataBy('email', $email, ['userId', 'verified', 'resettable', 'paid']);

		// убедитесь, что учетная запись была проверена, прежде чем начинать сброс пароля
		if ((int) $userData['verified'] !== 1) {
			throw new EmailNotVerifiedException();
		}

		// не разрешать сброс пароля, если пользователь явно отключил эту функцию
		if ((int) $userData['resettable'] !== 1) {
			throw new ResetDisabledException();
		}

//        if ((int) $userData['paid'] !== 1) {
//            throw new EmailNotPaidException();
//        }

		$openRequests = $this->throttling ? (int) $this->getOpenPasswordResetRequests($userData['userId']) : 0;

		if ($openRequests < $maxOpenRequests) {
			$this->throttle(['requestPasswordReset', $this->getIpAddress()], 4, (60 * 60 * 24 * 7), 2);
			$this->throttle(['requestPasswordReset', 'user', $userData['userId']], 4, (60 * 60 * 24 * 7), 2);

			$this->createPasswordResetRequest($userData['userId'], $requestExpiresAfter, $callback);
		} else {
			throw new TooManyRequestsException('', $requestExpiresAfter);
		}
	}

    /**
     * Инициирует запрос на сброс пароля для пользователя с указанным `email`.
     * Функция обратного вызова должна иметь следующую подпись: `function ($selector, $token)`
     * Обе части информации должны быть отправлены пользователю, обычно встроенные в ссылку.
     * Когда пользователь захочет перейти ко второму шагу сброса пароля, снова потребуются обе части.
     *
     * @param string $email the email address of the user who wants to request the password reset
     * @param callable $callback the function that sends the password reset information to the user
     * @param int|null $requestExpiresAfter (optional) the interval in seconds after which the request should expire
     * @param int|null $maxOpenRequests (optional) the maximum number of unexpired and unused requests per user
     * @throws InvalidEmailException if the email address was invalid or could not be found
     * @throws EmailNotVerifiedException if the email address has not been verified yet via confirmation email
     * @throws ResetDisabledException if the user has explicitly disabled password resets for their account
     * @throws TooManyRequestsException if the number of allowed attempts/requests has been exceeded
     * @throws AuthError if an internal problem occurred (do *not* catch)
     * @throws EmailNotPaidException
     *
     * @see canResetPasswordOrThrow
     * @see canResetPassword
     * @see resetPassword
     * @see resetPasswordAndSignIn
     */
    public function recoveryPassword($email, callable $callback, $requestExpiresAfter = null, $maxOpenRequests = null)
    {
        $email = ValidateEmail::fromString($email);

        $this->throttle(['enumerateUsers', $this->getIpAddress()], 1, (60 * 60), 75);

        if ($requestExpiresAfter === null) {
            // use six hours as the default
            $requestExpiresAfter = 60 * 60 * 6;
        }
        else {
            $requestExpiresAfter = (int) $requestExpiresAfter;
        }

        if ($maxOpenRequests === null) {
            // use two requests per user as the default
            $maxOpenRequests = 2;
        }
        else {
            $maxOpenRequests = (int) $maxOpenRequests;
        }

        $userData = $this->getUserDataBy('email', $email, ['userId', 'verified', 'resettable', 'paid']);

        // ensure that the account has been verified before initiating a password reset
        if ((int) $userData['verified'] !== 1) {
            throw new EmailNotVerifiedException();
        }

        // do not allow a password reset if the user has explicitly disabled this feature
        if ((int) $userData['resettable'] !== 1) {
            throw new ResetDisabledException();
        }
//        if ((int) $userData['paid'] !== 1) {
//            throw new EmailNotPaidException();
//        }

        $openRequests = $this->throttling ? (int) $this->getOpenPasswordResetRequests($userData['userId']) : 0;

        if ($openRequests < $maxOpenRequests) {
            $this->throttle(['requestPasswordReset', $this->getIpAddress()], 4, (60 * 60 * 24 * 7), 2);
            $this->throttle(['requestPasswordReset', 'user', $userData['userId']], 4, (60 * 60 * 24 * 7), 2);

            $this->updatePassword($userData['userId'], $callback);
        }
        else {
            throw new TooManyRequestsException('', $requestExpiresAfter);
        }
    }

    /**
     * Аутентифицирует существующего пользователя по `email` or `loginName`
     *
     * @param string $password the user's password
     * @param string|null $email (optional) the user's email address
     * @param string|null $loginName (optional) the user's loginName
     * @param int|null $rememberDuration (optional) the duration in seconds to keep the user logged in ("remember me"), e.g. `60 * 60 * 24 * 365.25` for one year
     * @param callable|null $onBeforeSuccess (optional) a function that receives the user's ID as its single parameter and is executed before successful authentication; must return `true` to proceed or `false` to cancel
     * @throws InvalidEmailException if the email address was invalid or could not be found
     * @throws UnknownUsernameException if an attempt has been made to authenticate with a non-existing loginName
     * @throws AmbiguousUsernameException if an attempt has been made to authenticate with an ambiguous loginName
     * @throws InvalidPasswordException if the password was invalid
     * @throws ValidatePasswordException if the password was invalid
     * @throws EmailNotVerifiedException if the email address has not been verified yet via confirmation email
     * @throws AttemptCancelledException if the attempt has been cancelled by the supplied callback that is executed before success
     * @throws TooManyRequestsException if the number of allowed attempts/requests has been exceeded
     * @throws AuthError if an internal problem occurred (do *not* catch)
     * @throws EmailNotPaidException
     */
	private function authenticateUserInternal($password, $email = null, $loginName = null, $rememberDuration = null, callable $onBeforeSuccess = null)
    {
		$this->throttle(['enumerateUsers', $this->getIpAddress()], 1000, (60 * 60), 75);
		$this->throttle(['attemptToLogin', $this->getIpAddress()], 4000, (60 * 60), 500, true);

		$columnsToFetch = [ 'userId', 'fullName', 'firstName', 'lastName', 'email', 'password', 'verified', 'paid', 'loginName', 'status', 'accessLevel', 'force_logout'];
        $roles          = [UserTypes::CUSTOMER];

		if ($email !== null) {
			$email = ValidateEmail::fromString($email);
			// найти информацию об учетной записи, используя указанный email
			$userData = $this->getUserDataBy('email', $email, $columnsToFetch);
		} elseif ($loginName !== null) {
            $loginName = \trim($loginName);
			// найти информацию об учетной записи, используя указанное loginName
			$userData = $this->getUserDataBy('loginName', $loginName, $columnsToFetch);
		}
		// если ни адрес электронной почты, ни имя пользователя не были предоставлены
		else {
			// мы не можем здесь ничего сделать, потому что вызов метода был недопустимым
			throw new EmailOrUsernameRequiredError();
		}

		$password = self::validatePassword($password);

		if (\password_verify($password, $userData['password'])) {
			// если пароль необходимо повторно хешировать, чтобы идти в ногу с совершенствованием методов взлома паролей
			if (\password_needs_rehash($userData['password'], \PASSWORD_DEFAULT)) {
				// создать новый хеш из пароля и обновить его в базе данных
				$this->updatePasswordInternal($userData['userId'], $password);
			}

			if ((int) $userData['verified'] === 1) {
			    // функция, которая получает идентификатор пользователя в качестве единственного параметра и выполняется до успешной аутентификации; должен вернуть true для продолжения или false для отмены
				if (!isset($onBeforeSuccess) || (\is_callable($onBeforeSuccess) && $onBeforeSuccess($userData['userId']) === true)) {

					$this->onLoginUserSuccessful($userData['userId'], $userData['fullName'], $userData['firstName'], $userData['lastName'], $userData['email'], $userData['loginName'], $userData['status'], $userData['accessLevel'], $userData['force_logout'], false);

					// продолжать поддерживать старый формат параметров
					if ($rememberDuration === true) {
						$rememberDuration = 60 * 60 * 24 * 28;
					} elseif ($rememberDuration === false) {
						$rememberDuration = null;
					}

					if ($rememberDuration !== null) {
						$this->createRememberDirective($userData['userId'], $rememberDuration);
					}

					return;
				}
				else {
					$this->throttle(['attemptToLogin', $this->getIpAddress()], 400, (60 * 60), 5, false);

					if (isset($email)) {
						$this->throttle(['attemptToLogin', 'email', $email], 500, (60 * 60 * 24), null, false);
					} elseif (isset($loginName)) {
						$this->throttle(['attemptToLogin', 'loginName', $loginName], 500, (60 * 60 * 24), null, false);
					}

					throw new AttemptCancelledException();
				}
			}
			else {
				throw new EmailNotVerifiedException();
			}
		}
		else {
			$this->throttle(['attemptToLogin', $this->getIpAddress()], 4, (60 * 60), 5, false);

			if (isset($email)) {
				$this->throttle(['attemptToLogin', 'email', $email], 500, (60 * 60 * 24), null, false);
			}
			elseif (isset($loginName)) {
				$this->throttle(['attemptToLogin', 'loginName', $loginName], 500, (60 * 60 * 24), null, false);
			}

			// мы не можем аутентифицировать пользователя из-за неправильного пароля
			throw new InvalidPasswordException();
		}
	}

    /**
     * Аутентифицирует существующего пользователя по номеру телефона
     *
     * @param string $password пароль
     * @param string|null $phone номер телефона
     * @param null $confirm подтверждение
     * @param int|null $rememberDuration запомнить меня
     * @param callable|null $onBeforeSuccess callback
     *
     * @throws AttemptCancelledException если попытка была отменена предоставленным обратным вызовом, который выполняется до успешного завершения
     * @throws InvalidPasswordException если пароль был неверным
     * @throws ValidatePasswordException если пароль был неверным
     * @throws InvalidPhoneException если телефон был недействителен или его не удалось найти
     * @throws PhoneNotVerifiedException если номер телефона еще не подтвержден
     * @throws TooManyRequestsException если количество разрешенных попыток/запросов было превышено
     * @throws UnknownIdException если пользователь с указанным ID не найден
     * @throws UserPhoneRequiredError пустой номер телефона
     * @throws IntegrityConstraintViolationException Ваш ip заблокирован из-за нарушений авторизации
     * @throws AuthError если возникла внутренняя проблема (do *not* catch)
     */
    private function authenticateExistingUserByPhone($password, $phone = null, $confirm = null, $rememberDuration = null, callable $onBeforeSuccess = null)
    {
        $this->throttle(['enumerateUsers', $this->getIpAddress()], 1000, (60 * 60), 75);
        $this->throttle(['attemptToPhone', $this->getIpAddress()], 4000, (60 * 60), 500, true);

        $columnsToFetch = [ 'userId', 'fullName', 'firstName', 'lastName', 'phone', 'email', 'password', 'verified', 'paid', 'loginName', 'status', 'accessLevel', 'force_logout'];

        if ($phone !== null) {
            // получение данных пользователя по номеру телефона
            $userData = $this->getUserDataByPhoneNumber(\trim($phone), $columnsToFetch);

//            print_r($userData);
        } else {
            throw new UserPhoneRequiredError();
        }

        $password = self::validatePassword($password);
        if (\password_verify($password, $userData['password'])) {
            if (\password_needs_rehash($userData['password'], \PASSWORD_DEFAULT)) {
                // создать новый хеш из пароля и обновить его в базе данных
                $this->updatePasswordInternal($userData['userId'], $password);
            }
            // был ли подтвержден номер телефона
            if ((int) $userData['verified'] === 1) {

                if ($confirm) {

                    $this->throttle(['attemptToPhone', $this->getIpAddress()], 400, (60 * 60), 5, false);
                    $this->throttle(['attemptToPhone', 'phone', $userData['phone']], 500, (60 * 60 * 24), null, false);

                    $this->createConfirmationRequestSms($userData['userId'], $userData['phone'], null, $onBeforeSuccess);

                }
                // функция, которая получает идентификатор пользователя в качестве единственного параметра и выполняется до успешной аутентификации; должен вернуть true для продолжения или false для отмены
                else if (!isset($onBeforeSuccess) || (\is_callable($onBeforeSuccess) && $onBeforeSuccess($userData['userId']) === true)) {

                    $this->onLoginUserSuccessful($userData['userId'], $userData['fullName'], $userData['firstName'], $userData['lastName'], $userData['email'], $userData['loginName'], $userData['status'], $userData['accessLevel'], $userData['force_logout'], false);

                    // продолжать поддерживать старый формат параметров
                    if ($rememberDuration === true) {
                        $rememberDuration = 60 * 60 * 24 * 28;
                    } elseif ($rememberDuration === false) {
                        $rememberDuration = null;
                    }

                    if ($rememberDuration !== null) {
                        $this->createRememberDirective($userData['userId'], $rememberDuration);
                    }

                    return;
                }
                // авторизация отменена
                else {
                    $this->throttle(['attemptToPhone', $this->getIpAddress()], 400, (60 * 60), 5, false);
                    $this->throttle(['attemptToPhone', 'phone', $userData['phone']], 500, (60 * 60 * 24), null, false);

                    throw new AttemptCancelledException();
                }

            }
            else {
                throw new PhoneNotVerifiedException();
            }
        }
        else {
            $this->throttle(['attemptToLogin', $this->getIpAddress()], 4, (60 * 60), 5, false);
            if (isset($userData['phone'])) {
                $this->throttle(['attemptToPhone', 'phone', $userData['phone']], 500, (60 * 60 * 24), null, false);
            }
            // мы не можем аутентифицировать пользователя из-за неправильного пароля
            throw new InvalidPasswordException();
        }
    }



	/** работа с номером телефона */

    /**
     * Подтверждает номер телефона (и активирует учетную запись), указав правильный селектор
     * Селектор должен быть сгенерирована ранее при регистрации новой учетной записи.
     *
     * @param string $selector the selector from the selector/token pair
     * @return string[] an array with the old email address (if any) at index zero and the new email address (which has just been verified) at index one
     * @throws InvalidSelectorTokenPairException if either the selector or the token was not correct
     * @throws TokenExpiredException if the token has already expired
     * @throws UserAlreadyExistsException if an attempt has been made to change the email address to a (now) occupied address
     * @throws TooManyRequestsException if the number of allowed attempts/requests has been exceeded
     * @throws AuthError if an internal problem occurred (do *not* catch)
     */
    public function confirmPhone($selector)
    {
        $this->throttle(['confirmPhone', $this->getIpAddress()], 5, (60 * 60), 10);
        $this->throttle(['confirmPhone', 'selector', $selector], 3, (60 * 60), 10);

        try {
            $confirmationData = connect()->table('users_confirmations AS uc')->join('users AS u', 'u.userId', '=', 'uc.user_id')
                ->select('uc.id, uc.user_id, uc.email AS new_phone, uc.token, uc.expires, u.phone AS old_phone')
                ->where('uc.selector', $selector)->get('array');
        }
        catch (Error $e) {
            throw new DatabaseError($e->getMessage());
        }

        if (!empty($confirmationData)) {
            if (\password_verify($selector, $confirmationData['token'])) {
                if ($confirmationData['expires'] >= \time()) {
                    // аннулировать любые потенциальные невыполненные запросы на сброс пароля
                    try {
                        connect('users_resets')->where('user',$confirmationData['user_id'])->delete();
                    } catch (Error $e) {
                        throw new DatabaseError($e->getMessage());
                    }

                    // отметьте `phone` как подтвержденный (и, возможно, обновите его на новый указанный `phone`)
                    try {
                        connect($this->table)->where('userId', $confirmationData['user_id'])->update([
                            'loginName' => $confirmationData['new_phone'],
                            'phone'     => $confirmationData['new_phone'],
                            'verified'  => 1,
                            'status'    => 1
                        ]);
                    }
                    catch (IntegrityConstraintViolationException $e) {
                        throw new UserAlreadyExistsException();
                    }
                    catch (Error $e) {
                        throw new DatabaseError($e->getMessage());
                    }

                    // если пользователь в настоящее время вошел в систему
                    if ($this->isLoggedIn()) {
                        // если пользователь только что подтвердил номер телефона для своей учетной записи
                        if ($this->getUserId() === $confirmationData['user_id']) {
                            // немедленно обновить `phone` в текущем сеансе
                            $_SESSION['siteUser'][self::SESSION_USER_PHONE] = $confirmationData['new_phone'];
                        }
                    }

                    // использовать токен, который только что использовался для подтверждения
                    try {
                        connect('users_confirmations')->where('id', $confirmationData['id'])->delete();
                    }
                    catch (Error $e) {
                        throw new DatabaseError($e->getMessage());
                    }

                    // если `phone` не менялся, а просто был подтвержден
                    if ($confirmationData['old_phone'] === $confirmationData['new_phone']) {
                        // вывод не должен содержать предыдущий `phone`
                        $confirmationData['old_phone'] = null;
                    }

                    return [
                        $confirmationData['old_phone'],
                        $confirmationData['new_phone']
                    ];
                }
                else {
                    throw new TokenExpiredException();
                }
            }
            else {
                throw new InvalidSelectorTokenPairException();
            }
        }
        else {
            throw new InvalidSelectorTokenPairException();
        }
    }

    /**
     * Подтверждает адрес электронной почты и активирует учетную запись, указав правильный селектор
     * Селектор должен быть сгенерирована ранее при регистрации новой учетной записи.
     * Пользователь будет автоматически авторизован, если эта операция прошла успешно.
     *
     * @param string $selector the selector from the selector/token pair
     * @param int|null $rememberDuration (optional) the duration in seconds to keep the user logged in ("remember me"), e.g. `60 * 60 * 24 * 365.25` for one year
     * @return string[] an array with the old email address (if any) at index zero and the new email address (which has just been verified) at index one
     * @throws InvalidSelectorTokenPairException if either the selector or the token was not correct
     * @throws UserAlreadyExistsException if an attempt has been made to change the email address to a (now) occupied address
     * @throws TooManyRequestsException if the number of allowed attempts/requests has been exceeded
     * @throws TokenExpiredException if the number of allowed attempts/requests has been exceeded
     * @throws AuthError if an internal problem occurred (do *not* catch)
     */
    public function confirmPhoneAndSignIn($selector, $rememberDuration = null)
    {
        $phoneBeforeAndAfter = $this->confirmPhone($selector);

        if (!$this->isLoggedIn()) {
            if ($phoneBeforeAndAfter[1] !== null) {
                $phoneBeforeAndAfter[1] = ValidatePhone::fromString($phoneBeforeAndAfter[1]);

                $userData = $this->getUserDataByPhoneNumber($phoneBeforeAndAfter[1], ['userId', 'email', 'fullName', 'firstName', 'lastName', 'loginName', 'status', 'accessLevel', 'force_logout']);

                $this->onLoginUserSuccessful($userData['userId'], $userData['fullName'], $userData['firstName'], $userData['lastName'], $userData['email'], $userData['loginName'], $userData['status'], $userData['accessLevel'], $userData['force_logout'], true);

                if ($rememberDuration !== null) {
                    $this->createRememberDirective($userData['userId'], $rememberDuration);
                }
            }
        }

        return $phoneBeforeAndAfter;
    }

    /**
     * Инициирует запрос на сброс пароля для пользователя с указанным `phone` номером телефона.
     * Функция обратного вызова должна иметь следующую подпись: `function ($selector, $token)`
     * Обе части информации должны быть отправлены пользователю, обычно встроенные в ссылку.
     * Когда пользователь захочет перейти ко второму шагу сброса пароля, снова потребуются обе части.
     *
     * @param string $phone `phone` пользователя, который хочет запросить сброс пароля
     * @param callable $callback функция, которая отправляет пользователю информацию о сбросе пароля
     * @param int|null $requestExpiresAfter (optional) интервал в секундах, по истечении которого запрос должен истечь
     * @param int|null $maxOpenRequests (optional) максимальное количество неиспользованных и неиспользованных запросов на одного пользователя
     * @throws PhoneNotVerifiedException если `phone` еще не был подтвержден с помощью `phone` подтверждения
     * @throws ResetDisabledException если пользователь явно отключил сброс пароля для своей учетной записи
     * @throws TooManyRequestsException если количество разрешенных попыток/запросов было превышено
     * @throws AuthError если возникла внутренняя проблема (do *not* catch)
     * @throws PhoneNotPaidException|InvalidPhoneException
     *
     * @see canResetPasswordOrThrow
     * @see canResetPassword
     * @see resetPassword
     * @see resetPasswordAndSignIn
     */
    public function forgotPhonePassword($phone, callable $callback, $requestExpiresAfter = null, $maxOpenRequests = null)
    {
        $phone = ValidatePhone::fromString($phone);

        $this->throttle(['enumerateUsers', $this->getIpAddress()], 1, (60 * 60), 75);

        $requestExpiresAfter = ($requestExpiresAfter === null) ? 60 * 60 * 6 : (int) $requestExpiresAfter;
        $maxOpenRequests = ($maxOpenRequests === null) ? 2 : (int) $maxOpenRequests;

        $userData = $this->getUserDataByPhoneNumber( $phone, ['userId', 'phone', 'verified', 'resettable', 'paid']);

        // убедитесь, что учетная запись была проверена, прежде чем начинать сброс пароля
        if ((int) $userData['verified'] !== 1) {
            throw new PhoneNotVerifiedException();
        }

        // не разрешать сброс пароля, если пользователь явно отключил эту функцию
        if ((int) $userData['resettable'] !== 1) {
            throw new ResetDisabledException();
        }

        $openRequests = $this->throttling ? (int) $this->getOpenPasswordResetRequests($userData['userId']) : 0;

        if ($openRequests < $maxOpenRequests) {
            $this->throttle(['requestPasswordReset', $this->getIpAddress()], 4, (60 * 60 * 24 * 7), 2);
            $this->throttle(['requestPasswordReset', 'user', $userData['userId']], 4, (60 * 60 * 24 * 7), 2);

            $this->createConfirmationRequestSms($userData['userId'], $userData['phone'], $requestExpiresAfter, $callback);

        } else {
            throw new TooManyRequestsException('', $requestExpiresAfter);
        }
    }

    /**
     * Инициирует запрос на сброс пароля для пользователя с указанным `phone` номером телефона
     * Функция обратного вызова должна иметь следующую подпись: `function ($password, $phone)`
     * Обе части информации должны быть отправлены пользователю, обычно встроенные в ссылку.
     * Когда пользователь захочет перейти ко второму шагу сброса пароля, снова потребуются обе части.
     *
     * @param string $phone the email address of the user who wants to request the password reset
     * @param callable $callback the function that sends the password reset information to the user
     * @param int|null $requestExpiresAfter (optional) the interval in seconds after which the request should expire
     * @param int|null $maxOpenRequests (optional) the maximum number of unexpired and unused requests per user
     * @throws InvalidEmailException if the email address was invalid or could not be found
     * @throws EmailNotVerifiedException if the email address has not been verified yet via confirmation email
     * @throws ResetDisabledException if the user has explicitly disabled password resets for their account
     * @throws TooManyRequestsException if the number of allowed attempts/requests has been exceeded
     * @throws AuthError if an internal problem occurred (do *not* catch)
     * @throws EmailNotPaidException
     *
     * @see canResetPasswordOrThrow
     * @see canResetPassword
     * @see resetPassword
     * @see resetPasswordAndSignIn
     */
    public function recoveryPhoneGenerationPassword($phone, callable $callback, $requestExpiresAfter = null, $maxOpenRequests = null)
    {
        $phone = ValidatePhone::fromString($phone);

        $this->throttle(['enumerateUsers', $this->getIpAddress()], 1, (60 * 60), 75);

        $requestExpiresAfter = ($requestExpiresAfter === null) ? 60 * 60 * 6 : (int) $requestExpiresAfter;
        $maxOpenRequests = ($maxOpenRequests === null) ? 2 : (int) $maxOpenRequests;
        $userData = $this->getUserDataByPhoneNumber($phone, ['userId', 'phone', 'verified', 'resettable']);

        // убедитесь, что учетная запись была проверена, прежде чем начинать сброс пароля
        if ((int) $userData['verified'] !== 1) {
            throw new EmailNotVerifiedException();
        }

        // не разрешать сброс пароля, если пользователь явно отключил эту функцию
        if ((int) $userData['resettable'] !== 1) {
            throw new ResetDisabledException();
        }

        $openRequests = $this->throttling ? (int) $this->getOpenPasswordResetRequests($userData['userId']) : 0;

        if ($openRequests < $maxOpenRequests) {
            $this->throttle(['requestPasswordReset', $this->getIpAddress()], 4, (60 * 60 * 24 * 7), 2);
            $this->throttle(['requestPasswordReset', 'user', $userData['userId']], 4, (60 * 60 * 24 * 7), 2);

            $this->updatePhoneGenerationPassword($userData['phone'], $userData['userId'], $callback);
        }
        else {
            throw new TooManyRequestsException('', $requestExpiresAfter);
        }
    }

    /**
     * Создает новый запрос на сброс пароля
     * Функция обратного вызова должна иметь следующую подпись:`function ($password, $phone)`
     * Обе части информации должны быть отправлены пользователю, как правило, встроены в ссылку.
     * Когда пользователь хочет перейти ко второму этапу сброса пароля, снова потребуются обе части.
     *
     * @param int $userId the ID of the user who requested the reset
     * @param callable $callback the function that sends the password reset information to the user
     * @throws AuthError if an internal problem occurred (do *not* catch)
     */
    private function updatePhoneGenerationPassword($phone, $userId, callable $callback)
    {
        $password = self::generateRandomString(  8);
        $userData['password'] = \password_hash($password, \PASSWORD_DEFAULT);

        try {
            connect($this->table)->where('userId', $userId)->update(['password' => $userData['password'], 'code' => $password]);
        }
        catch (Error $e) {
            throw new DatabaseError($e->getMessage());
        }

        if (\is_callable($callback)) {
            $callback($password, $phone);
        }
        else {
            throw new MissingCallbackError();
        }
    }


    /**
     * Инициирует запрос на сброс пароля для пользователя с указанным `phone` номером телефона
     * Функция обратного вызова должна иметь следующую подпись: `function ($password, $phone)`
     * Обе части информации должны быть отправлены пользователю, обычно встроенные в ссылку.
     * Когда пользователь захочет перейти ко второму шагу сброса пароля, снова потребуются обе части.
     *
     * @param string $phone the phone of the user who wants to request the password reset
     * @param string $password
     * @param callable $callback the function that sends the password reset information to the user
     * @param int|null $requestExpiresAfter (optional) the interval in seconds after which the request should expire
     * @param int|null $maxOpenRequests (optional) the maximum number of unexpired and unused requests per user
     * @throws AuthError
     * @throws InvalidPhoneException
     * @throws PhoneNotVerifiedException
     * @throws ResetDisabledException
     * @throws TooManyRequestsException
     */
    public function recoveryPhonePassword($phone, $password, callable $callback, $requestExpiresAfter = null, $maxOpenRequests = null)
    {
        $phone = ValidatePhone::fromString($phone);

        $this->throttle(['enumerateUsers', $this->getIpAddress()], 1, (60 * 60), 75);

        $requestExpiresAfter = ($requestExpiresAfter === null) ? 60 * 60 * 6 : (int) $requestExpiresAfter;
        $maxOpenRequests = ($maxOpenRequests === null) ? 2 : (int) $maxOpenRequests;
        $userData = $this->getUserDataByPhoneNumber($phone, ['userId', 'phone', 'verified', 'resettable']);

        // убедитесь, что учетная запись была проверена, прежде чем начинать сброс пароля
        if ((int) $userData['verified'] !== 1) {
            throw new PhoneNotVerifiedException();
        }

        // не разрешать сброс пароля, если пользователь явно отключил эту функцию
        if ((int) $userData['resettable'] !== 1) {
            throw new ResetDisabledException();
        }

        $openRequests = $this->throttling ? (int) $this->getOpenPasswordResetRequests($userData['userId']) : 0;

        if ($openRequests < $maxOpenRequests) {
            $this->throttle(['requestPasswordReset', $this->getIpAddress()], 4, (60 * 60 * 24 * 7), 2);
            $this->throttle(['requestPasswordReset', 'user', $userData['userId']], 4, (60 * 60 * 24 * 7), 2);

            $this->updatePhonePassword($userData['phone'], $password, $userData['userId'], $callback);
        }
        else {
            throw new TooManyRequestsException('', $requestExpiresAfter);
        }
    }

    /**
     * Создает новый запрос на сброс пароля
     * Функция обратного вызова должна иметь следующую подпись:`function ($password, $phone)`
     * Обе части информации должны быть отправлены пользователю, как правило, встроены в ссылку.
     * Когда пользователь хочет перейти ко второму этапу сброса пароля, снова потребуются обе части.
     *
     * @param int $userId the ID of the user who requested the reset
     * @param callable $callback the function that sends the password reset information to the user
     * @throws AuthError if an internal problem occurred (do *not* catch)
     */
    private function updatePhonePassword($phone, $password, $userId, callable $callback)
    {
        $userData['password'] = \password_hash($password, \PASSWORD_DEFAULT);

        try {
            connect($this->table)->where('userId', $userId)->update(['password' => $userData['password'], 'code' => $password]);
        }
        catch (Error $e) {
            throw new DatabaseError($e->getMessage());
        }

        if (\is_callable($callback)) {
            $callback($phone, $password);
        }
        else {
            throw new MissingCallbackError();
        }
    }


    /**
     * Возвращает запрошенные данные пользователя для учетной записи с указанным полю (если есть)
     * Вы никогда не должны передавать ненадежный ввод в параметр, который принимает список столбцов.
     *
     * @param $field
     * @param $unique
     * @param array $requestedColumns the columns to request from the user's record
     * @return array the user data (if an account was found)
     * @throws InvalidEmailException if the email address could not be found
     * @throws AuthError if an internal problem occurred (do *not* catch)
     */
    private function getUserDataBy($field, $unique, array $requestedColumns) {
        try {
            $projection = \implode(', ', $requestedColumns);
            $userData = connect($this->table)->select($projection)->where($field, $unique)->get('array');
        }
        catch (Error $e) {
            throw new DatabaseError($e->getMessage());
        }

        if (!empty($userData)) {
            return $userData;
        }
        else {
            throw new InvalidEmailException();
        }
    }

    /**
     * Возвращает запрошенные данные пользователя для учетной записи с указанным номеру телефона
     * Вы никогда не должны передавать ненадежный ввод в параметр, который принимает список столбцов.
     *
     * @param $phoneNumber
     * @param array $requestedColumns столбцы для запроса из записи пользователя
     * @return array данные пользователя (если учетная запись была найдена)
     * @throws InvalidPhoneException если `phone` не удалось найти
     * @throws AuthError если возникла внутренняя проблема (do *not* catch)
     */
    private function getUserDataByPhoneNumber($phoneNumber, array $requestedColumns) {
        try {
            $projection = \implode(', ', $requestedColumns);
            $userData = connect($this->table)->select($projection)->where('phone', $phoneNumber)->get('array');
        }
        catch (Error $e) {
            throw new DatabaseError($e->getMessage());
        }

        if ( !empty($userData) ) {
            return $userData;
        }
        else {
            throw new InvalidPhoneException();
        }
    }

    /**
     * Возвращает запрошенные данные пользователя для учетной записи с указанным номеру телефона
     * Вы никогда не должны передавать ненадежный ввод в параметр, который принимает список столбцов.
     *
     * @param $phoneNumber
     * @return bool (если учетная запись была найдена)
     * @throws InvalidPhoneException если `phone` не удалось найти
     * @throws AuthError если возникла внутренняя проблема (do *not* catch)
     * @throws PhoneNotVerifiedException
     */
    public function getCheckUserPhoneNumber($phoneNumber) {
        try {

            $userData = connect($this->table)->select('userId, loginName, verified, phone')->where('phone', $phoneNumber)->get('array');
        }
        catch (Error $e) {
            throw new DatabaseError($e->getMessage());
        }

        if ( !empty($userData) ) {
            return $userData;
        }
        else {
            throw new InvalidPhoneException();
        }
    }

	/**
     * Возвращает количество открытых запросов на сброс пароля указанным пользователем.
	 *
	 * @param int $userId the ID of the user to check the requests for
	 * @return int the number of open requests for a password reset
	 * @throws AuthError if an internal problem occurred (do *not* catch)
	 */
	private function getOpenPasswordResetRequests($userId) {
		try {
            $requests = connect()->table('users_resets')->count('*', 'count')->where('user', '=', $userId)->where('expires', '>', \time())->get();

			if (!empty($requests->count)) {
				return $requests->count;
			}
			else {
				return 0;
			}
		}
		catch (Error $e) {
			throw new DatabaseError($e->getMessage());
		}
	}

    /**
     * Создает новый запрос на сброс пароля
     * Функция обратного вызова должна иметь следующую подпись:`function ($password, $userId)`
     * Обе части информации должны быть отправлены пользователю, как правило, встроены в ссылку.
     * Когда пользователь хочет перейти ко второму этапу сброса пароля, снова потребуются обе части.
     *
     * @param int $userId the ID of the user who requested the reset
     * @param callable $callback the function that sends the password reset information to the user
     * @throws AuthError if an internal problem occurred (do *not* catch)
     */
    private function updatePassword($userId, callable $callback)
    {
        $password = self::generateRandomString(  8);
        $userData['password'] = \password_hash($password, \PASSWORD_DEFAULT);

        try {
            connect($this->table)->where('userId', $userId)->update(['password' => $userData['password'], 'code' => $password]);
        }
        catch (Error $e) {
            throw new DatabaseError($e->getMessage());
        }

        if (\is_callable($callback)) {
            $callback($password, $userId);
        }
        else {
            throw new MissingCallbackError();
        }
    }

	/**
     * Создает новый запрос на сброс пароля
     * Функция обратного вызова должна иметь следующую подпись: `function ($selector, $token)`
     * Обе части информации должны быть отправлены пользователю, как правило, встроены в ссылку.
     * Когда пользователь хочет перейти ко второму этапу сброса пароля, снова потребуются обе части.
	 *
	 * @param int $userId the ID of the user who requested the reset
	 * @param int $expiresAfter the interval in seconds after which the request should expire
	 * @param callable $callback the function that sends the password reset information to the user
	 * @throws AuthError if an internal problem occurred (do *not* catch)
	 */
	private function createPasswordResetRequest($userId, $expiresAfter, callable $callback)
    {
		$selector = self::createRandomString(20);
		$token = self::createRandomString(20);
		$tokenHashed = \password_hash($token, \PASSWORD_DEFAULT);
		$expiresAt = \time() + $expiresAfter;

		try {
            connect()->table('users_resets')->insert([
                'user' => $userId,
                'selector' => $selector,
                'token' => $tokenHashed,
                'expires' => $expiresAt
            ]);
		}
		catch (Error $e) {
			throw new DatabaseError($e->getMessage());
		}

		if (\is_callable($callback)) {
			$callback($selector, $token);
		}
		else {
			throw new MissingCallbackError();
		}
	}

	/**
     * Сбрасывает пароль для конкретной учетной записи, указав правильную пару селектор / токен
     * Пара selector/token должна быть сгенерирована ранее с помощью вызова {@see ForgotPassword}.
	 *
	 * @param string $selector the selector from the selector/token pair
	 * @param string $token the token from the selector/token pair
	 * @param string $newPassword the new password to set for the account
	 * @return string[] an array with the user's ID at index `id` and the user's email address at index `email`
	 * @throws InvalidSelectorTokenPairException if either the selector or the token was not correct
	 * @throws TokenExpiredException if the token has already expired
	 * @throws ResetDisabledException if the user has explicitly disabled password resets for their account
	 * @throws InvalidPasswordException if the new password was invalid
	 * @throws TooManyRequestsException if the number of allowed attempts/requests has been exceeded
	 * @throws AuthError if an internal problem occurred (do *not* catch)
	 *
	 * @see forgotPassword
	 * @see canResetPasswordOrThrow
	 * @see canResetPassword
	 * @see resetPasswordAndSignIn
	 */
	public function resetPassword($selector, $token, $newPassword)
    {
		$this->throttle([ 'resetPassword', $this->getIpAddress() ], 5, (60 * 60), 10);
		$this->throttle([ 'resetPassword', 'selector', $selector ], 3, (60 * 60), 10);
		$this->throttle([ 'resetPassword', 'token', $token ], 3, (60 * 60), 10);

		try {
            $resetData = connect()->table('users_resets', true)
                ->join($this->table,'users.userId','=','users_resets.user', '', true)
                ->select('users_resets.id, users_resets.user, users_resets.token, users_resets.expires, users.email, users.resettable')
                ->where('users_resets.selector', '=', $selector)->get('array');

		}
		catch (Error $e) {
			throw new DatabaseError($e->getMessage());
		}

		if (!empty($resetData)) {
			if ((int) $resetData['resettable'] === 1) {
                if (\password_verify($token, $resetData['token'])) {
                    if ($resetData['expires'] >= \time()) {
						$newPassword = self::validatePassword($newPassword);

						$this->updatePasswordInternal($resetData['user'], $newPassword);
						$this->forceLogoutForUserById($resetData['user']);

						try {
                            connect()->table('users_resets')->where('id','=', $resetData['id'])->delete();
						}
						catch (Error $e) {
							throw new DatabaseError($e->getMessage());
						}

						return ['userId' => $resetData['user'], 'email' => $resetData['email']
						];
					} else {
						throw new TokenExpiredException();
					}
				} else {
					throw new InvalidSelectorTokenPairException();
				}
			}
			else {
				throw new ResetDisabledException();
			}
		} else {
			throw new InvalidSelectorTokenPairException();
		}
	}

	/**
	 * Resets the password for a particular account by supplying the correct selector/token pair
	 *
	 * The selector/token pair must have been generated previously by calling {@see forgotPassword}
	 *
	 * The user will be automatically signed in if this operation is successful
	 *
	 * @param string $selector the selector from the selector/token pair
	 * @param string $token the token from the selector/token pair
	 * @param string $newPassword the new password to set for the account
	 * @param int|null $rememberDuration (optional) the duration in seconds to keep the user logged in ("remember me"), e.g. `60 * 60 * 24 * 365.25` for one year
	 * @return string[] an array with the user's ID at index `id` and the user's email address at index `email`
	 * @throws InvalidSelectorTokenPairException if either the selector or the token was not correct
	 * @throws TokenExpiredException if the token has already expired
	 * @throws ResetDisabledException if the user has explicitly disabled password resets for their account
	 * @throws InvalidPasswordException if the new password was invalid
	 * @throws TooManyRequestsException if the number of allowed attempts/requests has been exceeded
	 * @throws AuthError if an internal problem occurred (do *not* catch)
	 *
	 * @see forgotPassword
	 * @see canResetPasswordOrThrow
	 * @see canResetPassword
	 * @see resetPassword
	 */
	public function resetPasswordAndSignIn($selector, $token, $newPassword, $rememberDuration = null)
    {
		$idAndEmail = $this->resetPassword($selector, $token, $newPassword);

		if (!$this->isLoggedIn()) {
			$idAndEmail['email'] = ValidateEmail::fromString($idAndEmail['email']);

			$userData = $this->getUserDataBy('email', $idAndEmail['email'], ['fullName', 'firstName', 'lastName', 'loginName', 'status', 'accessLevel', 'force_logout']);

			$this->onLoginUserSuccessful($idAndEmail['userId'], $userData['fullName'], $userData['firstName'], $userData['lastName'], $idAndEmail['email'], $userData['loginName'], $userData['status'], $userData['accessLevel'], $userData['force_logout'], true);

			if ($rememberDuration !== null) {
				$this->createRememberDirective($idAndEmail['id'], $rememberDuration);
			}
		}

		return $idAndEmail;
	}

    /**
     * Проверьте, можно ли использовать прилагаемую пару селектор/токен для сброса пароля
     * Пароль может быть сброшен с использованием предоставленной информации, если этот метод * не * выдает никаких исключений
     * Пара селектор/токен должна быть сгенерирована ранее с помощью вызова {@see ForgotPassword}
     *
     * @param string $selector the selector from the selector/token pair
     * @param string $token the token from the selector/token pair
     * @throws InvalidSelectorTokenPairException if either the selector or the token was not correct
     * @throws TokenExpiredException if the token has already expired
     * @throws ResetDisabledException if the user has explicitly disabled password resets for their account
     * @throws TooManyRequestsException if the number of allowed attempts/requests has been exceeded
     * @throws AuthError if an internal problem occurred (do *not* catch)
     * @throws InvalidPasswordException
     *
     * @see forgotPassword
     * @see canResetPassword
     * @see resetPassword
     * @see resetPasswordAndSignIn
     */
	public function canResetPasswordOrThrow(string $selector, string $token) {
		try {
			// pass an invalid password intentionally to force an expected error
			$this->resetPassword($selector, $token, null);

			// we should already be in one of the `catch` blocks now so this is not expected
			throw new AuthError();
		}
		// if the password is the only thing that's invalid
		catch (InvalidPasswordException $ignored) {
			// the password can be reset
		}
		// if some other things failed (as well)
		catch (AuthException $e) {
			// re-throw the exception
			throw $e;
		}
	}

	/**
	 * Проверьте, можно ли использовать прилагаемую пару селектор/токен для сброса пароля
	 * Пара селектор/токен должна быть сгенерирована ранее с помощью вызова {@see ForgotPassword}
	 *
	 * @param string $selector the selector from the selector/token pair
	 * @param string $token the token from the selector/token pair
	 * @return bool whether the password can be reset using the supplied information
	 * @throws AuthError if an internal problem occurred (do *not* catch)
	 *
	 * @see forgotPassword
	 * @see canResetPasswordOrThrow
	 * @see resetPassword
	 * @see resetPasswordAndSignIn
	 */
	public function canResetPassword($selector, $token) {
		try {
			$this->canResetPasswordOrThrow($selector, $token);

			return true;
		}
		catch (AuthException $e) {
			return false;
		}
	}

	/**
	 * Задает, следует ли разрешать сброс пароля для учетной записи текущего пользователя, вошедшего в систему
	 *
	 * @param bool $enabled whether password resets should be enabled for the user's account
	 * @throws NotLoggedInException if the user is not currently signed in
	 * @throws AuthError if an internal problem occurred (do *not* catch)
	 */
	public function setPasswordResetEnabled($enabled) {
		$enabled = (bool) $enabled;

		if ($this->isLoggedIn()) {
			try {
                connect($this->table)->where('userId', $this->getUserId())->update(['resettable' => $enabled ? 1 : 0]);
			}
			catch (Error $e) {
				throw new DatabaseError($e->getMessage());
			}
		}
		else {
			throw new NotLoggedInException();
		}
	}

	/**
	 * Возвращает, разрешен ли сброс пароля для учетной записи текущего пользователя, вошедшего в систему
	 *
	 * @return bool
	 * @throws NotLoggedInException if the user is not currently signed in
	 * @throws AuthError if an internal problem occurred (do *not* catch)
	 */
	public function isPasswordResetEnabled()
    {
		if ($this->isLoggedIn()) {
			try {
                $enabled = connect($this->table)->select('resettable')->where('userId', $this->getUserId())->get();

				return (int) $enabled->resettable === 1;
			}
			catch (Error $e) {
				throw new DatabaseError($e->getMessage());
			}
		}
		else {
			throw new NotLoggedInException();
		}
	}


    /**
     * Изменяет текущий пароль пользователя, вошедшего в систему, при этом для проверки требуется старый пароль.
     *
     * @param array $userData the old password to verify account ownership
     * @throws DatabaseError
     * @throws NotLoggedInException
     */
    public function changeUserInfo($userData)
    {
        if ($this->isLoggedIn()) {
            $this->throttle(['enumerateUsers', $this->getIpAddress()], 1, (60 * 60), 75);

            $data = [
                'fullName'  => $userData['firstName'] . ' ' . $userData['lastName'] ,
                'firstName' => $userData['firstName'],
                'lastName'  => $userData['lastName'],
            ];
            try {
               $result = connect($this->table)->where('userId', $this->getUserId())->update($data);
            }
            catch (Error $e) {
                throw new DatabaseError($e->getMessage());
            }

            if ($result) {
                $_SESSION['siteUser'][self::SESSION_USER_FULL_NAME]    = $data['fullName'];
                $_SESSION['siteUser'][self::SESSION_USER_FIRST_NAME]   = $data['firstName'];
                $_SESSION['siteUser'][self::SESSION_USER_LAST_NAME]    = $data['lastName'];
            }
        }
        else {
            throw new NotLoggedInException();
        }
    }

    /**
     * Изменяет текущий номер, вошедшего в систему, при этом для проверки требуется старый пароль.
     *
     * @param string $newPhone новый номер телефона
     * @throws NotLoggedInException
     * @throws UserAlreadyExistsException
     */
    public function changeConfirmUserPhone(string $newPhone, $expires, callable $callback = null)
    {
        if ($this->isLoggedIn()) {

            $this->throttle(['enumerateUsers', $this->getIpAddress()], 1, (60 * 60), 75);

            try {
                $existingUsersWithNewPhone = connect($this->table)->count('*', 'count')->where('phone', $newPhone)->getValue();
            }
            catch (Error $e) {
                throw new DatabaseError($e->getMessage());
            }

            if ((int) $existingUsersWithNewPhone !== 0) {
                throw new UserAlreadyExistsException();
            }

            $this->throttle(['requestPhoneChange', 'userId', $this->getUserId()], 1, (60 * 60 * 24));
            $this->throttle(['requestPhoneChange', $this->getIpAddress()], 1, (60 * 60 * 24), 3);

            $this->createConfirmationRequestSms($this->getUserId(), $newPhone, $expires, $callback);
        }
        else {
            throw new NotLoggedInException();
        }
    }


    /**
     * Изменяет текущий пароль пользователя, вошедшего в систему, при этом для проверки требуется старый пароль.
     *
     * @param array $userData the old password to verify account ownership
     * @throws DatabaseError
     * @throws NotLoggedInException
     */
    public function changeUserPhone($userData, callable $callback = null)
    {
        if ($this->isLoggedIn()) {
            $this->throttle(['enumerateUsers', $this->getIpAddress()], 1, (60 * 60), 75);

//            $this->updatePhonePassword($userData['phone'], $password, $userData['userId'], $callback);
            if (!isset($userData['fullName']) || empty($userData['fullName'])) {
                $userData['fullName'] = $userData['firstName'] . ' ' . $userData['lastName'];
            }
            $data = array_filter([
                'fullName'  => $userData['fullName'],
                'firstName' => $userData['firstName'] ?? null,
                'lastName'  => $userData['lastName'] ?? null,
                'loginName' => $userData['phone'],
                'phone'     => $userData['phone'],
                'email'     => $userData['email'],
                'address'   => $userData['address'] ?? null,
            ]);
            try {
                $result = connect($this->table)->where('userId',  $this->getUserId())->update($data);
            }
            catch (Error $e) {
                throw new DatabaseError($e->getMessage());
            }

            if ($result) {
                $_SESSION['siteUser'][self::SESSION_USER_PHONE]    = $data['phone'];
            }
        }
        else {
            throw new NotLoggedInException();
        }
    }

    /**
     * Изменяет текущий пароль пользователя, вошедшего в систему, при этом для проверки требуется старый пароль.
     *
     * @param array $userData the old password to verify account ownership
     * @throws NotLoggedInException
     */
    public function changeUserPassport($userData)
    {
        if ($this->isLoggedIn()) {
            $this->throttle(['enumerateUsers', $this->getIpAddress()], 1, (60 * 60), 75);


            $data = [
                'passport'  => $userData['pSeries'] . ' ' . $userData['pNumber'],
                'pinfl'  => $userData['pinfl']
            ];
            try {
                $result = connect($this->table)->where('userId', $this->getUserId())->update($data);
            }
            catch (Error $e) {
                throw new DatabaseError($e->getMessage());
            }

//            if ($result) {
//                $_SESSION['siteUser'][self::SESSION_USER_PASSPORT]    = $data['passport'];
//                $_SESSION['siteUser'][self::SESSION_USER_PINFL]    = $data['pinfl'];
//            }
        }
        else {
            throw new NotLoggedInException();
        }
    }

    /**
     * @param $imageId
     * @throws DatabaseError
     */
    public function createUserPhoto($imageId)
    {
        try {
            $result = connect($this->table)->where('userId', $this->getUserId())->update(['fileId' => $imageId]);
        } catch (Error $e) {
            throw new DatabaseError($e->getMessage());
        }

        if ($result) {
            $_SESSION['siteUser'][self::SESSION_USER_PHOTO] = $imageId;
        }
    }

	/**
	 * Возвращает, вошел ли пользователь в систему в настоящее время, читая из session
	 *
	 * @return boolean вошел ли пользователь в систему или нет
	 */
	public function isLoggedIn()
    {
		return isset($_SESSION) && isset($_SESSION['siteUser'][self::SESSION_USER_LOGGED_IN]) && $_SESSION['siteUser'][self::SESSION_USER_LOGGED_IN] === true;
	}

    /**
     * Возвращает, имеет ли пользователь с данным идентификатором указанную роль
     *
     * @param int $userId the ID of the user to check the roles for
     * @param string $role
     * @throws UnknownIdException if no user with the specified ID has been found
     */
    public function doesUserHaveRole(int $userId, string $role)
    {
        if (empty($role)) {
            return false;
        }

        $rolesBitmask = connect($this->table)->select('accessLevel')->where('userId', $userId)->get();

        if ($rolesBitmask->accessLevel === null) {
            throw new UnknownIdException();
        }

        $role = (int) $role;
//        var_dump(($rolesBitmask->accessLevel & $role) === $role);
        return ($rolesBitmask->accessLevel & $role) === $role;
    }

    /**
     * Сокращение / псевдоним для ´isLoggedIn()´
     *
     * @return boolean
     */
    public function checkUser()
    {
        return $this->isLoggedIn();
    }

	/**
	 * Возвращает идентификатор пользователя, вошедшего в систему, путем чтения из сеанса
	 *
	 * @return int the user ID
	 */
	public function getUserId()
    {
		if (isset($_SESSION) && isset($_SESSION['siteUser'][self::SESSION_USER_USER_ID])) {
			return $_SESSION['siteUser'][self::SESSION_USER_USER_ID];
		}
		else {
			return null;
		}
	}

	/**
	 * Сокращение / псевдоним для {@see getUserId}
	 *
	 * @return int
	 */
	public function id()
    {
		return $this->getUserId();
	}

	/**
	 * Returns the currently signed-in user's email address by reading from the session
	 *
	 * @return string the email address
	 */
	public function getEmail()
    {
		if (isset($_SESSION) && isset($_SESSION['siteUser'][self::SESSION_USER_EMAIL])) {
			return $_SESSION['siteUser'][self::SESSION_USER_EMAIL];
		}
		else {
			return null;
		}
	}

	/**
	 * Returns the currently signed-in user's display name by reading from the session
	 *
	 * @return string the display name
	 */
	public function getUsername()
    {
		if (isset($_SESSION) && isset($_SESSION['siteUser'][self::SESSION_USER_USERNAME])) {
			return $_SESSION['siteUser'][self::SESSION_USER_USERNAME];
		}
		else {
			return null;
		}
	}

	/**
	 * Returns the currently signed-in user's status by reading from the session
	 *
	 * @return int the status as one of the constants from the {@see Status} class
	 */
	public function getStatus()
    {
		if (isset($_SESSION) && isset($_SESSION['siteUser'][self::SESSION_USER_STATUS])) {
			return $_SESSION['siteUser'][self::SESSION_USER_STATUS];
		}
		else {
			return null;
		}
	}

	/**
     * Возвращает, находится ли текущий авторизованный пользователь в "нормальном" состоянии.
	 * Returns whether the currently signed-in user is in "normal" state
	 *
	 * @return bool
	 *
	 * @see Status
	 * @see Auth::getStatus
	 */
	public function isNormal()
    {
		return $this->getStatus() === Status::NORMAL;
	}

    public function getBanned()
    {
        if (isset($_SESSION) && isset($_SESSION['siteUser'][self::SESSION_USER_BANNED])) {
            return $_SESSION['siteUser'][self::SESSION_USER_BANNED];
        } else {
            return null;
        }
    }

	/**
     * Возвращает, находится ли текущий авторизованный пользователь в "заблокированном" состоянии.
	 * Returns whether the currently signed-in user is in "locked" state
	 *
	 * @return bool
	 *
	 * @see Status
	 * @see Auth::getStatus
	 */
	public function isLocked()
    {
		return $this->getStatus() === Status::PENDING_REVIEW;
	}

	/**
     * Возвращает, находится ли текущий вошедший в систему пользователь в состоянии «ожидает проверки».
	 * Returns whether the currently signed-in user is in "pending review" state
	 *
	 * @return bool
	 *
	 * @see Status
	 * @see Auth::getStatus
	 */
	public function isPendingReview()
    {
		return $this->getStatus() === Status::PENDING_REVIEW;
	}



	/**
     * Возвращает, имеет ли текущий вошедший в систему пользователь указанную роль.
	 * Returns whether the currently signed-in user has the specified role
	 *
	 * @param string $role
	 * @return bool
	 */
	public function hasRole($role)
    {
		if (empty($role)) {
			return false;
		}

		if (isset($_SESSION) && isset($_SESSION['siteUser'][self::SESSION_USER_ROLES])) {

			return (((int) $_SESSION['siteUser'][self::SESSION_USER_ROLES]) & $role) === $role;
		}
		else {
			return false;
		}
	}

	/**
     * Возвращает, имеет ли текущий вошедший в систему пользователь * любую * из указанных ролей.
	 * Returns whether the currently signed-in user has *any* of the specified roles
	 *
	 * @param string[] ...$roles
	 * @return bool
	 */
	public function hasAnyRole(...$roles)
    {
		foreach ($roles as $role) {
			if ($this->hasRole($role)) {
				return true;
			}
		}

		return false;
	}

	/**
     * Возвращает, есть ли у текущего вошедшего в систему пользователя * все * указанные роли.
	 * Returns whether the currently signed-in user has *all* of the specified roles
	 *
	 * @param string[] ...$roles
	 * @return bool
	 */
	public function hasAllRoles(...$roles)
    {
		foreach ($roles as $role) {
			if (!$this->hasRole($role)) {
				return false;
			}
		}

		return true;
	}


	/**
     * Возвращает, запомнился ли текущий авторизованный пользователь долгоживущим файлом cookie.
	 * Returns whether the currently signed-in user has been remembered by a long-lived cookie
	 *
	 * @return bool whether they have been remembered
	 */
	public function isRemembered()
    {
		if (isset($_SESSION) && isset($_SESSION['siteUser'][self::SESSION_USER_REMEMBERED])) {
			return $_SESSION['siteUser'][self::SESSION_USER_REMEMBERED];
		}
		else {
			return null;
		}
	}

	/**
	 * Returns the user's current IP address
	 *
	 * @return string the IP address (IPv4 or IPv6)
	 */
	public function getIpAddress()
    {
		return $this->ipAddress;
	}

	/**
	 * Performs throttling or rate limiting using the token bucket algorithm (inverse leaky bucket algorithm)
	 *
	 * @param array $criteria the individual criteria that together describe the resource that is being throttled
	 * @param int $supply укажите количество единиц, которые необходимо предоставить за интервал (>= 1)
	 * @param int $interval интервал (в секундах), на который предоставляется подача(>= 5)
	 * @param int|null $burstiness (optional) допустимая степень вариации или неравномерности во время пиков (>= 1)
	 * @param bool|null $simulated (optional) whether to simulate a dry run instead of actually consuming the requested units
	 * @param int|null $cost (optional) количество единиц для запроса (>= 1)
	 * @param bool|null $force (optional) применять ли регулирование локально (с помощью этого вызова), даже если регулирование было отключено глобально (на экземпляре с помощью параметра конструктора)
	 * @return float the number of units remaining from the supply
	 * @throws TooManyRequestsException if the actual demand has exceeded the designated supply
	 * @throws AuthError if an internal problem occurred (do *not* catch)
	 */
    public function throttle(array $criteria, $supply, $interval, $burstiness = null, $simulated = null, $cost = null, $force = null)
    {
		// validate the supplied parameters and set appropriate defaults where necessary
		$force = ($force !== null) ? (bool) $force : false;

		if (!$this->throttling && !$force) {
			return $supply;
		}

		// generate a unique key for the bucket (consisting of 44 or fewer ASCII characters)
		$key = Base64::encodeUrlSafeWithoutPadding(\hash('sha256', \implode("\n", $criteria), true));

		// validate the supplied parameters and set appropriate defaults where necessary
		$burstiness = ($burstiness !== null) ? (int) $burstiness : 1000;
		$simulated = ($simulated !== null) ? (bool) $simulated : false;
		$cost = ($cost !== null) ? (int) $cost : 1;

		$now = \time();

		// determine the volume of the bucket
		$capacity = $burstiness * (int) $supply;

		// calculate the rate at which the bucket is refilled (per second)
		$bandwidthPerSecond = (int) $supply / (int) $interval;

		try {
            $bucket = connect()->table('users_throttling')->select('tokens, replenished_at')->where('bucket', '=', $key)->get('array');
		}
		catch (Error $e) {
			throw new DatabaseError($e->getMessage());
		}

		if ($bucket === null) {
			$bucket = [];
		}

		// initialize the number of tokens in the bucket
		$bucket['tokens'] = isset($bucket['tokens']) ? (float) $bucket['tokens'] : (float) $capacity;
		// initialize the last time that the bucket has been refilled (as a Unix timestamp in seconds)
		$bucket['replenished_at'] = isset($bucket['replenished_at']) ? (int) $bucket['replenished_at'] : $now;

		// replenish the bucket as appropriate
		$secondsSinceLastReplenishment = \max(0, $now - $bucket['replenished_at']);
		$tokensToAdd = $secondsSinceLastReplenishment * $bandwidthPerSecond;
		$bucket['tokens'] = \min((float) $capacity, $bucket['tokens'] + $tokensToAdd);
		$bucket['replenished_at'] = $now;

		$accepted = $bucket['tokens'] >= $cost;

		if (!$simulated) {
			if ($accepted) {
				// remove the requested number of tokens from the bucket
				$bucket['tokens'] = \max(0, $bucket['tokens'] - $cost);
                $bucket['tokens'] = str_replace(',', '.', $bucket['tokens']);
			}

			// set the earliest time after which the bucket *may* be deleted (as a Unix timestamp in seconds)
			$bucket['expires_at'] = $now + \floor($capacity / $bandwidthPerSecond * 2);

			// merge the updated bucket into the database
			try {
                $affected = connect()->table('users_throttling')->where('bucket', '=', $key)->update($bucket);
			}
			catch (Error $e) {
				throw new DatabaseError($e->getMessage());
			}

			if ($affected === 0) {
				$bucket['bucket'] = $key;

				try {
                    connect()->table('users_throttling')->insert($bucket);
				}
				catch (IntegrityConstraintViolationException $ignored) {

                }
				catch (Error $e) {
					throw new DatabaseError($e->getMessage());
				}
			}
		}

		if ($accepted) {
			return $bucket['tokens'];
		}
		else {
			$tokensMissing = $cost - $bucket['tokens'];
			$estimatedWaitingTimeSeconds = \ceil($tokensMissing / $bandwidthPerSecond);

			throw new TooManyRequestsException('', $estimatedWaitingTimeSeconds);
		}
	}


	/**
	 * Создает UUID v4 в соответствии с RFC 4122.
	 *
	 * UUID содержит 128 бит данных (где 122 случайных), то есть 36 символов.
	 *
	 * @return string the UUID
	 * @author Jack @ Stack Overflow
	 */
	public static function createUuid()
    {
		$data = \openssl_random_pseudo_bytes(16);

		// set the version to 0100
		$data[6] = \chr(\ord($data[6]) & 0x0f | 0x40);
		// set bits 6-7 to 10
		$data[8] = \chr(\ord($data[8]) & 0x3f | 0x80);

		return \vsprintf('%s%s-%s-%s-%s-%s%s%s', \str_split(\bin2hex($data), 4));
	}

	/**
	 * Generates a unique cookie name for the given descriptor based on the supplied seed
	 *
	 * @param string $descriptor a short label describing the purpose of the cookie, e.g. 'session'
	 * @param string|null $seed (optional) the data to deterministically generate the name from
	 * @return string
	 */
	public static function createCookieName($descriptor, $seed = null)
    {
		// use the supplied seed or the current UNIX time in seconds
		$seed = ($seed !== null) ? $seed : \time();

		foreach (self::COOKIE_PREFIXES as $cookiePrefix) {
			// if the seed contains a certain cookie prefix
			if (\strpos($seed, $cookiePrefix) === 0) {
				// prepend the same prefix to the descriptor
				$descriptor = $cookiePrefix . $descriptor;
			}
		}

		// generate a unique token based on the name(space) of this library and on the seed
		$token = Base64::encodeUrlSafeWithoutPadding(\md5(__NAMESPACE__ . "\n" . $seed, true));

		return $descriptor . '_' . $token;
	}

	/**
	 * Generates a unique cookie name for the 'remember me' feature
	 *
	 * @param string|null $sessionName (optional) the session name that the output should be based on
	 * @return string
	 */
	public static function createRememberCookieName($sessionName = null)
    {
		return self::createCookieName('remember', ($sessionName !== null) ? $sessionName : \session_name());
	}

	/**
	 * Returns the selector of a potential locally existing remember directive
	 *
	 * @return string|null
	 */
	private function getRememberDirectiveSelector()
    {
		if (isset($_COOKIE[$this->rememberCookieName])) {
			$selectorAndToken = \explode(self::COOKIE_CONTENT_SEPARATOR, $_COOKIE[$this->rememberCookieName], 2);

			return $selectorAndToken[0];
		}
		else {
			return null;
		}
	}

	/**
     * Возвращает дату истечения срока действия потенциально существующей локально директивы запоминания.
	 * Returns the expiry date of a potential locally existing remember directive
	 *
	 * @return int|null
	 */
	private function getRememberDirectiveExpiry()
    {
		// если пользователь в настоящее время вошел в систему
		if ($this->isLoggedIn()) {
			// определить селектор любой существующей в настоящее время директивы запоминания
			$existingSelector = $this->getRememberDirectiveSelector();

			// если в настоящее время существует директива запоминания, селектор которой мы только что получили
			if (isset($existingSelector)) {
				// получить дату истечения срока действия для данного селектора
                $existingExpiry = connect($this->table_remembered)->select('expires')->where('selector', $existingSelector)->where('user', $this->getUserId())->get();
				// если установлен срок годности
				if (isset($existingExpiry->expires)) {
					// вернуть дату
					return (int) $existingExpiry->expires;
				}
			}
		}

		return null;
	}

    /**
     * Получение пользователя
     *
     * @param $id
     * @param false $image
     * @return array|false|mixed|string|null
     */
	public function loadSiteUser($id, $image = false)
    {
        $fields = 'userId, fullName, firstName, middleName, lastName, loginName, phone, email, address, fileId, paid, favorites';
        if ($user = connect($this->table)->select($fields)->where('userId', $id)->where('status', 1)->get('array')) {
            if (!empty($user['address'])) {
                $user['address'] = json_decode($user['address'], true);
            }
           if ($image) {
               if ($fileId = connect($this->table_images)->select('fileId')->where('userId', $user['userId'])->getValue()) {
                   $file = connect('files')->select('fileId, directory, path, fileName, size')->where('fileId', $fileId)->get('array');
                   $user['image'] = ImageSizes::init()->get(null, $file);
               }
           }

           return $user;
        }
        return null;
    }


}
