*/ final class accounts_model extends core { /** * Регистрация * * @param string $name Входной псевдоним * @param string $email Почта * @param string $password Пароль (password) * @param bool $authentication Автоматическая аутентификация в случае успешной регистрации * @param array &$errors Журнал ошибок * * @return array|bool Аккаунт, если удалось аутентифицироваться */ public static function registration(string $name = null, string $email = null, string $password, array &$errors = []): array { try { if (static::account($errors)) { // Аутентифицирован пользователь // Запись ошибки throw new exception('Уже аутентифицирован'); } if (empty($account = static::read(['name' => $name]) or $account = static::read(['email' => $email]))) { // Не удалось найти аккаунт if (static::write($name, $email, $password, $errors)) { // Удалось зарегистрироваться return $account; } } else { // Удалось найти аккаунт return $account; } } catch (exception $e) { // Запись в журнал ошибок $errors[]= [ 'text' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine(), 'stack' => $e->getTrace() ]; } return []; } /** * Аутентификация * * @param string $login Входной псевдоним * @param string $password Пароль (password) * @param bool $remember Функция "Запомнить меня" - увеличенное время хранения cookies * @param array &$errors Журнал ошибок * * @return array Аккаунт (если не найден, то пустой массив) */ public static function authentication(string $login, string $password, bool $remember = false, array &$errors = []): array { try { if (static::account($errors)) { // Аутентифицирован пользователь // Запись ошибки throw new exception('Уже аутентифицирован'); } if (empty($account = static::read(['name' => $login]) or $account = static::read(['email' => $login]))) { // Не удалось найти аккаунт throw new exception('Не удалось найти аккаунт'); } if (password_verify($password, $account['password'])) { // Совпадают хеши паролей // Инициализация идентификатора сессии session_id($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[]= [ 'text' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine(), 'stack' => $e->getTrace() ]; } return []; } /** * Аутентификация * * @param array &$errors Журнал ошибок * * @return bool Удалось ли деаутентифицироваться */ public static function deauthentication(array &$errors = []): bool { try { if ($account = static::account($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[]= [ 'text' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine(), 'stack' => $e->getTrace() ]; } return false; } /** * Прочитать данные аккаунта, если пользователь аутентифицирован * * Можно использовать как проверку на аутентифицированность * * @param array &$errors Журнал ошибок * * @return array Аккаунт (если не найден, то пустой массив) * * @todo 1. Сделать в static::read() возможность передачи нескольких параметров и перенести туда непосредственно чтение аккаунта с проверкой хеша */ public static function account(array &$errors = []): array { try { if (!empty($_COOKIE['id']) && !empty($_COOKIE['hash'])) { // Аутентифицирован аккаунт (найдены cookie и они хранят значения - подразумевается, что не null или пустое) if ($_COOKIE['hash'] === static::hash((int) $_COOKIE['id'], errors: $errors)['hash']) { // Совпадает переданный хеш с тем, что хранится в базе данных } else { // Не совпадает переданный хеш с тем, что хранится в базе данных // Генерация ошибки throw new exception('Вы аутентифицированы с другого устройства (не совпадают хеши аутентификации)'); } // Инициализация запроса $request = static::$db->prepare("SELECT * FROM `accounts` WHERE `id` = :id && `hash` = :hash"); // Параметры запроса $params = [ ":id" => $_COOKIE['id'], ":hash" => $_COOKIE['hash'], ]; // Отправка запроса $request->execute($params); // Генерация ответа if (empty($account = $request->fetch(pdo::FETCH_ASSOC))) { // Не найдена связка идентификатора с хешем // Генерация ошибки throw new exception('Не найден пользотватель или время аутентификации истекло'); } // Чтение разрешений $account['permissions'] = static::permissions((int) $account['id'], $errors); return $account; } } catch (exception $e) { // Запись в журнал ошибок $errors[]= [ 'text' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine(), 'stack' => $e->getTrace() ]; } return []; } /** * Прочитать разрешения аккаунта * * @param int $id Идентификатор аккаунта * @param array &$errors Журнал ошибок * * @return array Разрешения аккаунта, если найдены */ public static function permissions(int $id, array &$errors = []): array { 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[]= [ '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 { 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[]= [ 'text' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine(), 'stack' => $e->getTrace() ]; } return null; } /** * Запись пользователя в базу данных * * @param string|null $name Имя * @param string|null $email Почта * @param string|null $password Пароль * @param array &$errors Журнал ошибок * * @return array Аккаунт (если не найден, то пустой массив) */ public static function write(string|null $name = null, string|null $email = null, string|null $password = null, array &$errors = []): array { try { // Инициализация параметров запроса $params = []; if (isset($name)) { try { // Проверка параметра if (iconv_strlen($name) < 3) throw new exception('Длина имени должна быть не менее 3 символов'); if (iconv_strlen($name) > 60) throw new exception('Длина имени должна быть не более 60 символов'); // Запись в буфер параметров запроса $params[':name'] = $name; } catch (exception $e) { // Запись в журнал ошибок $errors[] = [ 'text' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine() ]; goto end; } } if (isset($email)) { try { // Проверка параметра if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) throw new exception('Не удалось распознать почту'); if (iconv_strlen($email) < 3) throw new exception('Длина почты должна быть не менее 3 символов'); if (iconv_strlen($email) > 60) throw new exception('Длина почты должна быть не более 80 символов'); // Запись в буфер параметров запроса $params[':email'] = $email; } catch (exception $e) { // Запись в журнал ошибок $errors[] = [ 'text' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine() ]; goto end; } } if (isset($password)) { try { // Проверка параметра 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); } catch (exception $e) { // Запись в журнал ошибок $errors[] = [ 'text' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine() ]; goto end; } } // Инициализация запроса $request = static::$db->prepare("INSERT INTO `accounts` (" . (isset($name) ? '`name`' : '') . (isset($name) && isset($email) ? ', ' : '') . (isset($email) ? '`email`' : '') . ((isset($name) || isset($email)) && isset($password) ? ', ' : '') . (isset($password) ? '`password`' : '') . ") VALUES (" . (isset($name) ? ':name' : '') . (isset($name) && isset($email) ? ', ' : '') . (isset($email) ? ':email' : '') . ((isset($name) || isset($email)) && isset($password) ? ', ' : '') . (isset($password) ? ':password' : '') . ")"); // Отправка запроса $request->execute($params); // Генерация ответа $request->fetch(pdo::FETCH_ASSOC); try { if (isset($name)) { // Передано имя аккаунта // Чтение аккаунта $account = static::read(['name' => $name]); } else if (isset($email)) { // Передана почта аккаунта // Чтение аккаунта $account = static::read(['email' => $email]); } else { // Не передано ни имя, ни почта throw new exception('Не переданны данные для полноценной регистрации аккаунта'); } // Инициализация запроса $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[] = [ 'text' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine(), 'stack' => $e->getTrace() ]; } } catch (exception $e) { // Запись в журнал ошибок $errors[]= [ 'text' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine(), 'stack' => $e->getTrace() ]; } // Конец выполнения end: return isset($account) && $account ? $account : []; } /** * Чтение пользователя из базы данных * * @param array $search Поиск ('поле' => 'значение'), работает только с одним полем * @param array &$errors Журнал ошибок * * @return array Аккаунт, если найден */ public static function read(array $search, array &$errors = []): array { try { // Инициализация данных для поиска $field = array_keys($search)[0] ?? null; $value = $search[$field] ?? null; if (empty($field)) { // Получено пустое значение поля // Запись ошибки throw new exception('Пустое значение поля для поиска'); } // Инициализация запроса $request = static::$db->prepare("SELECT * FROM `accounts` WHERE `$field` = :field LIMIT 1"); // Параметры запроса $params = [ ":field" => $value, ]; // Отправка запроса $request->execute($params); // Генерация ответа if ($account = $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[] = [ 'text' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine() ]; } } else { // Не найден аккаунт throw new exception('Не удалось найти аккаунт'); } } catch (exception $e) { // Запись в журнал ошибок $errors[]= [ 'text' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine(), 'stack' => $e->getTrace() ]; } return isset($account) && $account ? $account : []; } /** * Запись или чтение хеша из базы данных * * @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 { 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[]= [ 'text' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine(), 'stack' => $e->getTrace() ]; } return ['hash' => $hash, 'time' => $time]; } }