*/ final class accounts_model extends core { /** * Идентификатор */ public int $id; /** * Почта */ public string $mail; /** * Пароль */ public string $password; /** * Хеш */ public string $hash; /** * Время активности хеша */ public int $time; /** * Время активности хеша */ public array $permissions; /** * Конструктор * * @param array $vars Параметры */ public function __construct(array $vars = []) { foreach ($vars as $key => $value) { // Перебор параметров // Запись свойства if (property_exists($this, $key)) $this->$key = $value; } } /** * Регистрация * * @param string $mail Почта * @param string $password Пароль * @param bool $authenticate Автоматическая аутентификация в случае успешной регистрации * @param array &$errors Журнал ошибок * * @return static|null Аккаунт */ public static function registration(string $mail, string $password, bool $authenticate = true, array &$errors = []): ?static { // Инициализация журнала ошибок $errors['account'] ?? $errors['account'] = []; try { if (static::init(errors: $errors)) { // Аутентифицирован пользователь // Запись ошибки throw new exception('Уже аутентифицирован'); } if (empty($account = static::read(['mail' => $mail]))) { // Не удалось найти аккаунт if (static::write($mail, $password, $errors)) { // Удалось зарегистрироваться if ($authenticate) { // Запрошена аутентификация // Аутентификация $account = static::authentication($mail, $password, true, $errors); } return $account; } } else { // Удалось найти аккаунт return $account; } } catch (exception $e) { // Запись в журнал ошибок $errors['account'][] = [ 'text' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine(), 'stack' => $e->getTrace() ]; } return null; } /** * Аутентификация * * @param string $mail Почта * @param string $password Пароль * @param bool $remember Функция "Запомнить меня" - увеличенное время хранения cookies * @param array &$errors Журнал ошибок * * @return static|null Аккаунт */ public static function authentication(string $mail, string $password, bool $remember = false, array &$errors = []): ?static { // Инициализация журнала ошибок $errors['account'] ?? $errors['account'] = []; try { if (static::init(errors: $errors)) { // Аутентифицирован пользователь // Запись ошибки throw new exception('Уже аутентифицирован'); } if (empty($account = static::read(['mail' => $mail]))) { // Не удалось найти аккаунт throw new exception('Не удалось найти аккаунт'); } if (password_verify($password, $account->password)) { // Совпадают хеши паролей // Инициализация идентификатора сессии session_id((string) $account->id); // Инициализация названия сессии session_name('id'); // Инициализация сессии session_start(); // Инициализация времени хранения хеша $time = time() + ($remember ? 604800 : 86400); // Инициализация хеша $hash = static::hash((int) $account->id, crypt($account->password, time() . $account->id), $time, $errors)['hash']; // Инициализация cookies setcookie('hash', $hash, $time, path: '/', secure: true); return $account; } else { // Не совпадают хеши паролей throw new exception('Неправильный пароль'); } } catch (exception $e) { // Запись в журнал ошибок $errors['account'][] = [ 'text' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine(), 'stack' => $e->getTrace() ]; } return null; } /** * Аутентификация * * @param array &$errors Журнал ошибок * * @return bool Удалось ли деаутентифицироваться */ public static function deauthentication(array &$errors = []): bool { // Инициализация журнала ошибок $errors['account'] ?? $errors['account'] = []; try { if ($account = static::init(errors: $errors)) { // Аутентифицирован пользователь // Инициализация запроса $request = static::$db->prepare("UPDATE `accounts` SET `hash` = null, `time` = 0 WHERE `id` = :id"); // Параметры запроса $params = [ ":id" => $account->id, ]; // Отправка запроса $request->execute($params); // Генерация ответа $request->fetch(pdo::FETCH_ASSOC); // Деинициализация cookies setcookie('id', '', 0, path: '/', secure: true); setcookie('hash', '', 0, path: '/', secure: true); return true; } else { // Не аутентифицирован пользователь // Запись ошибки throw new exception('Не аутентифицирован'); } } catch (exception $e) { // Запись в журнал ошибок $errors['account'][] = [ 'text' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine(), 'stack' => $e->getTrace() ]; } return false; } /** * Инициализация * * @param int|null $account Аккаунт (идентификатор) * @param array &$errors Журнал ошибок * * @return static|null Аккаунт */ public static function init(?int $account = null, array &$errors = []): ?static { // Инициализация журнала ошибок $errors['account'] ?? $errors['account'] = []; try { if (isset($account)) { // Получен идентификатор аккаунта if (empty($account = static::read(['id' => $account]))) { // Не найден аккаунт // Генерация ошибки throw new exception('Не найден пользователь'); } } else if (!empty($_COOKIE['id']) && !empty($_COOKIE['hash'])) { // Найдены cookie с данными аккаунта (подразумевается, что он аутентифицирован) if ($_COOKIE['hash'] === static::hash((int) $_COOKIE['id'], errors: $errors)['hash']) { // Совпадает переданный хеш с тем, что хранится в базе данных } else { // Не совпадает переданный хеш с тем, что хранится в базе данных // Генерация ошибки throw new exception('Вы аутентифицированы с другого устройства (не совпадают хеши аутентификации)'); } if (empty($account = static::read([ 'id' => $_COOKIE['id'], 'hash' => $_COOKIE['hash'] ]))) { // Не найден аккаунт или связка аккаунта с хешем // Генерация ошибки throw new exception('Не найден пользователь или время аутентификации истекло'); } } else { // Не найдены параметры для поиска аккаунта return null; } // Чтение разрешений $account->permissions = static::permissions((int) $account->id, $errors); return $account; } catch (exception $e) { // Запись в журнал ошибок $errors['account'][]= [ 'text' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine(), 'stack' => $e->getTrace() ]; } return null; } /** * Прочитать разрешения из базы данных * * @param int $id Идентификатор аккаунта * @param array &$errors Журнал ошибок * * @return array Разрешения аккаунта, если найдены */ public static function permissions(int $id, array &$errors = []): array { // Инициализация журнала ошибок $errors['account'] ?? $errors['account'] = []; try { // Инициализация запроса $request = static::$db->prepare("SELECT * FROM `permissions` WHERE `id` = :id"); // Параметры запроса $params = [ ":id" => $id ]; // Отправка запроса $request->execute($params); // Генерация ответа if (empty($response = $request->fetch(pdo::FETCH_ASSOC))) { // Не найдены разрешения // Генерация ошибки throw new exception('Не найдены разрешения'); } // Удаление ненужных данных unset($response['id']); return $response; } catch (exception $e) { // Запись в журнал ошибок $errors['account'][] = [ 'text' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine(), 'stack' => $e->getTrace() ]; } return []; } /** * Проверить разрешение * * @param string $permission Разрешение * @param int|null $id Идентификатор аккаунта * @param array &$errors Журнал ошибок * * @return bool|null Статус разрешения, если оно записано */ public static function access(string $permission, int|null $id = null, array &$errors = []): ?bool { // Инициализация журнала ошибок $errors['account'] ?? $errors['account'] = []; try { // Инициализация аккаунта $account = isset($id) ? self::read(['id' => $id], $errors) : self::account($errors); return isset($account->permissions[$permission]) ? (bool) $account->permissions[$permission] : null; } catch (exception $e) { // Запись в журнал ошибок $errors['account'][]= [ 'text' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine(), 'stack' => $e->getTrace() ]; } return null; } /** * Запись в базу данных * * @param string $mail Почта * @param string $password Пароль * @param array &$errors Журнал ошибок * * @return static|null Аккаунт */ public static function write(string $mail, string $password, array &$errors = []): ?static { // Инициализация журнала ошибок $errors['account'] ?? $errors['account'] = []; try { // Инициализация параметров запроса $params = []; try { // Проверка параметра if (filter_var($mail, FILTER_VALIDATE_mail) === false) throw new exception('Не удалось распознать почту'); if (iconv_strlen($mail) < 3) throw new exception('Длина почты должна быть не менее 3 символов'); if (iconv_strlen($mail) > 60) throw new exception('Длина почты должна быть не более 80 символов'); // Запись в буфер параметров запроса $params[':mail'] = $mail; // Проверка параметра if (iconv_strlen($password) < 3) throw new exception('Длина пароля должна быть не менее 3 символов'); if (iconv_strlen($password) > 60) throw new exception('Длина пароля должна быть не более 120 символов'); // Запись в буфер параметров запроса $params[':password'] = password_hash($password, PASSWORD_BCRYPT); // Инициализация запроса $request = static::$db->prepare("INSERT INTO `accounts` (" . (isset($name) ? '`name`' : '') . (isset($name) && isset($mail) ? ', ' : '') . (isset($mail) ? '`mail`' : '') . ((isset($name) || isset($mail)) && isset($password) ? ', ' : '') . (isset($password) ? '`password`' : '') . ") VALUES (" . (isset($name) ? ':name' : '') . (isset($name) && isset($mail) ? ', ' : '') . (isset($mail) ? ':mail' : '') . ((isset($name) || isset($mail)) && isset($password) ? ', ' : '') . (isset($password) ? ':password' : '') . ")"); // Отправка запроса $request->execute($params); // Генерация ответа $request->fetch(pdo::FETCH_ASSOC); // Чтение аккаунта $account = static::read(['mail' => $mail]); // Инициализация запроса $request = static::$db->prepare("INSERT INTO `permissions` (`id`) VALUES (:id)"); // Инициализация параметров $params = [ ':id' => $account->id ]; // Отправка запроса $request->execute($params); // Генерация ответа $request->fetch(pdo::FETCH_ASSOC); } catch (exception $e) { // Запись в журнал ошибок $errors['account'][] = [ 'text' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine(), 'stack' => $e->getTrace() ]; } } catch (exception $e) { // Запись в журнал ошибок $errors['account'][] = [ 'text' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine(), 'stack' => $e->getTrace() ]; } // Конец выполнения end: return $account ?? []; } /** * Чтение из базы данных * * @param array $expression Выражение поиска ('поле' => 'значение') * @param array &$errors Журнал ошибок * * @return static|null Аккаунт */ public static function read(array $expression, array &$errors = []): ?static { // Инициализация журнала ошибок $errors['account'] ?? $errors['account'] = []; try { // Инициализация выражения поиска $where = 'WHERE '; // Инициализация параметров запроса $params = []; foreach ($expression as $parameter => $value) { // Перебор выражения поиска // Запись в строку запроса $where .= "`$parameter` = :$parameter &&"; // Запись параметров запроса $params[":$parameter"] = $value; } // Очистка или реинициализация выражения поиска $where = empty($expression) ? '' : trim(trim($where, '&&')); // Инициализация запроса $request = static::$db->prepare("SELECT * FROM `accounts` $where LIMIT 1"); // Отправка запроса $request->execute($params); // Генерация ответа if ($account = new static($request->fetch(pdo::FETCH_ASSOC))) { // Найден аккаунт try { if ($permissions = static::permissions((int) $account->id, $errors)) { // Найдены разрешения // Запись в буфер данных аккаунта $account->permissions = $permissions; } else { // Не найдены разрешения throw new exception('Не удалось найти и прочитать разрешения'); } } catch (exception $e) { // Запись в журнал ошибок $errors['account'][] = [ 'text' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine() ]; } } else { // Не найден аккаунт throw new exception('Не удалось найти аккаунт'); } } catch (exception $e) { // Запись в журнал ошибок $errors['account'][]= [ 'text' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine(), 'stack' => $e->getTrace() ]; } return $account ?? null; } /** * Запись или чтение хеша из базы данных * * @param int $id Идентификатор аккаунта * @param int|null $hash Хеш аутентифиакции * @param string|null $time Время хранения хеша * @param array &$errors Журнал ошибок * * @return array ['hash' => $hash, 'time' => $time] */ public static function hash(int $id, string|null $hash = null, int|null $time = null, array &$errors = []): array { // Инициализация журнала ошибок $errors['account'] ?? $errors['account'] = []; try { if (isset($hash, $time)) { // Переданы хеш и его время хранения // Инициализация запроса $request = static::$db->prepare("UPDATE `accounts` SET `hash` = :hash, `time` = :time WHERE `id` = :id"); // Параметры запроса $params = [ ":id" => $id, ":hash" => $hash, ":time" => $time, ]; // Отправка запроса $request->execute($params); // Генерация ответа $request->fetch(pdo::FETCH_ASSOC); } else { // Не переданы хеш и его время хранения // Инициализация запроса $request = static::$db->prepare("SELECT `hash`, `time` FROM `accounts` WHERE `id` = :id"); // Параметры запроса $params = [ ":id" => $id, ]; // Отправка запроса $request->execute($params); // Генерация ответа extract((array) $request->fetch(pdo::FETCH_ASSOC)); if (!empty($response['time']) && $response['time'] <= time()) { // Истекло время жизни хеша // Инициализация запроса $request = static::$db->prepare("UPDATE `accounts` SET `hash` = :hash, `time` = :time WHERE `id` = :id"); // Параметры запроса $params = [ ":id" => $id, ":hash" => null, ":time" => null, ]; // Отправка запроса $request->execute($params); // Генерация ответа $response = $request->fetch(pdo::FETCH_ASSOC); // Генерация ошибки throw new exception('Время аутентификации истекло'); } } } catch (exception $e) { // Запись в журнал ошибок $errors['account'][] = [ 'text' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine(), 'stack' => $e->getTrace() ]; } return ['hash' => $hash, 'time' => $time]; } }