639 lines
23 KiB
PHP
639 lines
23 KiB
PHP
<?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 array &$errors Журнал ошибок
|
||
*
|
||
* @return bool|null Статус разрешения, если оно записано
|
||
*/
|
||
public function access(string $permission, array &$errors = []): ?bool
|
||
{
|
||
// Инициализация журнала ошибок
|
||
$errors['account'] ?? $errors['account'] = [];
|
||
|
||
try {
|
||
return isset($this->permissions[$permission]) ? (bool) $this->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];
|
||
}
|
||
}
|