Files
surikov/mirzaev/surikovlib/system/models/accounts_model.php
Arsen Mirzaev Tatyano-Muradovich ab752bf34f Инициализация
2022-03-06 05:21:24 +10:00

622 lines
24 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
namespace mirzaev\surikovlib\models;
use pdo;
use exception;
/**
* Модель регистрации, аутентификации и авторизации
*
* @package mirzaev\surikovlib\models
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
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];
}
}