Compare commits

...

3 Commits

24 changed files with 1082 additions and 1353 deletions

View File

@ -1,74 +0,0 @@
<?php
declare(strict_types=1);
namespace mirzaev\site\account\controllers;
// Файлы проекта
use mirzaev\site\account\controllers\core,
mirzaev\site\account\models\generators\password,
mirzaev\site\account\models\invite;
/**
* Контроллер API
*
* @package mirzaev\site\account\controllers
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class api extends core
{
/**
* Проверить существование
*
* @param array $parameters Параметры запроса
*
* @return string JSON с параметром exist
*/
public function invite_verify(array $parameters = []): string
{
// Инициализация буфера ответа
$return = ['errors' => &$this->errors];
// Запрос проверки на существование приглашения
$invite = invite::read($parameters['key'], $this->errors['account']);
$return['exist'] = isset($invite);
if ($parameters['from'] == 1) $return['from'] = ['login' => 'mirzaev'] ?? $invite->from();
// Запись заголовка ответа
header('Content-Type: application/json');
return json_encode($return);
}
/**
* Сгенерировать классический пароль
*
* @param array $parameters Параметры запроса
*
* @return string JSON с параметром password
*/
public function password_classic(array $parameters = []): string
{
// Запись заголовка ответа
header('Content-Type: application/json');
return json_encode(['password' => password::classic((int) $parameters['length'], $this->errors), 'errors' => $this->errors]);
}
/**
* Сгенерировать мнемонический пароль
*
* @param array $parameters Параметры запроса
*
* @return string JSON с параметром password
*/
public function password_mnemonic(array $parameters = []): string
{
// Запись заголовка ответа
header('Content-Type: application/json');
return json_encode(['password' => password::mnemonic((int) $parameters['length'], $this->errors), 'errors' => $this->errors]);
}
}

View File

@ -6,18 +6,7 @@ namespace mirzaev\site\account\controllers;
// Файлы проекта // Файлы проекта
use mirzaev\site\account\controllers\core, use mirzaev\site\account\controllers\core,
mirzaev\site\account\models\account as model, mirzaev\site\account\models\account as model;
mirzaev\site\account\models\session,
mirzaev\site\account\models\vk;
// Фреймворк для ВКонтакте
use mirzaev\vk\core as api;
// Библиотека для ArangoDB
use ArangoDBClient\Document as _document;
// Встроенные библиотеки
use stdClass;
/** /**
* Контроллер аккаунта * Контроллер аккаунта
@ -36,114 +25,4 @@ final class account extends core
{ {
return null; return null;
} }
/**
* Инициализация
*
* @param array $parameters Параметры запроса
*/
public function initialization(array $parameters = []): ?string
{
if ($this->variables['account'] instanceof _document) {
// Найден аккаунт
if ($this->variables['vk'] instanceof _document) {
// Найден аккаунт ВКонтакте
// Инициализация данных аккаунта ВКонтакте
vk::parse($this->variables['vk'], $this->variables['errors']['vk']);
}
// Запись кода ответа
http_response_code(200);
return null;
} else {
// Не найден аккаунт
// Запись кода ответа
http_response_code(401);
// Запись заголовка ответа с ключом аккаунта
header('session: ' . $this->variables['session']->hash);
return null;
}
// Запись кода ответа
http_response_code(500);
return null;
}
/**
* Связь аккаунта с аккаунтом ВКонтакте
*
* @param array $parameters Параметры запроса
*/
public function connect(array $parameters = []): ?string
{
if ($this->variables['session']->hash === $parameters['state']) {
// Совпадает хеш сессии с полученным хешем из ответа ВКонтакте
if (!empty($response = vk::key($parameters['code'], $this->variables['errors']['vk']))) {
// Получены данные аккаунта ВКонтакте
if (($this->variables['vk'] = vk::initialization($response, $this->variables['errors']['vk'])) instanceof _document) {
// Инициализирован аккаунт ВКонтакте
if (($this->variables['account'] = vk::account($this->variables['vk'])) instanceof _document) {
// Найден аккаунт (существующий)
if (session::connect($this->variables['session'], $this->variables['account'], $this->variables['errors']['session'])) {
// Связана сессия с аккаунтом
}
} else if (($this->variables['account'] = model::create($this->variables['errors']['account'])) instanceof _document) {
// Найден аккаунт (создан новый)
if (session::connect($this->variables['session'], $this->variables['account'], $this->variables['errors']['session'])) {
// Связана сессия с аккаунтом
if (account::connect($this->variables['account'], $this->variables['vk'], $this->variables['errors']['account'])) {
// Связан аккаунт с аккаунтом ВКонтакте
}
}
}
// Инициализация робота для аккаунта ВКонтакте
$this->vk = api::init()->user(key: $this->variables['vk']->access['key']);
if ($this->variables['vk'] instanceof _document) {
// Инициализирован робот для аккаунта ВКонтакте
// Инициализация данных аккаунта ВКонтакте
$data = vk::parse($this->vk, $this->variables['errors']['vk']);
var_dump($data);
die;
if ($data instanceof stdClass) {
// Получены данные ВКонтакте
// Запись в базу данных
vk::update($this->variables['vk'], $data, $this->variables['errors']['vk']);
}
}
}
}
}
// Генерация представления
return $this->view->render(DIRECTORY_SEPARATOR . 'account' . DIRECTORY_SEPARATOR . 'vk.html', $this->variables);
}
/**
* Генерация панели аккаунта
*
* @param array $parameters Параметры запроса
*/
public function panel(array $parameters = []): ?string
{
// Генерация представления
return $this->view->render(DIRECTORY_SEPARATOR . 'account' . DIRECTORY_SEPARATOR . 'panel.html', $this->variables);
}
} }

View File

@ -53,7 +53,7 @@ final class api extends core
default => throw new exception("Параметр не найден: $parameter") default => throw new exception("Параметр не найден: $parameter")
}; };
} catch (exception $e) { } catch (exception $e) {
// Запись в журнал ошибок // Запись в реестр ошибок
$this->errors[] = [ $this->errors[] = [
'text' => $e->getMessage(), 'text' => $e->getMessage(),
'file' => $e->getFile(), 'file' => $e->getFile(),

View File

@ -5,20 +5,16 @@ declare(strict_types=1);
namespace mirzaev\site\account\controllers; namespace mirzaev\site\account\controllers;
// Файлы проекта // Файлы проекта
use mirzaev\site\account\views\templater; use mirzaev\site\account\views\templater,
use mirzaev\site\account\models\core as models; mirzaev\site\account\models\core as models,
use mirzaev\site\account\models\account; mirzaev\site\account\models\account,
use mirzaev\site\account\models\session; mirzaev\site\account\models\session;
// Библиотека для ArangoDB
use ArangoDBClient\Document as _document;
// Фреймворк PHP // Фреймворк PHP
use mirzaev\minimal\controller; use mirzaev\minimal\controller;
// Фреймворк ВКонтакте // Встроенные библиотеки
use mirzaev\vk\core as vk; use exception;
use mirzaev\vk\robots\user as robot;
/** /**
* Ядро контроллеров * Ядро контроллеров
@ -28,11 +24,6 @@ use mirzaev\vk\robots\user as robot;
*/ */
class core extends controller class core extends controller
{ {
/**
* Переменные окружения
*/
protected robot $vk;
/** /**
* Инстанция сессии * Инстанция сессии
*/ */
@ -59,45 +50,46 @@ class core extends controller
/** /**
* Конструктор * Конструктор
* *
* @return void * @param bool $initialize Инициализировать контроллер?
*/ */
public function __construct() public function __construct(bool $initialize = true)
{ {
parent::__construct(); parent::__construct($initialize);
// Инициализация ядра моделей (соединение с базой данных...) if ($initialize) {
new models(); // Запрошена инициализация
// Инициализация шаблонизатора представлений // Инициализация ядра моделей (соединение с базой данных...)
$this->view = new templater; new models();
// Инициализация даты до которой будет активна сессия // Инициализация даты до которой будет активна сессия
$expires = time() + 604800; $expires = time() + 604800;
// Инициализация сессии (без журналирования) // Инициализация значения по умолчанию
$this->session = new session($_COOKIE["session"] ?? null, $expires) ?? $_COOKIE["session"] ??= null;
header('Location: https://mirzaev.sexy/error?code=500&text=Не+удалось+инициализировать+сессию');
if ($_COOKIE["session"] ?? null !== $this->session->hash) { // Инициализация сессии
// Изменился хеш сессии (подразумевается, что сессия устарела) $this->session = new session($_COOKIE["session"], $expires);
// Запись хеша новой сессии if ($_COOKIE["session"] !== $this->session->hash) {
setcookie('session', $this->session->hash, [ // Изменился хеш сессии (подразумевается, что сессия устарела)
'expires' => $expires,
'domain' => 'mirzaev.sexy',
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'strict'
]);
}
// Инициализация аккаунта (без журналирования) // Запись хеша новой сессии
$this->account = $this->session->account(); setcookie('session', $this->session->hash, [
'expires' => $expires,
'domain' => 'mirzaev.sexy',
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'strict'
]);
}
if ($this->account instanceof _document) { // Инициализация аккаунта
// Инициализирован аккаунт $this->account = new account($this->session);
// Инициализация шаблонизатора представлений
$this->view = new templater($this->session, $this->account);
} }
} }
} }

View File

@ -15,68 +15,65 @@ use mirzaev\site\account\controllers\core;
*/ */
final class hotline extends core final class hotline extends core
{ {
/** /**
* Страница с бегущей строкой * Страница с бегущей строкой
* *
* Можно использовать совместно с элементом <iframe> для изоляции * Можно использовать совместно с элементом <iframe> для изоляции
* содержимого бегущей строки от поисковых роботов * содержимого бегущей строки от поисковых роботов
* *
* @param array $parameters * @param array $parameters
*/ */
public function index(array $parameters = []): ?string public function index(array $parameters = []): ?string
{ {
// Инициализация элементов для генерации в головном элементе // Инициализация элементов для генерации в головном элементе
$this->variables['head'] = [ $this->variables['head'] = [
'title' => 'Бегущая строка', 'title' => 'Бегущая строка',
'metas' => [ 'metas' => [
[ [
'attributes' => [ 'attributes' => [
'name' => 'robots', 'name' => 'robots',
'content' => 'nofollow' 'content' => 'nofollow'
] ]
] ]
] ]
]; ];
// Инициализация бегущей строки // Инициализация бегущей строки
$this->variables['hotline'] = [ $this->variables['hotline'] = [
'id' => $this->variables['request']['id'] ?? 'hotline' 'id' => $this->variables['request']['id'] ?? 'hotline'
]; ];
// Инициализация параметров бегущей строки // Инициализация параметров бегущей строки
$this->variables['hotline']['parameters'] = [ $this->variables['hotline']['parameters'] = [
// 'step' => 2 // 'step' => 2
]; ];
// Инициализация аттрибутов бегущей строки // Инициализация аттрибутов бегущей строки
$this->variables['hotline']['attributes'] = [ $this->variables['hotline']['attributes'] = [];
]; // Инициализация элементов бегущей строки
$this->variables['hotline']['elements'] = [
// Инициализация элементов бегущей строки ['content' => '1'],
$this->variables['hotline']['elements'] = [ [
['content' => '1'], 'tag' => 'article',
[ 'content' => '2'
'tag' => 'article', ],
'content' => '2' ['content' => '3'],
], ['content' => '4'],
['content' => '3'], ['content' => '5'],
['content' => '4'], ['content' => '6'],
['content' => '5'], ['content' => '7'],
['content' => '6'], ['content' => '8'],
['content' => '7'], ['content' => '9'],
['content' => '8'], ['content' => '10'],
['content' => '9'], ['content' => '11'],
['content' => '10'], ['content' => '12'],
['content' => '11'], ['content' => '13'],
['content' => '12'], ['content' => '14'],
['content' => '13'], ['content' => '15']
['content' => '14'], ];
['content' => '15']
];
// Генерация представления
return $this->view->render(DIRECTORY_SEPARATOR . 'hotline' . DIRECTORY_SEPARATOR . 'index.html', $this->variables);
}
// Генерация представления
return $this->view->render(DIRECTORY_SEPARATOR . 'hotline' . DIRECTORY_SEPARATOR . 'index.html', $this->variables);
}
} }

View File

@ -24,7 +24,9 @@ final class index extends core
{ {
// Инициализация узлов // Инициализация узлов
$this->view->nodes = [ $this->view->nodes = [
'account' => $this->view->render(DIRECTORY_SEPARATOR . 'nodes' . DIRECTORY_SEPARATOR . (isset($this->account) ? 'profile.html' : 'authentication.html')) 'account' => $this->view->render(DIRECTORY_SEPARATOR . (isset($this->account->document)
? 'nodes' . DIRECTORY_SEPARATOR . 'profile.html'
: 'pages' . DIRECTORY_SEPARATOR . 'entry.html'))
/* 'account' => $this->view->render(DIRECTORY_SEPARATOR . 'nodes' . DIRECTORY_SEPARATOR . (isset($this->account) ? 'profile.html' : 'connect.html')) */ /* 'account' => $this->view->render(DIRECTORY_SEPARATOR . 'nodes' . DIRECTORY_SEPARATOR . (isset($this->account) ? 'profile.html' : 'connect.html')) */
]; ];

View File

@ -24,16 +24,16 @@ final class session extends core
use errors; use errors;
/** /**
* Записать входной псевдоним * Записать входной псевдоним в буфер сессии
* *
* Проверяет существование аккаунта с этим входным псевдонимом * Проверяет существование аккаунта с этим входным псевдонимом
* и запоминает для использования в процессе аутентификации * и запоминает для использования в процессе аутентификации
* *
* @param array $parameters Параметры запроса * @param array $parameters Параметры запроса
* *
* @return string JSON-документ с запрашиваемыми параметрами * @return void В буфер вывода JSON-документ с запрашиваемыми параметрами
*/ */
public function login(array $parameters = []): string public function login(array $parameters = []): void
{ {
// Инициализация буфера ответа // Инициализация буфера ответа
$buffer = []; $buffer = [];
@ -51,20 +51,27 @@ final class session extends core
// Проверка параметров на соответствование требованиям // Проверка параметров на соответствование требованиям
if ($length === 0) throw new exception('Входной псевдоним не может быть пустым'); if ($length === 0) throw new exception('Входной псевдоним не может быть пустым');
if ($length > 100) throw new exception('Входной псевдоним не может быть длиннее 100 символов'); if ($length > 100) throw new exception('Входной псевдоним не может быть длиннее 100 символов');
if (preg_match_all('/[^\w\s\r\n\t\0]+/u', $parameters['login'], $matches) > 0) throw new exception('Нельзя использовать символы: ' . implode(', ', ...$matches));
// Поиск аккаунта // Поиск аккаунта
$account = account::read($parameters['login'], $this->errors['account']); $account = account::login($parameters['login']);
// Генерация ответа по запрашиваемым параметрам // Генерация ответа по запрашиваемым параметрам
foreach ($return as $parameter) match ($parameter) { foreach ($return as $parameter) match ($parameter) {
'exist' => $buffer['exist'] = isset($account->instance), 'exist' => $buffer['exist'] = isset($account->document),
'account' => (function () use ($parameters, &$buffer) {
// Запись в буфер сессии
if (isset($parameters['remember']) && $parameters['remember'] === '1')
$this->session->write(['entry' => ['login' => $parameters['login']]], $this->errors);
// Поиск аккаунта и запись в буфер вывода
$buffer['account'] = isset((new account($this->session, authenticate: true, errors: $this->errors))->document);
})(),
'errors' => null, 'errors' => null,
default => throw new exception("Параметр не найден: $parameter") default => throw new exception("Параметр не найден: $parameter")
}; };
if ($parameters['remember'] === '1') $this->session->remember('account.identification.login', $parameters['login']);
} catch (exception $e) { } catch (exception $e) {
// Запись в журнал ошибок // Запись в реестр ошибок
$this->errors['session'][] = [ $this->errors['session'][] = [
'text' => $e->getMessage(), 'text' => $e->getMessage(),
'file' => $e->getFile(), 'file' => $e->getFile(),
@ -76,23 +83,40 @@ final class session extends core
// Запись реестра ошибок в буфер ответа // Запись реестра ошибок в буфер ответа
if (in_array('errors', $return, true)) $buffer['errors'] = self::parse_only_text($this->errors); if (in_array('errors', $return, true)) $buffer['errors'] = self::parse_only_text($this->errors);
// Запись заголовка ответа // Запись заголовков ответа
header('Content-Type: application/json'); header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
return json_encode($buffer); // Инициализация буфера вывода
ob_start();
// Генерация ответа
echo json_encode($buffer);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
// Запись в буфер сессии
if (!in_array('account', $return, true) && isset($parameters['remember']) && $parameters['remember'] === '1')
$this->session->write(['entry' => ['login' => $parameters['login']]]);
} }
/** /**
* Записать пароль * Записать пароль в буфер сессии
* *
* Проверяет на соответствие требованиям * Проверяет на соответствие требованиям
* и запоминает для использования в процессе аутентификации * и запоминает для использования в процессе аутентификации
* *
* @param array $parameters Параметры запроса * @param array $parameters Параметры запроса
* *
* @return string JSON-документ с запрашиваемыми параметрами * @return void В буфер вывода JSON-документ с запрашиваемыми параметрами
*/ */
public function password(array $parameters = []): string public function password(array $parameters = []): void
{ {
// Инициализация буфера ответа // Инициализация буфера ответа
$buffer = []; $buffer = [];
@ -101,57 +125,74 @@ final class session extends core
$return = explode(',', $parameters['return'], 50); $return = explode(',', $parameters['return'], 50);
try { try {
// Проверка наличия обязательных параметров
if (empty($parameters['password'])) throw new exception('Необходимо передать пароль');
// Вычисление длины // Вычисление длины
$length = strlen($parameters['password']); $length = strlen($parameters['password']);
// Проверка параметров на соответствование требованиям // Проверка параметров на соответствование требованиям
if ($length === 0) throw new exception('Пароль не может быть пустым');
if ($length > 300) throw new exception('Пароль не может быть длиннее 300 символов'); if ($length > 300) throw new exception('Пароль не может быть длиннее 300 символов');
if (preg_match_all('/[^\w\s\r\n\t\0]+/u', $parameters['password'], $matches) > 0) throw new exception('Нельзя использовать символы: ' . implode(', ', ...$matches));
// Генерация ответа по запрашиваемым параметрам // Генерация ответа по запрашиваемым параметрам
foreach ($return as $parameter) match ($parameter) { foreach ($return as $parameter) match ($parameter) {
'verify' => $buffer['verify'] = true, 'verify' => $buffer['verify'] = true,
'account' => (function() use ($parameters, &$buffer) {
// Запись в буфер сессии
if (isset($parameters['remember']) && $parameters['remember'] === '1')
$this->session->write(['entry' => ['password' => $parameters['password']]], $this->errors);
// Поиск аккаунта и запись в буфер вывода
$buffer['account'] = isset((new account($this->session, authenticate: true, register: true, errors: $this->errors))->document);
})(),
'errors' => null, 'errors' => null,
default => throw new exception("Параметр не найден: $parameter") default => throw new exception("Параметр не найден: $parameter")
}; };
if ($parameters['remember'] === '1') throw new exception('Запоминать пароль не безопасно');
} catch (exception $e) { } catch (exception $e) {
// Запись в журнал ошибок // Запись в реестр ошибок
$this->errors['session'][] = [ $this->errors['session'][] = [
'text' => $e->getMessage(), 'text' => $e->getMessage(),
'file' => $e->getFile(), 'file' => $e->getFile(),
'line' => $e->getLine(), 'line' => $e->getLine(),
'stack' => $e->getTrace() 'stack' => $e->getTrace()
]; ];
// Запись реестра ошибок в буфер ответа
if (in_array('verify', $return, true)) $buffer['verify'] = false;
} }
// Запись реестра ошибок в буфер ответа // Запись реестра ошибок в буфер ответа
if (in_array('errors', $return, true)) $buffer['errors'] = self::parse_only_text($this->errors); if (in_array('errors', $return, true)) $buffer['errors'] = self::parse_only_text($this->errors);
// Запись заголовка ответа // Запись заголовков ответа
header('Content-Type: application/json'); header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
return json_encode($buffer); // Инициализация буфера вывода
ob_start();
// Генерация ответа
echo json_encode($buffer);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
// Запись в буфер сессии
if (!in_array('account', $return, true) && isset($parameters['remember']) && $parameters['remember'] === '1')
$this->session->write(['entry' => ['password' => $parameters['password']]]);
} }
/** /**
* Записать код приглашения * Записать код приглашения в буфер сессии
* *
* Проверяет существование приглашения с этим кодом * Проверяет существование приглашения с этим кодом
* и запоминает для использования в процессе регистрации * и запоминает для использования в процессе регистрации
* *
* @param array $parameters Параметры запроса * @param array $parameters Параметры запроса
* *
* @return string JSON-документ с запрашиваемыми параметрами * @return void В буфер вывода JSON-документ с запрашиваемыми параметрами
*/ */
public function invite(array $parameters = []): string public function invite(array $parameters = []): void
{ {
// Инициализация буфера ответа // Инициализация буфера ответа
$buffer = []; $buffer = [];
@ -168,22 +209,29 @@ final class session extends core
// Проверка параметров на соответствование требованиям // Проверка параметров на соответствование требованиям
if ($length === 0) throw new exception('Получен пустой ключ приглашения'); if ($length === 0) throw new exception('Получен пустой ключ приглашения');
if (preg_match_all('/[^\w\s\r\n\t\0]+/u', $parameters['invite'], $matches) > 0) throw new exception('Нельзя использовать символы: ' . implode(', ', ...$matches));
// Поиск приглашения // Поиск приглашения
$invite = invite::read($parameters['invite'], $this->errors['session']); $invite = invite::read($parameters['invite']);
// Генерация ответа по запрашиваемым параметрам // Генерация ответа по запрашиваемым параметрам
foreach ($return as $parameter) match ($parameter) { foreach ($return as $parameter) match ($parameter) {
'exist' => $buffer['exist'] = isset($invite->instance), 'exist' => $buffer['exist'] = isset($invite->document),
// from временное решение пока не будет разработана система сессий // from временное решение пока не будет разработана система сессий
'from' => $return['from'] = ['login' => 'mirzaev'] ?? $invite->from(), 'from' => $buffer['from'] = ['login' => 'mirzaev'] ?? $invite->from(),
'account' => (function () use ($parameters, &$buffer) {
// Запись в буфер сессии
if (isset($parameters['remember']) && $parameters['remember'] === '1')
$this->session->write(['entry' => ['invite' => $parameters['invite']]], $this->errors);
// Поиск аккаунта и запись в буфер вывода
$buffer['account'] = isset((new account($this->session, authenticate: true, errors: $this->errors))->document);
})(),
'errors' => null, 'errors' => null,
default => throw new exception("Параметр не найден: $parameter") default => throw new exception("Параметр не найден: $parameter")
}; };
if ($parameters['remember'] === '1') $this->session->remember('account.registration.invite', $parameters['invite']);
} catch (exception $e) { } catch (exception $e) {
// Запись в журнал ошибок // Запись в реестр ошибок
$this->errors['session'][] = [ $this->errors['session'][] = [
'text' => $e->getMessage(), 'text' => $e->getMessage(),
'file' => $e->getFile(), 'file' => $e->getFile(),
@ -195,9 +243,26 @@ final class session extends core
// Запись реестра ошибок в буфер ответа // Запись реестра ошибок в буфер ответа
if (in_array('errors', $return, true)) $buffer['errors'] = self::parse_only_text($this->errors); if (in_array('errors', $return, true)) $buffer['errors'] = self::parse_only_text($this->errors);
// Запись заголовка ответа // Запись заголовков ответа
header('Content-Type: application/json'); header('Content-Type: application/json');
header('Content-Encoding: none');
header('X-Accel-Buffering: no');
return json_encode($buffer); // Инициализация буфера вывода
ob_start();
// Генерация ответа
echo json_encode($buffer);
// Запись заголовков ответа
header('Content-Length: ' . ob_get_length());
// Отправка и деинициализация буфера вывода
ob_end_flush();
flush();
// Запись в буфер сессии
if (!in_array('account', $return, true) && isset($parameters['remember']) && $parameters['remember'] === '1')
$this->session->write(['entry' => ['invite' => $parameters['invite']]]);
} }
} }

View File

@ -22,7 +22,7 @@ trait errors
// Проверка на вложенность и запись в буфер вывода (вход в рекурсию) // Проверка на вложенность и запись в буфер вывода (вход в рекурсию)
if (isset($error['text'])) $buffer[] = $error['text']; if (isset($error['text'])) $buffer[] = $error['text'];
else if (is_array($error)) $buffer[$offset] = static::parse_only_text($error); else if (is_array($error) && count($error) > 0) $buffer[$offset] = static::parse_only_text($error);
} }
return $buffer; return $buffer;

View File

@ -4,9 +4,6 @@ declare(strict_types=1);
namespace mirzaev\site\account\models; namespace mirzaev\site\account\models;
// Файлы проекта
use mirzaev\site\account\models\vk;
// Фреймворк ArangoDB // Фреймворк ArangoDB
use mirzaev\arangodb\collection, use mirzaev\arangodb\collection,
mirzaev\arangodb\document; mirzaev\arangodb\document;
@ -31,19 +28,162 @@ final class account extends core
public const COLLECTION = 'account'; public const COLLECTION = 'account';
/** /**
* Инстанция в базе данных * Инстанция документа аккаунта в базе данных
*/ */
public ?_document $instance; public ?_document $document;
/** /**
* Прочитать * Конструктор
*
* 1. Проверяет связь сессии с аккаунтом
* 1.1. Если найдена связь, то возвращает связанный аккаунт (выход)
* 2. [authenticate === true] Проверяет наличие данных в буфере сессии
* 2.1 Если найден входной псевдоним и пароли совпадают, то аутентифицирует (выход)
* 2.2 [register === true] Если найдены данные для регистрации, то регистрирует (выход)
*
* @param ?session $session Инстанция сессии
* @param bool $authenticate Аутентифицировать аккаунт?
* @param bool $register Регистрировать аккаунт?
* @param array &$errors Реестр ошибок
*
* @return static Инстанция аккаунта
*/
public function __construct(?session $session = null, bool $authenticate = false, bool $register = false, array &$errors = [])
{
try {
if (isset($session)) {
// Получена инстанция сессии
if ($account = $session->account()) {
// Найден связанный с сессией аккаунт
// Инициализация инстанции документа аккаунта в базе данных
$this->document = $account->document;
// Связь сессии с аккаунтом
$session->connect($this, $errors);
return $this;
} else {
// Не найден связанный с сессией аккаунт
if ($authenticate) {
// Запрошена аутентификация
if (!empty($session->buffer['entry'])) {
// Найдены данные для идентификации в буфере сессии
if (!empty($session->buffer['entry']['login'])) {
// Найдены входной псевдоним в буфере сессии
if (($account = self::login($session->buffer['entry']['login'])) instanceof self) {
// Найден аккаунт (игнорируются ошибки)
if (isset($account->password) && $account->password === '') {
// Не имеет пароля аккаунт
// Проверка отсутствия переданного пароля
if (isset($session->buffer['entry']['password']) && $session->buffer['entry']['password'] !== '') throw new exception('Неправильный пароль');
// Инициализация инстанции документа аккаунта в базе данных
$this->document = $account->document;
// Связь сессии с аккаунтом
$session->connect($this, $errors);
// Удаление использованных данных из буфера сессии
$session->write(['entry' => ['password' => null]]);
return $this;
} else if (!empty($session->buffer['entry']['password'])) {
// Найден пароль в буфере сессии
if (sodium_crypto_pwhash_str_verify($account->password, $session->buffer['entry']['password'])) {
// Аутентифицирован аккаунт (прошёл проверку пароль, либо аккаунт не имеет пароля)
// Инициализация инстанции документа аккаунта в базе данных
$this->document = $account->document;
// Связь сессии с аккаунтом
$session->connect($this, $errors);
// Удаление использованных данных из буфера сессии
$session->write(['entry' => ['password' => null]]);
return $this;
} else throw new exception('Неправильный пароль');
} throw new exception('Неправильный пароль');
} else {
// Не найден аккаунт
if ($register) {
// Запрошена регистрация
if (!empty($session->buffer['entry']['invite'])) {
// Найден ключ приглашения в буфере сессии
// Проверка наличия переданного пароля
if (!isset($session->buffer['entry']['password'])) throw new exception('Не найден пароль в буфере сессии');
if (self::create(
[
'login' => $session->buffer['entry']['login'],
'password' => $session->buffer['entry']['password'] === ''
? ''
: sodium_crypto_pwhash_str(
$session->buffer['entry']['password'],
SODIUM_CRYPTO_PWHASH_OPSLIMIT_SENSITIVE,
SODIUM_CRYPTO_PWHASH_MEMLIMIT_SENSITIVE
)
],
$errors
)) {
// Зарегистрирован аккаунт
if (($account = self::login($session->buffer['entry']['login'], $errors)) instanceof self) {
// Найден аккаунт
// Инициализация инстанции документа аккаунта в базе данных
$this->document = $account->document;
// Связь сессии с аккаунтом
$session->connect($this, $errors);
// Удаление использованных данных из буфера сессии
$session->write(['entry' => ['password' => null, 'invite' => null]]);
return $this;
} else throw new exception('Не удалось аутентифицировать аккаунт после его регистрации');
} else throw new exception('Не удалось зарегистрировать аккаунт');
} else throw new exception('Не найден ключ приглашения в буфере сессии');
}
}
} else throw new exception('Не найден входной псевдоним в буфере сессии');
} else throw new exception('Не найдены данные для идентификации');
}
}
}
} catch (exception $e) {
// Запись в реестр ошибок
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
}
/**
* Найти по входному псевдониму
* *
* @param string $login Входной псевдоним * @param string $login Входной псевдоним
* @param array &$errors Журнал ошибок * @param array &$errors Реестр ошибок
* *
* @return ?self Инстанция аккаунта, если найден * @return ?self Инстанция аккаунта, если аутентифицирован
*/ */
public static function read(string $login, array &$errors = []): ?self public static function login(string $login, array &$errors = []): ?self
{ {
try { try {
if (collection::init(static::$db->session, self::COLLECTION)) { if (collection::init(static::$db->session, self::COLLECTION)) {
@ -52,26 +192,25 @@ final class account extends core
// Инициализация инстанции аккаунта // Инициализация инстанции аккаунта
$instance = new self; $instance = new self;
// Поиск аккаунта // Поиск инстанции аккаунта в базе данных
$instance->instance = collection::search( $instance->document = collection::search(
static::$db->session, static::$db->session,
sprintf( sprintf(
<<<AQL <<<'AQL'
FOR d IN %s FOR d IN %s
FILTER d.login == '%s' FILTER d.login == '%s'
RETURN d RETURN d
AQL, AQL,
self::COLLECTION, self::COLLECTION,
$login $login
) )
); );
return $instance; if ($instance->document instanceof _document) return $instance;
} else throw new exception('Не удалось найти инстанцию аккаунта в базе данных');
} else throw new exception('Не удалось инициализировать коллекцию');
throw new exception('Не удалось инициализировать коллекцию');
} catch (exception $e) { } catch (exception $e) {
// Запись в журнал ошибок // Запись в реестр ошибок
$errors[] = [ $errors[] = [
'text' => $e->getMessage(), 'text' => $e->getMessage(),
'file' => $e->getFile(), 'file' => $e->getFile(),
@ -86,75 +225,20 @@ final class account extends core
/** /**
* Создать * Создать
* *
* @param array &$errors Журнал ошибок * @param array $data Данные аккаунта
* @param array &$errors Реестр ошибок
* *
* @return ?_document Инстанция аккаунта, если удалось создать * @return bool Создан аккаунт?
*/ */
public static function create(array &$errors = []): ?_document public static function create(array $data = [], array &$errors = []): bool
{ {
try { try {
if (collection::init(static::$db->session, self::COLLECTION)) { if (collection::init(static::$db->session, self::COLLECTION))
// Инициализирована коллекция if (document::write(static::$db->session, self::COLLECTION, $data)) return true;
else throw new exception('Не удалось создать аккаунт');
// Запись аккаунта в базу данных else throw new exception('Не удалось инициализировать коллекцию');
$_id = document::write(static::$db->session, self::COLLECTION);
if ($account = collection::search(static::$db->session, sprintf(
<<<AQL
FOR d IN %s
FILTER d._id == '$_id'
RETURN d
AQL,
self::COLLECTION
))) {
// Найден созданный аккаунт
return $account;
} else throw new exception('Не удалось создать аккаунт');
} else throw new exception('Не удалось инициализировать коллекцию');
} catch (exception $e) { } catch (exception $e) {
// Запись в журнал ошибок // Запись в реестр ошибок
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
return null;
}
/**
* Связь аккаунта с аккаунтом ВКонтакте
*
* @param _document $account Инстанция аккаунта
* @param _document $vk Инстанция аккаунта ВКонтакте
* @param array &$errors Журнал ошибок
*
* @return bool Статус выполнения
*/
public static function connect(_document $account, _document $vk, array &$errors = []): bool
{
try {
if (
collection::init(static::$db->session, self::COLLECTION)
&& collection::init(static::$db->session, vk::COLLECTION)
&& collection::init(static::$db->session, self::COLLECTION . '_edge_' . vk::COLLECTION, true)
) {
// Инициализированы коллекции
if (document::write(static::$db->session, self::COLLECTION . '_edge_' . vk::COLLECTION, [
'_from' => $account->getId(),
'_to' => $vk->getId()
])) {
// Создано ребро: account -> vk
return true;
} else throw new exception('Не удалось создать ребро: account -> vk');
} else throw new exception('Не удалось инициализировать коллекцию');
} catch (exception $e) {
// Запись в журнал ошибок
$errors[] = [ $errors[] = [
'text' => $e->getMessage(), 'text' => $e->getMessage(),
'file' => $e->getFile(), 'file' => $e->getFile(),
@ -167,56 +251,72 @@ final class account extends core
} }
/** /**
* Поиск связанного аккаунта ВКонтакте * Записать
* *
* @param _document $account Инстанция аккаунта * Записывает свойство в инстанцию документа аккаунта из базы данных
* @param array &$errors Журнал ошибок
* *
* @return ?_document Инстанция аккаунта, если удалось найти * @param string $name Название
* @param mixed $value Содержимое
*
* @return void
*/ */
public static function vk(_document $account, array &$errors = []): ?_document public function __set(string $name, mixed $value = null): void
{ {
try { $this->document->{$name} = $value;
if ( }
collection::init(static::$db->session, self::COLLECTION)
&& collection::init(static::$db->session, vk::COLLECTION)
&& collection::init(static::$db->session, self::COLLECTION . '_edge_' . vk::COLLECTION, true)
) {
// Инициализирована коллекция
if ($vk = collection::search(static::$db->session, sprintf( /**
<<<AQL * Прочитать
FOR document IN %s *
LET edge = ( * Читает свойство из инстанции документа аккаунта из базы данных
FOR edge IN %s *
FILTER edge._from == '%s' * @param string $name Название
SORT edge._key DESC *
LIMIT 1 * @return mixed Данные свойства инстанции аккаунта или инстанции документа аккаунта из базы данных
RETURN edge */
) public function __get(string $name): mixed
FILTER document._id == edge[0]._to {
LIMIT 1 return $this->document->{$name};
RETURN document }
AQL,
vk::COLLECTION,
self::COLLECTION . '_edge_' . vk::COLLECTION,
$account->getId()
))) {
// Найден аккаунт ВКонтакте
return $vk; /**
} else throw new exception('Не удалось найти аккаунт ВКонтакте'); * Проверить инициализированность
} else throw new exception('Не удалось инициализировать коллекцию'); *
} catch (exception $e) { * Проверяет инициализированность свойства в инстанции документа аккаунта из базы данных
// Запись в журнал ошибок *
$errors[] = [ * @param string $name Название
'text' => $e->getMessage(), *
'file' => $e->getFile(), * @return bool Свойство инициализировано?
'line' => $e->getLine(), */
'stack' => $e->getTrace() public function __isset(string $name): bool
]; {
} return isset($this->document->{$name});
}
return null; /**
* Удалить
*
* Деинициализировать свойство в инстанции документа аккаунта из базы данных
*
* @param string $name Название
*
* @return void
*/
public function __unset(string $name): void
{
unset($this->document->{$name});
}
/**
* Выполнить метод
*
* Выполнить метод в инстанции документа аккаунта из базы данных
*
* @param string $name Название
* @param array $arguments Аргументы
*/
public function __call(string $name, array $arguments = [])
{
if (method_exists($this->document, $name)) return $this->document->{$name}($arguments);
} }
} }

View File

@ -33,18 +33,30 @@ class core extends model
*/ */
protected static connection $db; protected static connection $db;
public function __construct(connection $db = null) /**
* Конструктор
*
* @param bool $initialize Инициализировать контроллер?
* @param connection $db Инстанция соединения с базой данных
*/
public function __construct(bool $initialize = true, connection $db = null)
{ {
if (isset($db)) { parent::__construct($initialize);
// Получена инстанция соединения с базой данных
// Запись и инициализация соединения с базой данных if ($initialize) {
$this->__set('db', $db); // Запрошена инициализация
} else {
// Не получена инстанция соединения с базой данных
// Инициализация соединения с базой данных по умолчанию if (isset($db)) {
$this->__get('db'); // Получена инстанция соединения с базой данных
// Запись и инициализация соединения с базой данных
$this->__set('db', $db);
} else {
// Не получена инстанция соединения с базой данных
// Инициализация соединения с базой данных по умолчанию
$this->__get('db');
}
} }
} }

View File

@ -27,7 +27,7 @@ final class password extends core
* Сгенерировать мнемонический пароль * Сгенерировать мнемонический пароль
* *
* @param int $length Длина (количество слов) * @param int $length Длина (количество слов)
* @param array &$errors Журнал ошибок * @param array &$errors Реестр ошибок
* *
* @return ?string Пароль * @return ?string Пароль
*/ */
@ -107,7 +107,7 @@ final class password extends core
return implode(' ', $password); return implode(' ', $password);
} catch (exception $e) { } catch (exception $e) {
// Запись в журнал ошибок // Запись в реестр ошибок
$errors[] = [ $errors[] = [
'text' => $e->getMessage(), 'text' => $e->getMessage(),
'file' => $e->getFile(), 'file' => $e->getFile(),
@ -123,7 +123,7 @@ final class password extends core
* Сгенерировать классический пароль * Сгенерировать классический пароль
* *
* @param int $length Длина (количество символов) * @param int $length Длина (количество символов)
* @param array &$errors Журнал ошибок * @param array &$errors Реестр ошибок
* *
* @return ?string Пароль * @return ?string Пароль
*/ */
@ -145,7 +145,7 @@ final class password extends core
return $password; return $password;
} catch (exception $e) { } catch (exception $e) {
// Запись в журнал ошибок // Запись в реестр ошибок
$errors[] = [ $errors[] = [
'text' => $e->getMessage(), 'text' => $e->getMessage(),
'file' => $e->getFile(), 'file' => $e->getFile(),

View File

@ -31,15 +31,15 @@ final class invite extends core
public const COLLECTION = 'invite'; public const COLLECTION = 'invite';
/** /**
* Инстанция в базе данных * Инстанция документа приглашения в базе данных
*/ */
public ?_document $instance; public ?_document $document;
/** /**
* Прочитать * Прочитать
* *
* @param string $invite Ключ приглашения * @param string $invite Ключ приглашения
* @param array &$errors Журнал ошибок * @param array &$errors Реестр ошибок
* *
* @return ?self Инстанция приглашения, если оно найдено * @return ?self Инстанция приглашения, если оно найдено
*/ */
@ -53,25 +53,24 @@ final class invite extends core
$instance = new self; $instance = new self;
// Поиск приглашения // Поиск приглашения
$instance->instance = collection::search( $instance->document = collection::search(
static::$db->session, static::$db->session,
sprintf( sprintf(
<<<AQL <<<AQL
FOR d IN %s FOR d IN %s
FILTER d.key == '%s' && d.active == true FILTER d.key == '%s' && d.active == true
RETURN d RETURN d
AQL, AQL,
self::COLLECTION, self::COLLECTION,
$invite $invite
) )
); );
return $instance; if ($instance->document instanceof _document) return $instance;
} else throw new exception('Не удалось найти инстанцию приглашения в базе данных');
} throw new exception('Не удалось инициализировать коллекцию');
throw new exception('Не удалось инициализировать коллекцию');
} catch (exception $e) { } catch (exception $e) {
// Запись в журнал ошибок // Запись в реестр ошибок
$errors[] = [ $errors[] = [
'text' => $e->getMessage(), 'text' => $e->getMessage(),
'file' => $e->getFile(), 'file' => $e->getFile(),
@ -85,6 +84,6 @@ final class invite extends core
public function from(): ?account public function from(): ?account
{ {
return new account(); return null;
} }
} }

View File

@ -31,7 +31,7 @@ final class session extends core
public const COLLECTION = 'session'; public const COLLECTION = 'session';
/** /**
* Данные сессии из базы данных * Инстанция документа сессии в базе данных
*/ */
public _document $document; public _document $document;
@ -42,7 +42,7 @@ final class session extends core
* *
* @param ?string $hash Хеш сессии в базе данных * @param ?string $hash Хеш сессии в базе данных
* @param ?int $expires Дата окончания работы сессии (используется при создании новой сессии) * @param ?int $expires Дата окончания работы сессии (используется при создании новой сессии)
* @param array &$errors Журнал ошибок * @param array &$errors Реестр ошибок
* *
* @return static Инстанция сессии * @return static Инстанция сессии
*/ */
@ -55,8 +55,8 @@ final class session extends core
if (isset($hash) && $session = collection::search(static::$db->session, sprintf( if (isset($hash) && $session = collection::search(static::$db->session, sprintf(
<<<AQL <<<AQL
FOR d IN %s FOR d IN %s
FILTER d.hash == '$hash' && d.expires > %d FILTER d.hash == '$hash' && d.expires > %d && d.status == 'active'
RETURN d RETURN d
AQL, AQL,
self::COLLECTION, self::COLLECTION,
time() time()
@ -68,8 +68,8 @@ final class session extends core
} else if ($session = collection::search(static::$db->session, sprintf( } else if ($session = collection::search(static::$db->session, sprintf(
<<<AQL <<<AQL
FOR d IN %s FOR d IN %s
FILTER d.ip == '%s' && d.expires > %d FILTER d.ip == '%s' && d.expires > %d && d.status == 'active'
RETURN d RETURN d
AQL, AQL,
self::COLLECTION, self::COLLECTION,
$_SERVER['REMOTE_ADDR'], $_SERVER['REMOTE_ADDR'],
@ -84,20 +84,21 @@ final class session extends core
// Запись сессии в базу данных // Запись сессии в базу данных
$_id = document::write(static::$db->session, self::COLLECTION, [ $_id = document::write(static::$db->session, self::COLLECTION, [
'ip' => $_SERVER['REMOTE_ADDR'], 'status' => 'active',
'expires' => $expires ?? time() + 604800 'expires' => $expires ?? time() + 604800,
'ip' => $_SERVER['REMOTE_ADDR']
]); ]);
if ($session = collection::search(static::$db->session, sprintf( if ($session = collection::search(static::$db->session, sprintf(
<<<AQL <<<AQL
FOR d IN %s FOR d IN %s
FILTER d._id == '$_id' && d.expires > %d FILTER d._id == '$_id' && d.expires > %d && d.status == 'active'
RETURN d RETURN d
AQL, AQL,
self::COLLECTION, self::COLLECTION,
time() time()
))) { ))) {
// Найдена созданная сессия // Найдена только что созданная сессия
// Запись хеша // Запись хеша
$session->hash = sodium_bin2hex(sodium_crypto_generichash($_id)); $session->hash = sodium_bin2hex(sodium_crypto_generichash($_id));
@ -112,7 +113,7 @@ final class session extends core
} }
} else throw new exception('Не удалось инициализировать коллекцию'); } else throw new exception('Не удалось инициализировать коллекцию');
} catch (exception $e) { } catch (exception $e) {
// Запись в журнал ошибок // Запись в реестр ошибок
$errors[] = [ $errors[] = [
'text' => $e->getMessage(), 'text' => $e->getMessage(),
'file' => $e->getFile(), 'file' => $e->getFile(),
@ -128,14 +129,16 @@ final class session extends core
} }
/** /**
* Связь сессии с аккаунтом * Инициализировать связб сессии с аккаунтом
* *
* @param _document $account Инстанция аккаунта * Ищет связь сессии с аккаунтом, если не находит, то создаёт её
* @param array &$errors Журнал ошибок
* *
* @return bool Статус выполнения * @param account $account Инстанция аккаунта
* @param array &$errors Реестр ошибок
*
* @return bool Связан аккаунт?
*/ */
public function connect(_document $account, array &$errors = []): bool public function connect(account $account, array &$errors = []): bool
{ {
try { try {
if ( if (
@ -145,17 +148,30 @@ final class session extends core
) { ) {
// Инициализирована коллекция // Инициализирована коллекция
if (document::write(static::$db->session, self::COLLECTION . '_edge_' . account::COLLECTION, [ if (
'_from' => $this->document->getId(), collection::search(static::$db->session, sprintf(
'_to' => $account->getId() <<<AQL
])) { FOR document IN %s
// Создано ребро: session -> account FILTER document._from == '%s' && document._to == '%s'
LIMIT 1
RETURN document
AQL,
self::COLLECTION . '_edge_' . account::COLLECTION,
$this->document->getId(),
$account->getId()
)) instanceof _document
|| document::write(static::$db->session, self::COLLECTION . '_edge_' . account::COLLECTION, [
'_from' => $this->document->getId(),
'_to' => $account->getId()
])
) {
// Найдено, либо создано ребро: session -> account
return true; return true;
} else throw new exception('Не удалось создать ребро: session -> account'); } else throw new exception('Не удалось создать ребро: session -> account');
} else throw new exception('Не удалось инициализировать коллекцию'); } else throw new exception('Не удалось инициализировать коллекцию');
} catch (exception $e) { } catch (exception $e) {
// Запись в журнал ошибок // Запись в реестр ошибок
$errors[] = [ $errors[] = [
'text' => $e->getMessage(), 'text' => $e->getMessage(),
'file' => $e->getFile(), 'file' => $e->getFile(),
@ -168,13 +184,13 @@ final class session extends core
} }
/** /**
* Поиск связанного аккаунта * Найти связанный аккаунт
* *
* @param array &$errors Журнал ошибок * @param array &$errors Реестр ошибок
* *
* @return ?_document Инстанция аккаунта, если удалось найти * @return ?account Инстанция аккаунта, если удалось найти
*/ */
public function account(array &$errors = []): ?_document public function account(array &$errors = []): ?account
{ {
try { try {
if ( if (
@ -184,31 +200,34 @@ final class session extends core
) { ) {
// Инициализированы коллекции // Инициализированы коллекции
if ($account = collection::search(static::$db->session, sprintf( // Инициализация инстанции аккаунта
$account = new account;
// Поиск инстанции аккаунта в базе данных
$account->document = collection::search(static::$db->session, sprintf(
<<<AQL <<<AQL
FOR document IN %s FOR document IN %s
LET edge = ( LET edge = (
FOR edge IN %s FOR edge IN %s
FILTER edge._from == '%s' FILTER edge._from == '%s'
SORT edge._key DESC SORT edge._key DESC
LIMIT 1
RETURN edge
)
FILTER document._id == edge[0]._to
LIMIT 1 LIMIT 1
RETURN edge RETURN document
)
FILTER document._id == edge[0]._to
LIMIT 1
RETURN document
AQL, AQL,
account::COLLECTION, account::COLLECTION,
self::COLLECTION . '_edge_' . account::COLLECTION, self::COLLECTION . '_edge_' . account::COLLECTION,
$this->document->getId() $this->getId()
))) { ));
// Найден аккаунт
return $account; if ($account->document instanceof _document) return $account;
} else throw new exception('Не удалось найти аккаунт'); else throw new exception('Не удалось найти инстанцию аккаунта в базе данных');
} else throw new exception('Не удалось инициализировать коллекцию'); } else throw new exception('Не удалось инициализировать коллекцию');
} catch (exception $e) { } catch (exception $e) {
// Запись в журнал ошибок // Запись в реестр ошибок
$errors[] = [ $errors[] = [
'text' => $e->getMessage(), 'text' => $e->getMessage(),
'file' => $e->getFile(), 'file' => $e->getFile(),
@ -220,6 +239,47 @@ final class session extends core
return null; return null;
} }
/**
* Записать в буфер сессии
*
* @param array $data Данные для записи
* @param array &$errors Реестр ошибок
*
* @return bool Записаны данные в буфер сессии?
*/
public function write(array $data, array &$errors = []): bool
{
try {
if (collection::init(static::$db->session, self::COLLECTION)) {
// Инициализирована коллекция
// Проверка инициализированности инстанции документа из базы данных
if (!isset($this->document)) throw new exception('Не инициализирована инстанция документа из базы данных');
// Запись параметров в инстанцию документа из базы данных
$this->document->buffer = array_replace_recursive($this->document->buffer ?? [], $data);
if (document::update(static::$db->session, $this->document)) {
// Записано обновление
return true;
}
throw new exception('Не удалось записать данные в буфер сессии');
} else throw new exception('Не удалось инициализировать коллекцию');
} catch (exception $e) {
// Запись в реестр ошибок
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
return false;
}
/** /**
* Записать * Записать
* *
@ -287,6 +347,6 @@ final class session extends core
*/ */
public function __call(string $name, array $arguments = []) public function __call(string $name, array $arguments = [])
{ {
if (method_exists($this, $name)) return $this->document->{$name}($arguments); if (method_exists($this->document, $name)) return $this->document->{$name}($arguments);
} }
} }

View File

@ -1,555 +0,0 @@
<?php
declare(strict_types=1);
namespace mirzaev\site\account\models;
// Файлы проекта
use mirzaev\site\account\models\account;
// Фреймворк ArangoDB
use mirzaev\arangodb\collection,
mirzaev\arangodb\document;
// Фреймворк ВКонтакте
use mirzaev\vk\robots\user as robot;
// Библиотека для ArangoDB
use ArangoDBClient\Document as _document;
// Библиотека браузера
use GuzzleHttp\Client as browser;
// Встроенные библиотеки
use exception;
use stdClass;
/**
* Модель аккаунта ВКонтакте
*
* @package mirzaev\site\account\models
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class vk extends core
{
/**
* Коллекция
*/
public const COLLECTION = 'vk';
/**
* Инициализация
*
* @param string $response Ответ сервера ВКонтакте с данными аккаунта
* @param array &$errors Журнал ошибок
*
* @return ?_document Инстанция аккаунта ВКонтакте, если удалось создать
*/
public static function initialization(string $response = '', array &$errors = []): ?_document
{
try {
if (collection::init(static::$db->session, self::COLLECTION)) {
// Инициализирована коллекция
// Инициализация данных аккаунта ВКонтакте
$data = json_decode($response);
if ($account = collection::search(static::$db->session, sprintf(
<<<AQL
FOR d IN %s
FILTER d.id == $data->user_id
RETURN d
AQL,
self::COLLECTION
))) {
// Найден аккаунт ВКонтакте
return $account;
} else {
// Не найден аккаунт ВКонтакте
return self::create($response, $errors);
}
} else throw new exception('Не удалось инициализировать коллекцию');
} catch (exception $e) {
// Запись в журнал ошибок
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
return null;
}
/**
* Создание
*
* @param string $response Ответ сервера ВКонтакте с данными аккаунта
* @param array &$errors Журнал ошибок
*
* @return ?_document Инстанция аккаунта ВКонтакте, если удалось создать
*/
public static function create(string $response = '', array &$errors = []): ?_document
{
try {
if (collection::init(static::$db->session, self::COLLECTION)) {
// Инициализирована коллекция
// Запись аккаунта в базу данных
$_id = document::write(static::$db->session, self::COLLECTION);
if ($account = collection::search(static::$db->session, sprintf(
<<<AQL
FOR d IN %s
FILTER d._id == '$_id'
RETURN d
AQL,
self::COLLECTION
))) {
// Найден созданный аккаунт ВКонтакте
if (document::update(static::$db->session, $account)) {
// Записано обновление
// Запись данных об аккаунте ВКонтакте и возврат (bool)
return self::update($account, json_decode($response), $errors);
}
}
throw new exception('Не удалось создать аккаунт ВКонтакте');
} else throw new exception('Не удалось инициализировать коллекцию');
} catch (exception $e) {
// Запись в журнал ошибок
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
return null;
}
/**
* Запросить ключ
*
* @param string $code Код полученный от ВКонтакте
* @param array &$errors Журнал ошибок
*
* @return ?string Тело ответа, если получен код ответа 200
*/
public static function key(string $code = '', array &$errors = []): ?string
{
try {
// Инициализация браузера
$browser = new browser();
// Запрос
$response = $browser->request('GET', "https://oauth.vk.com/access_token?client_id=51447080&client_secret=KYlk0nGELW0A9ds7NQi6&redirect_uri=https://mirzaev.sexy/account/vk/connect&code=$code");
if ($response->getStatusCode() === 200) {
// Ответ сервера: 200
return (string) $response->getBody();
} else throw new exception('Не удалось получить ключ ВКонтакте (' . $response->getStatusCode() . ')');
} catch (exception $e) {
// Запись в журнал ошибок
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
return null;
}
/**
* Поиск связанного аккаунта
*
* @param _document $vk Инстанция аккаунта ВКонтакте
* @param array &$errors Журнал ошибок
*
* @return ?_document Инстанция аккаунта, если удалось найти
*/
public static function account(_document $vk, array &$errors = []): ?_document
{
try {
if (
collection::init(static::$db->session, self::COLLECTION)
&& collection::init(static::$db->session, account::COLLECTION)
&& collection::init(static::$db->session, account::COLLECTION . '_edge_' . self::COLLECTION, true)
) {
// Инициализированы коллекции
if ($account = collection::search(static::$db->session, sprintf(
<<<AQL
FOR document IN %s
LET edge = (
FOR edge IN %s
FILTER edge._to == '%s'
SORT edge._key DESC
LIMIT 1
RETURN edge
)
FILTER document._id == edge[0]._from
LIMIT 1
RETURN document
AQL,
account::COLLECTION,
account::COLLECTION . '_edge_' . self::COLLECTION,
$vk->getId()
))) {
// Найден аккаунт
return $account;
} else throw new exception('Не удалось найти аккаунт');
} else throw new exception('Не удалось инициализировать коллекцию');
} catch (exception $e) {
// Запись в журнал ошибок
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
return null;
}
/**
* Запрос данных аккаунта ВКонтакте с серверов ВКонтакте
*
* @param robot $vk Инстанция аккаунта ВКонтакте
* @param array &$errors Журнал ошибок
*
* @return ?stdClass Данные аккаунта ВКонтакте, если получены
*/
public static function parse(robot $vk, array &$errors = []): ?stdClass
{
try {
// Запрос к API-серверу ВКонтакте
$response = $vk->user->get(fields: [
'activities',
'about',
// 'blacklisted',
// 'blacklisted_by_me',
'books',
'bdate',
'can_be_invited_group',
'can_post',
'can_see_all_posts',
'can_see_audio',
'can_send_friend_request',
'can_write_private_message',
'career',
'common_count',
'connections',
'contacts',
'city',
'country',
'crop_photo',
'domain',
'education',
'exports',
'followers_count',
'friend_status',
'has_photo',
'has_mobile',
'home_town',
'photo_50',
'photo_100',
'photo_200',
'photo_200_orig',
'photo_400_orig',
'photo_max',
'photo_max_orig',
'sex',
'site',
'schools',
'screen_name',
'status',
'verified',
'games',
'interests',
'is_favorite',
'is_friend',
'is_hidden_from_feed',
'last_seen',
'maiden_name',
'military',
'movies',
'music',
'nickname',
'occupation',
'online',
'personal',
'photo_id',
'quotes',
'relation',
'relatives',
'timezone',
'tv',
'universities'
])[0];
if (!empty($response)) {
// Получен ответ
return $response;
}
} catch (exception $e) {
// Запись в журнал ошибок
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
return null;
}
/**
* Обновление данных аккаунта ВКонтакте
*
* Все файлы (аватар, например) будут скачаны на сервер
*
* @param _document $vk Инстанция аккаунта ВКонтакте
* @param stdClass $data Информация об аккаунте (self::parse() или json_decode())
* @param array &$errors Журнал ошибок
*
* @return ?_document Инстанция аккаунта ВКонтакте, если удалось обновить
*/
public static function update(_document $vk, stdClass $data, array &$errors = []): ?_document
{
try {
if (collection::init(static::$db->session, self::COLLECTION)) {
// Инициализирована коллекция
if (empty($vk->id) and isset($data->user_id) || isset($data->id)) {
// Получен идентификатор
// Запись
$vk->id = $data->user_id ?? $data->id;
// Удаление из списка необработанных
unset($data->user_id, $data->id);
} else if (empty($vk->id)) throw new exception('Не удалось найти идентификатор аккаунта ВКонтакте');
if (isset($data->access_token, $data->expires_in)) {
// Получен ключ
// Запись
$vk->access = [
'key' => $data->access_token,
'expires' => $data->expires_in
];
// Удаление из списка необработанных
unset($data->access_token, $data->expires_in);
}
// Инициализация браузера
$browser = new browser();
// Инициализация директории с обложкой
if (!file_exists($path = INDEX . DIRECTORY_SEPARATOR . 'storage' . DIRECTORY_SEPARATOR . $vk->id . DIRECTORY_SEPARATOR . 'cover' . DIRECTORY_SEPARATOR))
mkdir($path, 0775, true);
if (isset($data->photo_50)) {
// Получено изображение 50x50
if ($browser->get($data->photo_50, ['sink' => $file = "$path/50x50.jpg"])->getStatusCode() === 200)
$vk->cover =
($vk->cover ?? []) +
[
'50x50' => ($vk->cover['50x50'] ?? []) +
[
'source' => $data->photo_50,
'public' => "/storage/$vk->id/cover/50x50.jpg",
'local' => $file,
]
];
else throw new exception('Не удалось получить изображение 50x50 с серверов ВКонтакте');
// Удаление из списка необработанных
unset($data->photo_50);
}
// Инициализация директории с обложкой
if (!file_exists($path = INDEX . DIRECTORY_SEPARATOR . 'storage' . DIRECTORY_SEPARATOR . $vk->id . DIRECTORY_SEPARATOR . 'cover' . DIRECTORY_SEPARATOR))
mkdir($path, 0775, true);
if (isset($data->photo_100)) {
// Получено изображение 100x100
if ($browser->get($data->photo_100, ['sink' => $file = "$path/100x100.jpg"])->getStatusCode() === 200)
$vk->cover =
($vk->cover ?? []) +
[
'100x100' => ($vk->cover['100x100'] ?? []) +
[
'source' => $data->photo_100,
'public' => "/storage/$vk->id/cover/100x100.jpg",
'local' => $file,
]
];
else throw new exception('Не удалось получить изображение 100x100 с серверов ВКонтакте');
// Удаление из списка необработанных
unset($data->photo_100);
}
// Инициализация директории с обложкой
if (!file_exists($path = INDEX . DIRECTORY_SEPARATOR . 'storage' . DIRECTORY_SEPARATOR . $vk->id . DIRECTORY_SEPARATOR . 'cover' . DIRECTORY_SEPARATOR))
mkdir($path, 0775, true);
if (isset($data->photo_200)) {
// Получено изображение 200x200
if ($browser->get($data->photo_200, ['sink' => $file = "$path/200x200.jpg"])->getStatusCode() === 200)
$vk->cover =
($vk->cover ?? []) +
[
'200x200' => ($vk->cover['200x200'] ?? []) +
[
'source' => $data->photo_200,
'public' => "/storage/$vk->id/cover/200x200.jpg",
'local' => $file,
]
];
else throw new exception('Не удалось получить изображение 200x200 с серверов ВКонтакте');
// Удаление из списка необработанных
unset($data->photo_200);
}
if (isset($data->photo_200_orig)) {
// Получено изображение 200x
if ($browser->get($data->photo_200_orig, ['sink' => $file = "$path/200x.jpg"])->getStatusCode() === 200)
$vk->cover =
($vk->cover ?? []) +
[
'200x' => ($vk->cover['200x'] ?? []) +
[
'source' => $data->photo_200_orig,
'public' => "/storage/$vk->id/cover/200x.jpg",
'local' => $file,
]
];
else throw new exception('Не удалось получить изображение 200x с серверов ВКонтакте');
// Удаление из списка необработанных
unset($data->photo_200_orig);
}
if (isset($data->photo_400_orig)) {
// Получено изображение 400x
if ($browser->get($data->photo_400_orig, ['sink' => $file = "$path/400x.jpg"])->getStatusCode() === 200)
$vk->cover =
($vk->cover ?? []) +
[
'400x' => ($vk->cover['400x'] ?? []) +
[
'source' => $data->photo_400_orig,
'public' => "/storage/$vk->id/cover/400x.jpg",
'local' => $file,
]
];
else throw new exception('Не удалось получить изображение 400x с серверов ВКонтакте');
// Удаление из списка необработанных
unset($data->photo_400_orig);
}
if (isset($data->photo_max)) {
// Получено изображение MAXxMAX
if ($browser->get($data->photo_max, ['sink' => $file = "$path/MAXxMAX.jpg"])->getStatusCode() === 200)
$vk->cover =
($vk->cover ?? []) +
[
'MAXxMAX' => ($vk->cover['MAXxMAX'] ?? []) +
[
'source' => $data->photo_max,
'public' => "/storage/$vk->id/cover/MAXxMAX.jpg",
'local' => $file,
]
];
else throw new exception('Не удалось получить изображение MAXxMAX с серверов ВКонтакте');
// Удаление из списка необработанных
unset($data->photo_max);
}
if (isset($data->photo_max_orig)) {
// Получено изображение MAXx
if ($browser->get($data->photo_max_orig, ['sink' => $file = "$path/MAXx.jpg"])->getStatusCode() === 200)
$vk->cover =
($vk->cover ?? []) +
[
'MAXx' => ($vk->cover['MAXx'] ?? []) +
[
'source' => $data->photo_max_orig,
'public' => "/storage/$vk->id/cover/MAXx.jpg",
'local' => $file,
]
];
else throw new exception('Не удалось получить изображение MAXx с серверов ВКонтакте');
// Удаление из списка необработанных
unset($data->photo_max_orig);
}
if (isset($data->crop_photo)) {
// Получено изображение MAXx
if ($browser->get($data->photo_max_orig, ['sink' => $file = "$path/MAXx.jpg"])->getStatusCode() === 200)
$vk->cover =
($vk->cover ?? []) +
[
'MAXx' => ($vk->cover['MAXx'] ?? []) +
[
'source' => $data->photo_max_orig,
'public' => "/storage/$vk->id/cover/MAXx.jpg",
'local' => $file,
]
];
else throw new exception('Не удалось получить изображение MAXx с серверов ВКонтакте');
// Удаление из списка необработанных
unset($data->photo_max_orig);
}
// Перебор оставшихся параметров
foreach ($data as $key => $value) $vk->{$key} = $value;
if (document::update(static::$db->session, $vk)) {
// Записано обновление
return $vk;
} else throw new exception('Не удалось записать данные аккаунта ВКонтакте');
} else throw new exception('Не удалось инициализировать коллекцию');
} catch (exception $e) {
// Запись в журнал ошибок
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
return null;
}
}

View File

@ -19,7 +19,14 @@ main {
align-items: unset; align-items: unset;
} }
div.column {
display: flex;
flex-direction: column;
gap: 20px;
}
section.panel { section.panel {
--display : flex;
z-index : 1000; z-index : 1000;
width : 400px; width : 400px;
position : absolute; position : absolute;
@ -27,6 +34,10 @@ section.panel {
flex-direction: column; flex-direction: column;
} }
div.column>section.panel {
position : unset;
}
section.panel.medium { section.panel.medium {
width: 300px; width: 300px;
} }
@ -43,7 +54,6 @@ section.panel#classic {
margin-left: 570px; margin-left: 570px;
} }
section.panel>section.body>ul { section.panel>section.body>ul {
margin: 0 5%; margin: 0 5%;
padding: 0; padding: 0;
@ -61,6 +71,34 @@ section.panel>section.body>ul>li {
animation-fill-mode : forwards; animation-fill-mode : forwards;
animation-timing-function: cubic-bezier(.47,0,.74,.71); animation-timing-function: cubic-bezier(.47,0,.74,.71);
} }
section.panel>section.body>dl {
margin: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
section.panel>section.body>dl>* {
word-break: break-word;
animation-duration : .35s;
animation-name : uprise;
animation-fill-mode : forwards;
animation-timing-function: cubic-bezier(.47,0,.74,.71);
}
section.panel>section.body>dl>dt {
margin-left: 20px;
display: none;
font-size: 0.9rem;
font-weight: bold;
}
section.panel>section.body>dl>dd {
margin-left: unset;
font-size: 0.8rem;
}
section.panel>section.header { section.panel>section.header {
z-index : 1000; z-index : 1000;
height : 50px; height : 50px;

View File

@ -21,3 +21,44 @@
filter: blur(0px); filter: blur(0px);
} }
} }
@keyframes window-vertical-open {
0% {
height: 0;
opacity: 0;
}
100% {
height: var(--height, inherit);
opacity: var(--opacity, 1);
}
}
@keyframes window-vertical-close {
0% {
height: var(--height, inherit);
opacity: var(--opacity, 1);
}
100% {
height: 0;
opacity: 0;
}
}
.animation.window:not(.hidden, .horizontal) {
overflow: hidden;
animation-duration : .1s;
animation-name : window-vertical-open;
animation-fill-mode : forwards;
animation-timing-function: ease-in;
}
.animation.window.hidden:not(.horizontal) {
overflow: hidden;
animation-duration : .05s;
animation-name : window-vertical-close;
animation-fill-mode : forwards;
animation-timing-function: ease-out;
}

View File

@ -1,5 +1,3 @@
@import url('/fonts/commissioner.ttf');
@media (prefers-color-scheme: light) { @media (prefers-color-scheme: light) {
:root { :root {
--background-above-1 : #fff; --background-above-1 : #fff;
@ -81,9 +79,8 @@
user-select : none; user-select : none;
} }
.hidden { .hidden:not(.animation) {
display: none !important; display: none !important;
opacity: 0;
} }
* { * {

View File

@ -39,11 +39,7 @@ $router->write('/session/password', 'session', 'password', 'POST');
$router->write('/session/invite', 'session', 'invite', 'POST'); $router->write('/session/invite', 'session', 'invite', 'POST');
// Инициализация ядра // Инициализация ядра
$core = new core(namespace: __NAMESPACE__, router: $router); $core = new core(namespace: __NAMESPACE__, router: $router, controller: new controller(false), model: new model(false));
// Инициализация ядер
$core->controller = new controller;
$core->model = new model;
// Обработка запроса // Обработка запроса
echo $core->start(); echo $core->start();

View File

@ -9,6 +9,7 @@ class password {
* @return {object} {(string) password, (array) errors} * @return {object} {(string) password, (array) errors}
*/ */
static async generate(length = 12, type = "classic") { static async generate(length = 12, type = "classic") {
// Запрос к серверу
return await fetch("https://account.mirzaev.sexy/api/generate/password", { return await fetch("https://account.mirzaev.sexy/api/generate/password", {
method: "POST", method: "POST",
headers: { headers: {

View File

@ -6,12 +6,12 @@ class session {
* *
* Записывает входной псевдоним в сессию, а так же проверяет существование аккаунта с ним * Записывает входной псевдоним в сессию, а так же проверяет существование аккаунта с ним
* *
* @param {string} login Входной * @param {string} login Входной псевдоним
* *
* @return {object} {(bool) exist, (array) errors} * @return {object} {(bool) exist, (array) errors}
*/ */
static async login(login) { static async login(login) {
// Запрос // Запрос к серверу
return await fetch('https://account.mirzaev.sexy/session/login', { return await fetch('https://account.mirzaev.sexy/session/login', {
method: 'POST', method: 'POST',
headers: { headers: {
@ -32,16 +32,16 @@ class session {
* *
* @param {string} password Пароль * @param {string} password Пароль
* *
* @return {object} {(bool) verify, (array) errors} * @return {object} {(bool) verify, (bool) account, (array) errors}
*/ */
static async password(password) { static async password(password) {
// Запрос // Запрос к серверу
return await fetch('https://account.mirzaev.sexy/session/password', { return await fetch('https://account.mirzaev.sexy/session/password', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
}, },
body: `password=${password}&return=verify,errors` body: `password=${password}&remember=1&return=verify,account,errors`
}) })
.then((response) => response.json()) .then((response) => response.json())
.then((data) => { .then((data) => {
@ -59,7 +59,7 @@ class session {
* @return {object} {(bool) exist, (array) from, (array) errors} * @return {object} {(bool) exist, (array) from, (array) errors}
*/ */
static async invite(invite) { static async invite(invite) {
// Запрос // Запрос к серверу
return await fetch("https://account.mirzaev.sexy/session/invite", { return await fetch("https://account.mirzaev.sexy/session/invite", {
method: "POST", method: "POST",
headers: { headers: {
@ -69,6 +69,16 @@ class session {
}) })
.then((response) => response.json()) .then((response) => response.json())
.then((data) => { .then((data) => {
if (data.exist === false) {
// Не найдено приглашение
// Инициализация категории ошибок
if (typeof data.errors.session === 'undefined') data.errors.session = [];
// Запись ошибки
data.errors.session.push('Не найдено приглашение');
}
return data; return data;
}); });
} }

View File

@ -1,247 +0,0 @@
{% block css %}
<link type="text/css" rel="stylesheet" href="/css/account.css">
<link type="text/css" rel="stylesheet" href="/css/icons/arrow_right.css">
<link type="text/css" rel="stylesheet" href="/css/icons/nametag.css">
<link type="text/css" rel="stylesheet" href="/css/icons/keyhole.css">
<link type="text/css" rel="stylesheet" href="/css/icons/user_add.css">
{% endblock %}
{% block body %}
<section id="entry" class="panel medium">
<section class="header unselectable">
<h1>Идентификация</h1>
</section>
<section class="body">
<label id="login">
<i class="nametag"></i>
<input name="login" type="text" placeholder="Входной псевдоним"
value="{{ account.login ?? session.buffer.login ?? cookie.buffer_login }}"
onkeypress="if (event.keyCode === 13) _login()" autofocus>
<button class="accept" onclick="_login()"><i class="arrow right"></i></button>
</label>
<label id="password" class="hidden">
<i class="keyhole"></i>
<input name="password" type="password" placeholder="Пароль" autocomplete="current-password"
onkeypress="if (event.keyCode === 13) _password()">
<button class="accept" onclick="_password()"><i class="arrow right"></i></button>
</label>
<label id="invite" class="hidden">
<i class="user add"></i>
<input name="invite" type="text" placeholder="Ключ приглашения" onkeypress="if (event.keyCode === 13) _invite()">
<button class="accept" onclick="_invite()"><i class="arrow right"></i></button>
</label>
</section>
</section>
<section id="mnemonic" class="panel small hidden">
<section class="header unselectable">
<h2>Мнемонические</h2>
</section>
<section class="body">
<ul></ul>
</section>
</section>
<section id="classic" class="panel small hidden">
<section class="header unselectable">
<h2>Классические</h2>
</section>
<section class="body">
<ul></ul>
</section>
</section>
<script>
// Инициализация реестра ошибок
let errors = new Map;
// Инициализация функций в глобальной области видимости
let _login, _password, _invite
document.addEventListener('damper.initialized', function (e) {
// Инициализирован демпфер
// Инициализация узлов
const entry = document.getElementById('entry');
const mnemonic = document.getElementById('mnemonic');
const classic = document.getElementById('classic');
// Инициализация элемента с заголовком
const title = entry.getElementsByTagName('h1')[0];
// Инициализация элементов-оболочек полей ввода
const labels = {
login: document.getElementById('login'),
invite: document.getElementById('invite'),
password: document.getElementById('password')
};
/**
* Отправить входной псевдоним на сервер
*
* @return {void}
*/
_login = () => {
// Инициализация поля ввода
const input = labels.login.querySelector('input[name=login]');
// Блокировка поля ввода
input.disabled = true;
return e.detail.damper(async () => {
// Запрос к серверу
const response = await session.login(input.value, errors);
if (response.exist) {
// Найден аккаунт
// Инициализация интерфейса аутентификации
title.innerText = 'Аутентификация';
labels.login.classList.add('hidden');
labels.password.classList.remove('hidden');
labels.password.querySelector('input[name=password]').focus();
} else {
// Не найден аккаунт
// Инициализация интерфейса регистрации
title.innerText = 'Регистрация';
labels.login.classList.add('hidden');
labels.invite.classList.remove('hidden');
labels.invite.querySelector('input[name=invite]').focus();
}
}, 1000)();
};
/**
* Отправить пароль на сервер
*
* @return {void}
*/
_password = () => {
// Инициализация поля ввода
const input = labels.password.querySelector('input[name=password]');
// Блокировка поля ввода
input.disabled = true;
return e.detail.damper(async () => {
// Деинициализация индикатора и анимации об ошибке
input.classList.remove('error');
// Запрос к серверу
const response = await session.password(input.value, errors);
if (response.verify) {
// Пройдена проверка пароля на соответствие требованиям
} else {
// Не пройдена проверка пароля на соответствие требованиям
// Разблокировка поля для ввода
input.disabled = false;
// Инициализация отображения ошибки
input.classList.add('error');
// Фокусировка на поле ввода
input.focus();
}
}, 1000)();
}
/**
* Отправить код приглашения на сервер
*
* @return {void}
*/
_invite = () => {
// Инициализация поля ввода
const input = labels.invite.querySelector('input[name=invite]');
// Блокировка поля ввода
input.disabled = true;
return e.detail.damper(async () => {
// Деинициализация индикатора и анимации об ошибке
input.classList.remove('error');
// Запрос к серверу
const response = await session.invite(input.value, errors);
if (response.exist) {
// Найдено приглашение
// Инициализация интерфейса ввода пароля
labels.invite.classList.add('hidden');
labels.password.querySelector('input[name=password]').autocomplete = 'new-password';
labels.password.classList.remove('hidden');
mnemonic.classList.remove('hidden');
classic.classList.remove('hidden');
for (let i = 0; i < 6; i++) {
// Генерация HTML-элементов с примерами мнемонических паролей
// Инициализация HTML-элемента
const element = document.createElement('li');
// Генерация пароля
password.generate(Math.floor(Math.random() * 3) + 2, 'mnemonic')
.then((responce) => {
if (responce.password === '') {
// Не удалось сгенерировать пароль
// Перезапуск итерации
--i;
return;
}
// Запись пароля
element.innerText = responce.password;
// Запись HTML-элемента с паролем в список
mnemonic.getElementsByTagName('ul')[0].appendChild(element);
});
}
for (let i = 0; i < 8; i++) {
// Генерация HTML-элементов с примерами классических паролей
// Инициализация HTML-элемента
const element = document.createElement('li');
// Генерация пароля
password.generate(Math.floor(Math.random() * 14) + 4)
.then((responce) => {
if (responce.password === '') {
// Не удалось сгенерировать пароль
// Перезапуск итерации
--i;
return;
}
// Запись пароля
element.innerText = responce.password;
// Запись HTML-элемента с паролем в список
classic.getElementsByTagName('ul')[0].appendChild(element);
});
}
} else {
// Не найдено приглашение
// Разблокировка поля для ввода
input.disabled = false;
// Инициализация отображения ошибки
input.classList.add('error');
// Фокусировка на поле ввода
input.focus();
}
}, 1000)();
};
});
</script>
{% endblock %}
{% block js %}
<script type="text/javascript" src="/js/session.js"></script>
<script type="text/javascript" src="/js/password.js"></script>
{% endblock %}

View File

@ -0,0 +1,410 @@
{% block css %}
<link type="text/css" rel="stylesheet" href="/css/account.css">
<link type="text/css" rel="stylesheet" href="/css/icons/arrow_right.css">
<link type="text/css" rel="stylesheet" href="/css/icons/nametag.css">
<link type="text/css" rel="stylesheet" href="/css/icons/keyhole.css">
<link type="text/css" rel="stylesheet" href="/css/icons/user_add.css">
{% endblock %}
{% block body %}
<div class="column">
<section id="entry" class="panel medium">
<section class="header unselectable">
<h1>Идентификация</h1>
</section>
<section class="body">
<label id="login">
<i class="nametag"></i>
<input name="login" type="text" placeholder="Входной псевдоним"
value="{{ account.login ?? session.buffer.login ?? cookie.buffer_login }}"
onkeypress="if (event.keyCode === 13) __login()" autofocus>
<button class="accept" onclick="__login()"><i class="arrow right"></i></button>
</label>
<label id="password" class="hidden">
<i class="keyhole"></i>
<input name="password" type="password" placeholder="Пароль" autocomplete="current-password"
onkeypress="if (event.keyCode === 13) __password()">
<button class="accept" onclick="__password()"><i class="arrow right"></i></button>
</label>
<label id="invite" class="hidden">
<i class="user add"></i>
<input name="invite" type="text" placeholder="Ключ приглашения"
onkeypress="if (event.keyCode === 13) __invite()">
<button class="accept" onclick="__invite()"><i class="arrow right"></i></button>
</label>
</section>
</section>
<section id="errors" class="panel medium animation window hidden" style="--height: 300px">
<section class="body">
<dl></dl>
</section>
</section>
</div>
<section id="mnemonic" class="panel small hidden">
<section class="header unselectable">
<h2>Мнемонические</h2>
</section>
<section class="body">
<ul></ul>
</section>
</section>
<section id="classic" class="panel small hidden">
<section class="header unselectable">
<h2>Классические</h2>
</section>
<section class="body">
<ul></ul>
</section>
</section>
<script>
// Инициализация функций в глобальной области видимости
let _login, __login, _password, __password, _invite, __invite, _errors;
document.addEventListener('damper.initialized', function (e) {
// Инициализирован демпфер
// Инициализация HTML-элемента с блоком входа в аккаунт
const entry = {wrap: document.getElementById('entry')};
entry.title = entry.wrap.getElementsByTagName('h1')[0];
// Инициализация HTML-элемента с блоком мнемонических паролей
const mnemonic = {wrap: document.getElementById('mnemonic')};
mnemonic.list = mnemonic.wrap.getElementsByTagName('ul')[0];
// Инициализация HTML-элемента с блоком классических паролей
const classic = {wrap: document.getElementById('classic')};
classic.list = classic.wrap.getElementsByTagName('ul')[0];
// Инициализация HTML-элемента с блоком ошибок
const errors = {wrap: document.getElementById('errors')};
errors.list = errors.wrap.getElementsByTagName('dl')[0];
// Инициализация HTML-элементов-оболочек полей ввода
const labels = {
login: {label: document.getElementById('login')},
password: {label: document.getElementById('password')},
invite: {label: document.getElementById('invite')}
};
labels.login.input = labels.login.label.querySelector('input[name=login]');
labels.password.input = labels.password.label.querySelector('input[name=password]');
labels.invite.input = labels.invite.label.querySelector('input[name=invite]');
/**
* Отправить входной псевдоним на сервер
*
* @return {void}
*/
_login = e.detail.damper(async () => {
// Деинициализация индикатора и анимации об ошибке
labels.login.input.classList.remove('error');
// Запрос к серверу
const response = await session.login(labels.login.input.value);
if (_errors(response.errors)) {
// Сгенерированы ошибки
// Разлокировка поля ввода
labels.login.input.disabled = false;;
// Инициализация отображения ошибки
labels.login.input.classList.add('error');
// Фокусировка на поле ввода
labels.login.input.focus();
} else {
// Не сгенерированы ошибки (подразумевается их отсутствие)
if (typeof response.exist === 'boolean')
if (response.exist) {
// Найден аккаунт
// Инициализация интерфейса аутентификации
entry.title.innerText = 'Аутентификация';
labels.login.label.classList.add('hidden');
labels.password.label.classList.remove('hidden');
labels.password.input.focus();
} else {
// Не найден аккаунт
// Инициализация интерфейса регистрации
entry.title.innerText = 'Регистрация';
labels.login.label.classList.add('hidden');
labels.invite.label.classList.remove('hidden');
labels.invite.input.focus();
}
}
}, 500);
/**
* Отправить входной псевдоним на сервер с дополнительной подготовкой
*
* @return {void}
*/
__login = () => {
// Блокировка поля ввода
labels.login.input.disabled = true;
// Реинициализация блока ошибок
_errors();
// Запуск процесса отправки входного псевдонима
_login();
}
/**
* Отправить пароль на сервер
*
* @return {void}
*/
_password = e.detail.damper(async () => {
// Деинициализация индикатора и анимации об ошибке
labels.password.input.classList.remove('error');
// Запрос к серверу
const response = await session.password(labels.password.input.value);
if (_errors(response.errors)) {
// Сгенерированы ошибки
// Разлокировка поля ввода
labels.password.input.disabled = false;
// Инициализация отображения ошибки
labels.password.input.classList.add('error');
// Фокусировка на поле ввода
labels.password.input.focus();
} else {
// Не сгенерированы ошибки (подразумевается их отсутствие)
if (typeof response.verify === 'boolean')
if (response.verify) {
// Пройдена проверка пароля на соответствие требованиям
if (response.account) {
// Инициализирован аккаунт
}
}
}
}, 500);
/**
* Отправить пароль на сервер с дополнительной подготовкой
*
* @return {void}
*/
__password = () => {
// Блокировка поля ввода
labels.password.input.disabled = true;
// Реинициализация блока ошибок
_errors();
// Запуск процесса отправки входного псевдонима
_password();
}
/**
* Отправить код приглашения на сервер
*
* @return {void}
*/
_invite = e.detail.damper(async () => {
// Деинициализация индикатора и анимации об ошибке
labels.invite.input.classList.remove('error');
// Запрос к серверу
const response = await session.invite(labels.invite.input.value);
if (_errors(response.errors)) {
// Сгенерированы ошибки
// Разблокировка поля ввода
labels.invite.input.disabled = false;
// Инициализация отображения ошибки
labels.invite.input.classList.add('error');
// Фокусировка на поле ввода
labels.invite.input.focus();
} else {
// Не сгенерированы ошибки (подразумевается их отсутствие)
if (typeof response.exist === 'boolean')
if (response.exist) {
// Найдено приглашение
// Инициализация интерфейса ввода пароля
labels.invite.label.classList.add('hidden');
labels.password.input.autocomplete = 'new-password';
labels.password.label.classList.remove('hidden');
mnemonic.wrap.classList.remove('hidden');
classic.wrap.classList.remove('hidden');
for (let i = 0; i < 6; i++) {
// Генерация HTML-элементов с примерами мнемонических паролей
// Инициализация HTML-элемента
const element = document.createElement('li');
// Генерация пароля
password.generate(Math.floor(Math.random() * 3) + 2, 'mnemonic')
.then((responce) => {
if (responce.password === '') {
// Не удалось сгенерировать пароль
// Перезапуск итерации
--i;
return;
}
// Запись пароля
element.innerText = responce.password;
// Запись HTML-элемента с паролем в список
mnemonic.list.appendChild(element);
// Генерация списка с текстом ошибок
_errors(response.errors);
});
}
for (let i = 0; i < 8; i++) {
// Генерация HTML-элементов с примерами классических паролей
// Инициализация HTML-элемента
const element = document.createElement('li');
// Генерация пароля
password.generate(Math.floor(Math.random() * 14) + 4)
.then((responce) => {
if (responce.password === '') {
// Не удалось сгенерировать пароль
// Перезапуск итерации
--i;
return;
}
// Запись пароля
element.innerText = responce.password;
// Запись HTML-элемента с паролем в список
classic.list.appendChild(element);
// Генерация списка с текстом ошибок
_errors(response.errors);
});
}
}
}
}, 500);
/**
* Отправить код приглашения на сервер с дополнительной подготовкой
*
* @return {void}
*/
__invite = () => {
// Блокировка поля ввода
labels.invite.input.disabled = true;
// Реинициализация блока ошибок
_errors();
// Запуск процесса отправки входного псевдонима
_invite();
}
/**
* Сгенерировать HTML-элемент с блоком ошибок
*
* @param {object} registry Реестр ошибок
* @param {bool} reinitialization Реинициализировать?
*
* @return {bool} Сгенерированы ошибки?
*/
_errors = (registry, reinitialization = true) => {
function height() {
// Скрытие HTML-элемента
errors.wrap.style.zIndex = '-99999';
errors.wrap.style.left = '-99999px';
errors.wrap.style.top = '-99999px';
errors.wrap.style.position = 'absolute';
errors.wrap.style.display = 'var(--display, unset)';
errors.wrap.style.opacity = '0';
errors.wrap.style.animationName = 'unset';
// Реинициализация переменной с данными о высоте HTML-элемента
errors.wrap.style.setProperty('--height', errors.wrap.offsetHeight + 'px');
// Отмена скрытия HTML-элемента
errors.wrap.style.zIndex =
errors.wrap.style.left =
errors.wrap.style.top =
errors.wrap.style.position =
errors.wrap.style.display =
errors.wrap.style.opacity =
errors.wrap.style.animationName = null;
}
// Удаление ошибок из прошлой генерации
if (reinitialization) errors.list.innerHTML = null;
for (error in registry) {
// Генерация HTML-элементов с текстами ошибок
// Инициализация HTML-элемента
const element = document.createElement('dd');
if (typeof registry[error] === 'object') {
// Категория ошибок
// Проверка наличия ошибок
if (registry[error].length === 0) continue;
// Инициализация HTML-элемента
const element = document.createElement('dt');
// Запись текста категории
element.innerText = error;
// Запись HTML-элемента в список
errors.list.appendChild(element);
// Реинициализация высоты
height();
// Обработка вложенных ошибок (вход в рекурсию)
_errors(registry[error], false);
} else {
// Текст ошибки (подразумевается)
// Запись текста ошибки
element.innerText = registry[error];
// Запись HTML-элемента в список
errors.list.appendChild(element);
// Реинициализация высоты
height();
}
}
// Реинициализация HTML-элемента с текстом ошибок
if (reinitialization && errors.list.childElementCount === 0) errors.wrap.classList.add('hidden');
else errors.wrap.classList.remove('hidden');
return errors.list.childElementCount === 0 ? false : true;
};
});
</script>
{% endblock %}
{% block js %}
<script type="text/javascript" src="/js/session.js"></script>
<script type="text/javascript" src="/js/password.js"></script>
{% endblock %}

View File

@ -5,6 +5,10 @@ declare(strict_types=1);
namespace mirzaev\site\account\views; namespace mirzaev\site\account\views;
// Файлы проекта // Файлы проекта
use mirzaev\site\account\models\session,
mirzaev\site\account\models\account;
// Фреймворк PHP
use mirzaev\minimal\controller; use mirzaev\minimal\controller;
// Шаблонизатор представлений // Шаблонизатор представлений
@ -37,13 +41,15 @@ final class templater extends controller implements ArrayAccess
* *
* @return void * @return void
*/ */
public function __construct() public function __construct(?session &$session = null, ?account &$account = null)
{ {
// Инициализация шаблонизатора // Инициализация шаблонизатора
$this->twig = new twig(new FilesystemLoader(VIEWS)); $this->twig = new twig(new FilesystemLoader(VIEWS));
// Инициализация глобальных переменных // Инициализация глобальных переменных
$this->twig->addGlobal('cookie', $_COOKIE); $this->twig->addGlobal('cookie', $_COOKIE);
if (isset($session->document)) $this->twig->addGlobal('session', $session);
if (isset($account->document)) $this->twig->addGlobal('account', $account);
} }
/** /**