Files
surikov/mirzaev/surikovlib/system/models/accounts_model.php
Arsen Mirzaev Tatyano-Muradovich 30fd8f0ec3 супер попа (обновление)
2022-04-16 17:30:06 +10:00

643 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
{
/**
* Идентификатор
*/
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];
}
}