generated from mirzaev/pot
DUMB MOVING STARTED (DEVELOPING)
This commit is contained in:
parent
87bd15640a
commit
f70f2c86ea
|
@ -1,3 +1,2 @@
|
||||||
# site-account
|
# accounts
|
||||||
|
Accounts system site of the Svoboda organization
|
||||||
Site for intersite authentication
|
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
{
|
{
|
||||||
"name": "mirzaev/site-account",
|
"name": "svoboda/accounts",
|
||||||
"description": "API for intersite authentication",
|
|
||||||
"readme": "README.md",
|
|
||||||
"keywords": [
|
|
||||||
"site",
|
|
||||||
"api",
|
|
||||||
"authentication"
|
|
||||||
],
|
|
||||||
"type": "site",
|
"type": "site",
|
||||||
"homepage": "https://git.mirzaev.sexy/mirzaev/site-account",
|
"description": "Accounts system site of the Svoboda organization",
|
||||||
|
"keywords": [
|
||||||
|
"accounts",
|
||||||
|
"svoboda",
|
||||||
|
"anarchism",
|
||||||
|
"minimal"
|
||||||
|
],
|
||||||
|
"readme": "README.md",
|
||||||
"license": "WTFPL",
|
"license": "WTFPL",
|
||||||
|
"homepage": "https://git.svoboda.works/svoboda/accounts",
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
"name": "Arsen Mirzaev Tatyano-Muradovich",
|
"name": "Arsen Mirzaev Tatyano-Muradovich",
|
||||||
|
@ -20,23 +21,19 @@
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"email": "arsen@mirzaev.sexy",
|
"email": "arsen@mirzaev.sexy",
|
||||||
"wiki": "https://git.mirzaev.sexy/mirzaev/site-account/wiki",
|
"wiki": "https://git.svoboda.works/svoboda/accounts/wiki",
|
||||||
"issues": "https://git.mirzaev.sexy/mirzaev/site-account/issues"
|
"issues": "https://git.svoboda.works/svoboda/accounts/issues"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "funding",
|
"type": "funding",
|
||||||
"url": "https://fund.mirzaev.sexy"
|
"url": "https://fund.svoboda.works"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
"php": "~8.2",
|
"php": "~8.4",
|
||||||
"ext-sodium": "~8.2",
|
"ext-sodium": "~8.4",
|
||||||
"mirzaev/minimal": "^2.0.x-dev",
|
"mirzaev/minimal": "^3.4.0",
|
||||||
"mirzaev/accounts": "~1.2.x-dev",
|
|
||||||
"mirzaev/arangodb": "^1.0.0",
|
|
||||||
"mirzaev/vk": "^5.0",
|
|
||||||
"triagens/arangodb": "~3.9.x-dev",
|
|
||||||
"twig/twig": "^3.4",
|
"twig/twig": "^3.4",
|
||||||
"guzzlehttp/guzzle": "^7.5",
|
"guzzlehttp/guzzle": "^7.5",
|
||||||
"scripturadesign/markov": "^2.0"
|
"scripturadesign/markov": "^2.0"
|
||||||
|
@ -46,12 +43,15 @@
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"mirzaev\\site\\account\\": "mirzaev/site/account/system"
|
"svoboda\\accounts\\": "svoboda/accounts/system"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"autoload-dev": {
|
"autoload-dev": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"mirzaev\\site\\account\\tests\\": "mirzaev/site/account/tests"
|
"svoboda\\accounts\\tests\\": "svoboda/accounts/tests"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"pre-update-cmd": "./install.sh"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 68589e968cbc043f35c2948a9c90293b6f5f9cb9
|
|
@ -0,0 +1,53 @@
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
|
||||||
|
server_name account.svoboda.works;
|
||||||
|
|
||||||
|
# 301 302
|
||||||
|
return 301 https://$server_name$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
listen 443 quic;
|
||||||
|
listen [::]:443 ssl;
|
||||||
|
listen [::]:443 quic;
|
||||||
|
|
||||||
|
server_name account.svoboda.works;
|
||||||
|
|
||||||
|
http2 on;
|
||||||
|
http3 on;
|
||||||
|
quic_gso on;
|
||||||
|
quic_retry on;
|
||||||
|
|
||||||
|
add_header Alt-Svc 'h3=":$server_port"; ma=86400';
|
||||||
|
add_header x-quic 'h3';
|
||||||
|
|
||||||
|
root /var/www/account.svoboda.works/svoboda/account/system/public;
|
||||||
|
|
||||||
|
index index.php;
|
||||||
|
|
||||||
|
keepalive_timeout 60;
|
||||||
|
|
||||||
|
include snippets/ssl-params.conf;
|
||||||
|
include snippets/ssl-svoboda.conf;
|
||||||
|
include snippets/php8_4.conf;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.php?$query_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~* \.(?:jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|mp3|ogg|ogv|webm|htc|woff2|woff)$ {
|
||||||
|
expires 1M;
|
||||||
|
access_log off;
|
||||||
|
add_header Cache-Control "max-age=2629746, public";
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~* \.(?:css|js|mjs|min)$ {
|
||||||
|
expires 1y;
|
||||||
|
access_log off;
|
||||||
|
add_header Cache-Control "max-age=31556952, public";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 0300f3376550b9d0a07d1c41db88452af67e366b
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 81aca4001629e8f3cab8a849c1e892dbac74c88a
|
|
@ -0,0 +1,37 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Renaming project folder
|
||||||
|
if [ -d author/project ]; then
|
||||||
|
mv author/project author/accounts
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Renaming project author folder
|
||||||
|
if [ -d author ]; then
|
||||||
|
mv author svoboda
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Initializing the javascript modules folder
|
||||||
|
if [ ! -d svoboda/accounts/system/public/js/modules ]; then
|
||||||
|
mkdir -p ./svoboda/accounts/system/public/js/modules
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Updating repositories
|
||||||
|
cd damper.mjs && git pull
|
||||||
|
cd ../hotline.mjs && git pull
|
||||||
|
cd ../graph.mjs && git pull
|
||||||
|
cd ../
|
||||||
|
|
||||||
|
# Installing "damper.min.mjs"
|
||||||
|
if [ ! -h svoboda/accounts/system/public/js/modules/damper.min.mjs ]; then
|
||||||
|
ln -s ../../../../../../damper.mjs/damper.min.mjs svoboda/accounts/system/public/js/modules/damper.min.mjs
|
||||||
|
fi
|
||||||
|
|
||||||
|
# installing "hotline.min.mjs"
|
||||||
|
if [ ! -h svoboda/accounts/system/public/js/modules/hotline.min.mjs ]; then
|
||||||
|
ln -s ../../../../../../hotline.mjs/hotline.min.mjs svoboda/accounts/system/public/js/modules/hotline.min.mjs
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Installing "graph.min.mjs"
|
||||||
|
if [ ! -h svoboda/accounts/system/public/js/modules/graph.min.mjs ]; then
|
||||||
|
ln -s ../../../../../../graph.mjs/graph.min.mjs svoboda/accounts/system/public/js/modules/graph.min.mjs
|
||||||
|
fi
|
|
@ -1,160 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace mirzaev\site\account\models;
|
|
||||||
|
|
||||||
use mirzaev\minimal\model;
|
|
||||||
|
|
||||||
use mirzaev\arangodb\connection;
|
|
||||||
|
|
||||||
use exception;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ядро моделей
|
|
||||||
*
|
|
||||||
* @package mirzaev\site\account\models
|
|
||||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
|
||||||
*/
|
|
||||||
class core extends model
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Коллекция в которой хранятся аккаунты
|
|
||||||
*/
|
|
||||||
public const SETTINGS = '../settings/arangodb.php';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Постфикс
|
|
||||||
*/
|
|
||||||
public string $postfix = '';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Соединение с базой данных
|
|
||||||
*/
|
|
||||||
protected static connection $db;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Конструктор
|
|
||||||
*
|
|
||||||
* @param bool $initialize Инициализировать контроллер?
|
|
||||||
* @param connection $db Инстанция соединения с базой данных
|
|
||||||
*/
|
|
||||||
public function __construct(bool $initialize = true, connection $db = null)
|
|
||||||
{
|
|
||||||
parent::__construct($initialize);
|
|
||||||
|
|
||||||
if ($initialize) {
|
|
||||||
// Запрошена инициализация
|
|
||||||
|
|
||||||
if (isset($db)) {
|
|
||||||
// Получена инстанция соединения с базой данных
|
|
||||||
|
|
||||||
// Запись и инициализация соединения с базой данных
|
|
||||||
$this->__set('db', $db);
|
|
||||||
} else {
|
|
||||||
// Не получена инстанция соединения с базой данных
|
|
||||||
|
|
||||||
// Инициализация соединения с базой данных по умолчанию
|
|
||||||
$this->__get('db');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Записать свойство
|
|
||||||
*
|
|
||||||
* @param string $name Название
|
|
||||||
* @param mixed $value Значение
|
|
||||||
*/
|
|
||||||
public function __set(string $name, mixed $value = null): void
|
|
||||||
{
|
|
||||||
match ($name) {
|
|
||||||
'db' => (function () use ($value) {
|
|
||||||
if ($this->__isset('db')) {
|
|
||||||
// Свойство уже было инициализировано
|
|
||||||
|
|
||||||
// Выброс исключения (неудача)
|
|
||||||
throw new exception('Запрещено реинициализировать соединение с базой данных ($this->db)', 500);
|
|
||||||
} else {
|
|
||||||
// Свойство ещё не было инициализировано
|
|
||||||
|
|
||||||
if ($value instanceof connection) {
|
|
||||||
// Передано подходящее значение
|
|
||||||
|
|
||||||
// Запись свойства (успех)
|
|
||||||
self::$db = $value;
|
|
||||||
} else {
|
|
||||||
// Передано неподходящее значение
|
|
||||||
|
|
||||||
// Выброс исключения (неудача)
|
|
||||||
throw new exception('Соединение с базой данных ($this->db) должен быть инстанцией mirzaev\arangodb\connection', 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})(),
|
|
||||||
default => parent::__set($name, $value)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Прочитать свойство
|
|
||||||
*
|
|
||||||
* @param string $name Название
|
|
||||||
*
|
|
||||||
* @return mixed Содержимое
|
|
||||||
*/
|
|
||||||
public function __get(string $name): mixed
|
|
||||||
{
|
|
||||||
return match ($name) {
|
|
||||||
'db' => (function () {
|
|
||||||
if (!$this->__isset('db')) {
|
|
||||||
// Свойство не инициализировано
|
|
||||||
|
|
||||||
// Инициализация значения по умолчанию исходя из настроек
|
|
||||||
$this->__set('db', new connection(require static::SETTINGS));
|
|
||||||
}
|
|
||||||
|
|
||||||
return self::$db;
|
|
||||||
})(),
|
|
||||||
default => parent::__get($name)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Проверить свойство на инициализированность
|
|
||||||
*
|
|
||||||
* @param string $name Название
|
|
||||||
*/
|
|
||||||
public function __isset(string $name): bool
|
|
||||||
{
|
|
||||||
return match ($name) {
|
|
||||||
default => parent::__isset($name)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Удалить свойство
|
|
||||||
*
|
|
||||||
* @param string $name Название
|
|
||||||
*/
|
|
||||||
public function __unset(string $name): void
|
|
||||||
{
|
|
||||||
match ($name) {
|
|
||||||
default => parent::__isset($name)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Статический вызов
|
|
||||||
*
|
|
||||||
* @param string $name Название
|
|
||||||
* @param array $arguments Параметры
|
|
||||||
*/
|
|
||||||
public static function __callStatic(string $name, array $arguments): mixed
|
|
||||||
{
|
|
||||||
match ($name) {
|
|
||||||
'db' => (new static)->__get('db'),
|
|
||||||
default => throw new exception("Не найдено свойство или функция: $name", 500)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,352 +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;
|
|
||||||
|
|
||||||
// Библиотека для ArangoDB
|
|
||||||
use ArangoDBClient\Document as _document;
|
|
||||||
|
|
||||||
// Встроенные библиотеки
|
|
||||||
use exception;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Модель сессий
|
|
||||||
*
|
|
||||||
* @package mirzaev\site\account\models
|
|
||||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
|
||||||
*/
|
|
||||||
final class session extends core
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Коллекция
|
|
||||||
*/
|
|
||||||
public const COLLECTION = 'session';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Инстанция документа сессии в базе данных
|
|
||||||
*/
|
|
||||||
public _document $document;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Конструктор
|
|
||||||
*
|
|
||||||
* Инициализация сессии и запись в свойство $this->document
|
|
||||||
*
|
|
||||||
* @param ?string $hash Хеш сессии в базе данных
|
|
||||||
* @param ?int $expires Дата окончания работы сессии (используется при создании новой сессии)
|
|
||||||
* @param array &$errors Реестр ошибок
|
|
||||||
*
|
|
||||||
* @return static Инстанция сессии
|
|
||||||
*/
|
|
||||||
public function __construct(?string $hash = null, ?int $expires = null, array &$errors = [])
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
if (collection::init(static::$db->session, self::COLLECTION)) {
|
|
||||||
// Инициализирована коллекция
|
|
||||||
|
|
||||||
if (isset($hash) && $session = collection::search(static::$db->session, sprintf(
|
|
||||||
<<<AQL
|
|
||||||
FOR d IN %s
|
|
||||||
FILTER d.hash == '$hash' && d.expires > %d && d.status == 'active'
|
|
||||||
RETURN d
|
|
||||||
AQL,
|
|
||||||
self::COLLECTION,
|
|
||||||
time()
|
|
||||||
))) {
|
|
||||||
// Найдена сессия по хешу
|
|
||||||
|
|
||||||
// Запись в свойство
|
|
||||||
$this->document = $session;
|
|
||||||
} else if ($session = collection::search(static::$db->session, sprintf(
|
|
||||||
<<<AQL
|
|
||||||
FOR d IN %s
|
|
||||||
FILTER d.ip == '%s' && d.expires > %d && d.status == 'active'
|
|
||||||
RETURN d
|
|
||||||
AQL,
|
|
||||||
self::COLLECTION,
|
|
||||||
$_SERVER['REMOTE_ADDR'],
|
|
||||||
time()
|
|
||||||
))) {
|
|
||||||
// Найдена сессия по данным пользователя
|
|
||||||
|
|
||||||
// Запись в свойство
|
|
||||||
$this->document = $session;
|
|
||||||
} else {
|
|
||||||
// Не найдена сессия
|
|
||||||
|
|
||||||
// Запись сессии в базу данных
|
|
||||||
$_id = document::write(static::$db->session, self::COLLECTION, [
|
|
||||||
'status' => 'active',
|
|
||||||
'expires' => $expires ?? time() + 604800,
|
|
||||||
'ip' => $_SERVER['REMOTE_ADDR']
|
|
||||||
]);
|
|
||||||
|
|
||||||
if ($session = collection::search(static::$db->session, sprintf(
|
|
||||||
<<<AQL
|
|
||||||
FOR d IN %s
|
|
||||||
FILTER d._id == '$_id' && d.expires > %d && d.status == 'active'
|
|
||||||
RETURN d
|
|
||||||
AQL,
|
|
||||||
self::COLLECTION,
|
|
||||||
time()
|
|
||||||
))) {
|
|
||||||
// Найдена только что созданная сессия
|
|
||||||
|
|
||||||
// Запись хеша
|
|
||||||
$session->hash = sodium_bin2hex(sodium_crypto_generichash($_id));
|
|
||||||
|
|
||||||
if (document::update(static::$db->session, $session)) {
|
|
||||||
// Записано обновление
|
|
||||||
|
|
||||||
// Запись в свойство
|
|
||||||
$this->document = $session;
|
|
||||||
} 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()
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function __destruct()
|
|
||||||
{
|
|
||||||
// Закрыть сессию
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Инициализировать связб сессии с аккаунтом
|
|
||||||
*
|
|
||||||
* Ищет связь сессии с аккаунтом, если не находит, то создаёт её
|
|
||||||
*
|
|
||||||
* @param account $account Инстанция аккаунта
|
|
||||||
* @param array &$errors Реестр ошибок
|
|
||||||
*
|
|
||||||
* @return bool Связан аккаунт?
|
|
||||||
*/
|
|
||||||
public function connect(account $account, array &$errors = []): bool
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
if (
|
|
||||||
collection::init(static::$db->session, self::COLLECTION)
|
|
||||||
&& collection::init(static::$db->session, account::COLLECTION)
|
|
||||||
&& collection::init(static::$db->session, self::COLLECTION . '_edge_' . account::COLLECTION, true)
|
|
||||||
) {
|
|
||||||
// Инициализирована коллекция
|
|
||||||
|
|
||||||
if (
|
|
||||||
collection::search(static::$db->session, sprintf(
|
|
||||||
<<<AQL
|
|
||||||
FOR document IN %s
|
|
||||||
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;
|
|
||||||
} else throw new exception('Не удалось создать ребро: session -> account');
|
|
||||||
} else throw new exception('Не удалось инициализировать коллекцию');
|
|
||||||
} catch (exception $e) {
|
|
||||||
// Запись в реестр ошибок
|
|
||||||
$errors[] = [
|
|
||||||
'text' => $e->getMessage(),
|
|
||||||
'file' => $e->getFile(),
|
|
||||||
'line' => $e->getLine(),
|
|
||||||
'stack' => $e->getTrace()
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Найти связанный аккаунт
|
|
||||||
*
|
|
||||||
* @param array &$errors Реестр ошибок
|
|
||||||
*
|
|
||||||
* @return ?account Инстанция аккаунта, если удалось найти
|
|
||||||
*/
|
|
||||||
public function account(array &$errors = []): ?account
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
if (
|
|
||||||
collection::init(static::$db->session, self::COLLECTION)
|
|
||||||
&& collection::init(static::$db->session, account::COLLECTION)
|
|
||||||
&& collection::init(static::$db->session, self::COLLECTION . '_edge_' . account::COLLECTION, true)
|
|
||||||
) {
|
|
||||||
// Инициализированы коллекции
|
|
||||||
|
|
||||||
// Инициализация инстанции аккаунта
|
|
||||||
$account = new account;
|
|
||||||
|
|
||||||
// Поиск инстанции аккаунта в базе данных
|
|
||||||
$account->document = collection::search(static::$db->session, sprintf(
|
|
||||||
<<<AQL
|
|
||||||
FOR document IN %s
|
|
||||||
LET edge = (
|
|
||||||
FOR edge IN %s
|
|
||||||
FILTER edge._from == '%s'
|
|
||||||
SORT edge._key DESC
|
|
||||||
LIMIT 1
|
|
||||||
RETURN edge
|
|
||||||
)
|
|
||||||
FILTER document._id == edge[0]._to
|
|
||||||
LIMIT 1
|
|
||||||
RETURN document
|
|
||||||
AQL,
|
|
||||||
account::COLLECTION,
|
|
||||||
self::COLLECTION . '_edge_' . account::COLLECTION,
|
|
||||||
$this->getId()
|
|
||||||
));
|
|
||||||
|
|
||||||
if ($account->document instanceof _document) 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 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Записать
|
|
||||||
*
|
|
||||||
* Записывает свойство в инстанцию документа сессии из базы данных
|
|
||||||
*
|
|
||||||
* @param string $name Название
|
|
||||||
* @param mixed $value Содержимое
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function __set(string $name, mixed $value = null): void
|
|
||||||
{
|
|
||||||
$this->document->{$name} = $value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Прочитать
|
|
||||||
*
|
|
||||||
* Читает свойство из инстанции документа сессии из базы данных
|
|
||||||
*
|
|
||||||
* @param string $name Название
|
|
||||||
*
|
|
||||||
* @return mixed Данные свойства инстанции сессии или инстанции документа сессии из базы данных
|
|
||||||
*/
|
|
||||||
public function __get(string $name): mixed
|
|
||||||
{
|
|
||||||
return $this->document->{$name};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Проверить инициализированность
|
|
||||||
*
|
|
||||||
* Проверяет инициализированность свойства в инстанции документа сессии из базы данных
|
|
||||||
*
|
|
||||||
* @param string $name Название
|
|
||||||
*
|
|
||||||
* @return bool Свойство инициализировано?
|
|
||||||
*/
|
|
||||||
public function __isset(string $name): bool
|
|
||||||
{
|
|
||||||
return isset($this->document->{$name});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Удалить
|
|
||||||
*
|
|
||||||
* Деинициализировать свойство в инстанции документа сессии из базы данных
|
|
||||||
*
|
|
||||||
* @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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,268 +0,0 @@
|
||||||
@keyframes glare {
|
|
||||||
2%,
|
|
||||||
100% {
|
|
||||||
left : 130%;
|
|
||||||
bottom : -200%;
|
|
||||||
width : 120px;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
z-index: 1000;
|
|
||||||
top: 20%;
|
|
||||||
position: relative;
|
|
||||||
height: unset;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: unset;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.column {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
section.panel {
|
|
||||||
--display : flex;
|
|
||||||
z-index : 1000;
|
|
||||||
width : 400px;
|
|
||||||
position : absolute;
|
|
||||||
display : flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.column>section.panel {
|
|
||||||
position : unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
section.panel.medium {
|
|
||||||
width: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
section.panel.small {
|
|
||||||
width: 220px;
|
|
||||||
}
|
|
||||||
|
|
||||||
section.panel#mnemonic {
|
|
||||||
margin-left: -570px;
|
|
||||||
}
|
|
||||||
|
|
||||||
section.panel#classic {
|
|
||||||
margin-left: 570px;
|
|
||||||
}
|
|
||||||
|
|
||||||
section.panel>section.body>ul {
|
|
||||||
margin: 0 5%;
|
|
||||||
padding: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
list-style: square;
|
|
||||||
}
|
|
||||||
|
|
||||||
section.panel>section.body>ul>li {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
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 {
|
|
||||||
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 {
|
|
||||||
z-index : 1000;
|
|
||||||
height : 50px;
|
|
||||||
display : flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: end;
|
|
||||||
animation-duration: 120s;
|
|
||||||
border-radius : 3px 3px 0 0;
|
|
||||||
background-color : var(--background-above);
|
|
||||||
}
|
|
||||||
|
|
||||||
section#profile>section.header {
|
|
||||||
margin-left : -50px;
|
|
||||||
height : 100px;
|
|
||||||
padding : 30px 0;
|
|
||||||
clip-path : url(#profile-header-mask);
|
|
||||||
}
|
|
||||||
|
|
||||||
section#profile>section.header>img.avatar {
|
|
||||||
z-index : 1500;
|
|
||||||
left : 6px;
|
|
||||||
top : 36px;
|
|
||||||
width : 88px;
|
|
||||||
height : 88px;
|
|
||||||
position : absolute;
|
|
||||||
margin : auto;
|
|
||||||
object-fit : cover;
|
|
||||||
border-radius : 100%;
|
|
||||||
cursor : pointer;
|
|
||||||
image-rendering : smooth;
|
|
||||||
box-shadow : 0px 0px 12px 0px rgba(0, 0, 0, 0.5);
|
|
||||||
-webkit-box-shadow: 0px 0px 12px 0px rgba(0, 0, 0, 0.5);
|
|
||||||
-moz-box-shadow : 0px 0px 12px 0px rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
section#profile>section.header>img.avatar:hover {
|
|
||||||
left : 0;
|
|
||||||
top : 30px;
|
|
||||||
width : 100px;
|
|
||||||
height : 100px;
|
|
||||||
box-shadow : 0px 0px 8px 0px rgba(0, 0, 0, 0.3);
|
|
||||||
-webkit-box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.3);
|
|
||||||
-moz-box-shadow : 0px 0px 8px 0px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
section#profile>section.header>img.cover {
|
|
||||||
z-index : -5000;
|
|
||||||
left : -50px;
|
|
||||||
top : 0;
|
|
||||||
position : absolute;
|
|
||||||
width : calc(100% + 100px);
|
|
||||||
height : 100%;
|
|
||||||
object-position: 0px 30%;
|
|
||||||
object-fit : cover;
|
|
||||||
clip-path : polygon(50px 0, calc(100% - 50px) 0, calc(100% - 50px) 100%, 50px 100%);
|
|
||||||
border-radius : 0 0 3px 3px;
|
|
||||||
background : var(--background-above);
|
|
||||||
}
|
|
||||||
|
|
||||||
section#profile>section.header>div.glare {
|
|
||||||
z-index : 3000;
|
|
||||||
left : -30px;
|
|
||||||
top : -300px;
|
|
||||||
width : 30px;
|
|
||||||
height : 400%;
|
|
||||||
position : absolute;
|
|
||||||
rotate : 25deg;
|
|
||||||
opacity : 0.2;
|
|
||||||
filter : unset;
|
|
||||||
pointer-events : none;
|
|
||||||
animation-name : glare;
|
|
||||||
animation-duration : 32s;
|
|
||||||
animation-delay : 2s;
|
|
||||||
animation-fill-mode : forwards;
|
|
||||||
animation-timing-function: linear;
|
|
||||||
background-color : #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
section#profile>section.header>div {
|
|
||||||
animation-duration: 80s;
|
|
||||||
}
|
|
||||||
|
|
||||||
section#profile>section.header>a {
|
|
||||||
margin : auto;
|
|
||||||
width : 100%;
|
|
||||||
margin-left : 110px;
|
|
||||||
padding-bottom: 0.5ex;
|
|
||||||
white-space : nowrap;
|
|
||||||
overflow-x : hidden;
|
|
||||||
text-overflow : ellipsis;
|
|
||||||
font-size : 1.3em;
|
|
||||||
font-weight : bold;
|
|
||||||
color : var(--text-inverse);
|
|
||||||
text-shadow : 0 0 8px #00000080;
|
|
||||||
}
|
|
||||||
|
|
||||||
section.panel>section.header>:is(h1, h2, h3) {
|
|
||||||
margin-bottom: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
section.panel>section.body {
|
|
||||||
padding : 20px 30px;
|
|
||||||
gap : 10px;
|
|
||||||
display : flex;
|
|
||||||
flex-direction : column;
|
|
||||||
border-radius : 0 0 3px 3px;
|
|
||||||
background-color : var(--background-above);
|
|
||||||
}
|
|
||||||
|
|
||||||
section#profile>section.body>ul {
|
|
||||||
margin : unset;
|
|
||||||
margin-left : 10%;
|
|
||||||
}
|
|
||||||
|
|
||||||
section#profile>section.body ul ul {
|
|
||||||
padding-top: 1ex;
|
|
||||||
}
|
|
||||||
|
|
||||||
section#profile>section.body ul li:not(:last-child) {
|
|
||||||
margin-bottom: 1ex;
|
|
||||||
}
|
|
||||||
|
|
||||||
section#profile>section.body div.buttons {
|
|
||||||
margin-top: 10px;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
section#profile>section.body div.buttons>button {
|
|
||||||
padding : 1ex 2ex;
|
|
||||||
cursor : pointer;
|
|
||||||
border-radius : 3px;
|
|
||||||
font-size : 0.9em;
|
|
||||||
background-color: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
section#profile>section.body div.buttons>button:hover {
|
|
||||||
color: var(--text-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
section#profile>section.body div.buttons>button:active {
|
|
||||||
color : var(--text-active);
|
|
||||||
transition: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
section#profile>section.body div.buttons>button:first-of-type {
|
|
||||||
margin-left : auto;
|
|
||||||
margin-right: 5%;
|
|
||||||
}
|
|
||||||
|
|
||||||
section#profile>section.body div.buttons>button:last-of-type {
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
section#profile>section.body div.buttons>button.accept {
|
|
||||||
padding : 1ex 5ex;
|
|
||||||
color : var(--text-inverse);
|
|
||||||
background-color: #63954d;
|
|
||||||
}
|
|
||||||
|
|
||||||
section#profile>section.body div.buttons>button.accept:hover {
|
|
||||||
color : var(--text-inverse-above);
|
|
||||||
background-color: #6fa259;
|
|
||||||
}
|
|
||||||
|
|
||||||
section#profile>section.body div.buttons>button.accept:active {
|
|
||||||
background-color: #63954d;
|
|
||||||
}
|
|
|
@ -1,288 +0,0 @@
|
||||||
@media (prefers-color-scheme: light) {
|
|
||||||
:root {
|
|
||||||
--background-above-1 : #fff;
|
|
||||||
--background-above : #fff6f6;
|
|
||||||
--background : #e8dada;
|
|
||||||
--background-below : #d7c5c5;
|
|
||||||
--background-inverse : #221e1e;
|
|
||||||
--background-inverse-dark : #120f0f;
|
|
||||||
--node-background-important: #c3eac3;
|
|
||||||
--node-background-completed: #b0c0b0;
|
|
||||||
--node-background : #bdb;
|
|
||||||
--connection : #b2b7b2;
|
|
||||||
--connection-completed : #d1d1d1;
|
|
||||||
--text : #151313;
|
|
||||||
--text-hover : #463e3e;
|
|
||||||
--text-active : #0e0e0e;
|
|
||||||
--text-inverse-above : #fff;
|
|
||||||
--text-inverse : #efefef;
|
|
||||||
--text-inverse-below : #d0d0d0;
|
|
||||||
--text-red : #f8a2a2;
|
|
||||||
--text-red-hover : #ffbcbc;
|
|
||||||
--text-red-active : #e69191;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
--background-above-1: #322d2d;
|
|
||||||
--background-above : #2b2525;
|
|
||||||
--background : #221e1e;
|
|
||||||
--background-below : #121010;
|
|
||||||
--node-background : #221e1e;
|
|
||||||
--text : #e6e6e6;
|
|
||||||
--text-hover : #fff;
|
|
||||||
--text-active : #d0d0d0;
|
|
||||||
--text-inverse : #020202;
|
|
||||||
--red-light-1 : #dc4343;
|
|
||||||
--red-light : #bf3737;
|
|
||||||
--red : #a43333;
|
|
||||||
--red-dark : #8d2a2a;
|
|
||||||
--input-error : #6c2424;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes page-background-gradient {
|
|
||||||
25% {
|
|
||||||
left: -350%;
|
|
||||||
top : 0%;
|
|
||||||
}
|
|
||||||
|
|
||||||
50% {
|
|
||||||
left: 0%;
|
|
||||||
top : 0%;
|
|
||||||
}
|
|
||||||
|
|
||||||
75% {
|
|
||||||
left: 0%;
|
|
||||||
top : -350%;
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
left: -350%;
|
|
||||||
top : -350%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--link : #3c76ff;
|
|
||||||
--link-hover : #6594ff;
|
|
||||||
--link-active: #3064dd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.unselectable {
|
|
||||||
-webkit-touch-callout: none;
|
|
||||||
-webkit-user-select : none;
|
|
||||||
-khtml-user-select : none;
|
|
||||||
-moz-user-select : none;
|
|
||||||
-ms-user-select : none;
|
|
||||||
user-select : none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hidden:not(.animation) {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
text-decoration: none;
|
|
||||||
outline : none;
|
|
||||||
border : none;
|
|
||||||
color : var(--text);
|
|
||||||
font-family : Fira, sans-serif;
|
|
||||||
transition : 0.1s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre, code {
|
|
||||||
font-family: Hack, monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: var(--link);
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
color: var(--link-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
a:active {
|
|
||||||
color : var(--link-active);
|
|
||||||
transition: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
position: relative;
|
|
||||||
height: 26px;
|
|
||||||
display: flex;
|
|
||||||
overflow: hidden;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
label>i:first-child {
|
|
||||||
left: 8px;
|
|
||||||
top: calc((26px - var(--height)) / 2);
|
|
||||||
position: absolute !important;
|
|
||||||
margin: auto;
|
|
||||||
color: #8c7d7d;
|
|
||||||
}
|
|
||||||
|
|
||||||
label * {
|
|
||||||
/* color: var(--text-inverse); */
|
|
||||||
}
|
|
||||||
|
|
||||||
label>input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0 8px;
|
|
||||||
background-color: var(--background-above-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
label>input+button {
|
|
||||||
background-color: var(--red);
|
|
||||||
}
|
|
||||||
|
|
||||||
i+input {
|
|
||||||
padding-left: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
input.error {
|
|
||||||
animation-duration : 1s;
|
|
||||||
animation-name : input-error;
|
|
||||||
animation-fill-mode : forwards;
|
|
||||||
animation-timing-function: ease-in;
|
|
||||||
}
|
|
||||||
|
|
||||||
section.header>h1 {
|
|
||||||
font-size: 1.3rem;
|
|
||||||
line-height: 1.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
section.header>:is(h2, h3) {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
line-height: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
height : 100vh;
|
|
||||||
margin : 0;
|
|
||||||
background-color: var(--background);
|
|
||||||
}
|
|
||||||
|
|
||||||
body>div.background {
|
|
||||||
z-index : -50000;
|
|
||||||
left : -350%;
|
|
||||||
top : -350%;
|
|
||||||
width : 500%;
|
|
||||||
height : 500%;
|
|
||||||
position : absolute;
|
|
||||||
filter : blur(200px);
|
|
||||||
animation-duration : 15s;
|
|
||||||
animation-name : page-background-gradient;
|
|
||||||
animation-iteration-count: infinite;
|
|
||||||
background-repeat : no-repeat;
|
|
||||||
animation-timing-function: linear;
|
|
||||||
background-image : radial-gradient(circle, var(--background-above) 0%, rgba(0, 0, 0, 0) 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
aside {
|
|
||||||
z-index : 500;
|
|
||||||
grid-column: 1/ 4;
|
|
||||||
grid-row : 2;
|
|
||||||
overflow : hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
z-index : 5000;
|
|
||||||
position : absolute;
|
|
||||||
display : flex;
|
|
||||||
flex-direction: column;
|
|
||||||
box-shadow : 2px 0 5px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
header>menu {
|
|
||||||
margin : unset;
|
|
||||||
padding : 20px;
|
|
||||||
display : flex;
|
|
||||||
flex-direction : column;
|
|
||||||
flex-grow : 1;
|
|
||||||
background-color: var(--background-light-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
header>#account>button#login {
|
|
||||||
z-index: 1500;
|
|
||||||
}
|
|
||||||
|
|
||||||
header>menu a {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
display : flex;
|
|
||||||
align-items : center;
|
|
||||||
}
|
|
||||||
|
|
||||||
header>menu a:last-child {
|
|
||||||
margin-bottom: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
header>menu a svg {
|
|
||||||
margin-right: 8px;
|
|
||||||
height : 1.2rem;
|
|
||||||
position : relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
header>menu a:hover svg {
|
|
||||||
margin-left : -5px;
|
|
||||||
margin-right: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
header>menu a svg path {
|
|
||||||
fill: var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
header>section {
|
|
||||||
background-color: var(--background-light-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
header :is(button, a[type="button"]) {
|
|
||||||
width : 100%;
|
|
||||||
height : 40px;
|
|
||||||
display : flex;
|
|
||||||
justify-content : center;
|
|
||||||
align-items : center;
|
|
||||||
cursor : pointer;
|
|
||||||
background-color: var(--red);
|
|
||||||
transition : unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
header button {
|
|
||||||
font-weight : bold;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
header :is(button, a[type="button"]):hover {
|
|
||||||
background-color: var(--red-light);
|
|
||||||
}
|
|
||||||
|
|
||||||
header :is(button, a[type="button"]):active {
|
|
||||||
background-color: var(--red-dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
header>nav {
|
|
||||||
margin-top : auto;
|
|
||||||
display : flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
z-index : 1000;
|
|
||||||
height : 100%;
|
|
||||||
display : flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content : center;
|
|
||||||
align-items : center;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
z-index : 3000;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace mirzaev\site\account;
|
|
||||||
|
|
||||||
// Файлы проекта
|
|
||||||
use mirzaev\site\account\controllers\core as controller,
|
|
||||||
mirzaev\site\account\models\core as model;
|
|
||||||
|
|
||||||
// Фреймворк
|
|
||||||
use mirzaev\minimal\core,
|
|
||||||
mirzaev\minimal\router;
|
|
||||||
|
|
||||||
ini_set('error_reporting', E_ALL);
|
|
||||||
ini_set('display_errors', 1);
|
|
||||||
ini_set('display_startup_errors', 1);
|
|
||||||
|
|
||||||
define('VIEWS', realpath('..' . DIRECTORY_SEPARATOR . 'views'));
|
|
||||||
define('STORAGE', realpath('..' . DIRECTORY_SEPARATOR . 'storage'));
|
|
||||||
define('INDEX', __DIR__);
|
|
||||||
|
|
||||||
// Автозагрузка
|
|
||||||
require __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php';
|
|
||||||
|
|
||||||
// Инициализация маршрутазитора
|
|
||||||
$router = new router;
|
|
||||||
|
|
||||||
// Запись маршрутов
|
|
||||||
$router->write('/', 'index', 'index');
|
|
||||||
$router->write('/system/hotline', 'hotline', 'index');
|
|
||||||
$router->write('/system/graph', 'graph', 'index');
|
|
||||||
$router->write('/account/initialization', 'account', 'initialization', 'POST');
|
|
||||||
$router->write('/account/vk/connect', 'account', 'connect');
|
|
||||||
$router->write('/account/panel', 'account', 'panel');
|
|
||||||
$router->write('/api/generate/password', 'api', 'password', 'POST');
|
|
||||||
$router->write('/session/login', 'session', 'login', 'POST');
|
|
||||||
$router->write('/session/password', 'session', 'password', 'POST');
|
|
||||||
$router->write('/session/invite', 'session', 'invite', 'POST');
|
|
||||||
|
|
||||||
// Инициализация ядра
|
|
||||||
$core = new core(namespace: __NAMESPACE__, router: $router, controller: new controller(false), model: new model(false));
|
|
||||||
|
|
||||||
// Обработка запроса
|
|
||||||
echo $core->start();
|
|
|
@ -1 +0,0 @@
|
||||||
arangodb.php
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<psalm
|
||||||
|
errorLevel="7"
|
||||||
|
resolveFromConfigFile="true"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns="https://getpsalm.org/schema/config"
|
||||||
|
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
|
||||||
|
findUnusedBaselineEntry="true"
|
||||||
|
>
|
||||||
|
<projectFiles>
|
||||||
|
<directory name="mirzaev/site/account/system" />
|
||||||
|
<ignoreFiles>
|
||||||
|
<directory name="vendor" />
|
||||||
|
</ignoreFiles>
|
||||||
|
</projectFiles>
|
||||||
|
</psalm>
|
|
@ -13,9 +13,6 @@ use mirzaev\site\account\views\templater,
|
||||||
// Фреймворк PHP
|
// Фреймворк PHP
|
||||||
use mirzaev\minimal\controller;
|
use mirzaev\minimal\controller;
|
||||||
|
|
||||||
// Встроенные библиотеки
|
|
||||||
use exception;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ядро контроллеров
|
* Ядро контроллеров
|
||||||
*
|
*
|
||||||
|
@ -24,25 +21,25 @@ use exception;
|
||||||
*/
|
*/
|
||||||
class core extends controller
|
class core extends controller
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Постфикс
|
||||||
|
*/
|
||||||
|
final public const POSTFIX = '';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Инстанция сессии
|
* Инстанция сессии
|
||||||
*/
|
*/
|
||||||
public session $session;
|
protected readonly session $session;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Инстанция аккаунта
|
* Инстанция аккаунта
|
||||||
*/
|
*/
|
||||||
public ?account $account;
|
protected readonly ?account $account;
|
||||||
|
|
||||||
/**
|
|
||||||
* Постфикс
|
|
||||||
*/
|
|
||||||
public string $postfix = '';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Реестр ошибок
|
* Реестр ошибок
|
||||||
*/
|
*/
|
||||||
public array $errors = [
|
protected array $errors = [
|
||||||
'session' => [],
|
'session' => [],
|
||||||
'account' => []
|
'account' => []
|
||||||
];
|
];
|
||||||
|
@ -63,7 +60,7 @@ class core extends controller
|
||||||
new models();
|
new models();
|
||||||
|
|
||||||
// Инициализация даты до которой будет активна сессия
|
// Инициализация даты до которой будет активна сессия
|
||||||
$expires = time() + 604800;
|
$expires = strtotime( '+1 week' );
|
||||||
|
|
||||||
// Инициализация значения по умолчанию
|
// Инициализация значения по умолчанию
|
||||||
$_COOKIE["session"] ??= null;
|
$_COOKIE["session"] ??= null;
|
|
@ -10,6 +10,9 @@ use mirzaev\site\account\controllers\core,
|
||||||
mirzaev\site\account\models\invite,
|
mirzaev\site\account\models\invite,
|
||||||
mirzaev\site\account\models\account;
|
mirzaev\site\account\models\account;
|
||||||
|
|
||||||
|
// Библиотека для ArangoDB
|
||||||
|
use ArangoDBClient\Document as _document;
|
||||||
|
|
||||||
// Встроенные библиотеки
|
// Встроенные библиотеки
|
||||||
use exception;
|
use exception;
|
||||||
|
|
||||||
|
@ -53,19 +56,31 @@ final class session extends core
|
||||||
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));
|
if (preg_match_all('/[^\w\s\r\n\t\0]+/u', $parameters['login'], $matches) > 0) throw new exception('Нельзя использовать символы: ' . implode(', ', ...$matches));
|
||||||
|
|
||||||
|
if ($remember = isset($parameters['remember']) && $parameters['remember'] === '1') {
|
||||||
|
// Запрошено запоминание
|
||||||
|
|
||||||
|
// Запись в cookie
|
||||||
|
setcookie('entry_login', $parameters['login'], [
|
||||||
|
'expires' => strtotime('+30 minutes'),
|
||||||
|
'path' => '/',
|
||||||
|
'secure' => true,
|
||||||
|
'httponly' => true,
|
||||||
|
'samesite' => 'strict'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
// Поиск аккаунта
|
// Поиск аккаунта
|
||||||
$account = account::login($parameters['login']);
|
$account = account::login($parameters['login']);
|
||||||
|
|
||||||
// Генерация ответа по запрашиваемым параметрам
|
// Генерация ответа по запрашиваемым параметрам
|
||||||
foreach ($return as $parameter) match ($parameter) {
|
foreach ($return as $parameter) match ($parameter) {
|
||||||
'exist' => $buffer['exist'] = isset($account->document),
|
'exist' => $buffer['exist'] = isset($account) && $account->instance() instanceof _document,
|
||||||
'account' => (function () use ($parameters, &$buffer) {
|
'account' => (function () use ($parameters, $remember, &$buffer) {
|
||||||
// Запись в буфер сессии
|
// Запись в буфер сессии
|
||||||
if (isset($parameters['remember']) && $parameters['remember'] === '1')
|
if ($remember) $this->session->write(['entry' => ['login' => $parameters['login']]], $this->errors);
|
||||||
$this->session->write(['entry' => ['login' => $parameters['login']]], $this->errors);
|
|
||||||
|
|
||||||
// Поиск аккаунта и запись в буфер вывода
|
// Поиск аккаунта и запись в буфер вывода
|
||||||
$buffer['account'] = isset((new account($this->session, authenticate: true, errors: $this->errors))->document);
|
$buffer['account'] = (new account($this->session, authenticate: true, errors: $this->errors))?->instance() instanceof _document;
|
||||||
})(),
|
})(),
|
||||||
'errors' => null,
|
'errors' => null,
|
||||||
default => throw new exception("Параметр не найден: $parameter")
|
default => throw new exception("Параметр не найден: $parameter")
|
||||||
|
@ -102,7 +117,7 @@ final class session extends core
|
||||||
flush();
|
flush();
|
||||||
|
|
||||||
// Запись в буфер сессии
|
// Запись в буфер сессии
|
||||||
if (!in_array('account', $return, true) && isset($parameters['remember']) && $parameters['remember'] === '1')
|
if (!in_array('account', $return, true) && ($remember ?? false))
|
||||||
$this->session->write(['entry' => ['login' => $parameters['login']]]);
|
$this->session->write(['entry' => ['login' => $parameters['login']]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,7 +156,7 @@ final class session extends core
|
||||||
$this->session->write(['entry' => ['password' => $parameters['password']]], $this->errors);
|
$this->session->write(['entry' => ['password' => $parameters['password']]], $this->errors);
|
||||||
|
|
||||||
// Поиск аккаунта и запись в буфер вывода
|
// Поиск аккаунта и запись в буфер вывода
|
||||||
$buffer['account'] = isset((new account($this->session, authenticate: true, register: true, errors: $this->errors))->document);
|
$buffer['account'] = (new account($this->session, authenticate: true, register: true, errors: $this->errors))?->instance() instanceof _document;
|
||||||
})(),
|
})(),
|
||||||
'errors' => null,
|
'errors' => null,
|
||||||
default => throw new exception("Параметр не найден: $parameter")
|
default => throw new exception("Параметр не найден: $parameter")
|
||||||
|
@ -216,7 +231,7 @@ final class session extends core
|
||||||
|
|
||||||
// Генерация ответа по запрашиваемым параметрам
|
// Генерация ответа по запрашиваемым параметрам
|
||||||
foreach ($return as $parameter) match ($parameter) {
|
foreach ($return as $parameter) match ($parameter) {
|
||||||
'exist' => $buffer['exist'] = isset($invite->document),
|
'exist' => $buffer['exist'] = isset($invite) && $invite->instance() instanceof _document,
|
||||||
// from временное решение пока не будет разработана система сессий
|
// from временное решение пока не будет разработана система сессий
|
||||||
'from' => $buffer['from'] = ['login' => 'mirzaev'] ?? $invite->from(),
|
'from' => $buffer['from'] = ['login' => 'mirzaev'] ?? $invite->from(),
|
||||||
'account' => (function () use ($parameters, &$buffer) {
|
'account' => (function () use ($parameters, &$buffer) {
|
||||||
|
@ -225,7 +240,7 @@ final class session extends core
|
||||||
$this->session->write(['entry' => ['invite' => $parameters['invite']]], $this->errors);
|
$this->session->write(['entry' => ['invite' => $parameters['invite']]], $this->errors);
|
||||||
|
|
||||||
// Поиск аккаунта и запись в буфер вывода
|
// Поиск аккаунта и запись в буфер вывода
|
||||||
$buffer['account'] = isset((new account($this->session, authenticate: true, errors: $this->errors))->document);
|
$buffer['account'] = (new account($this->session, authenticate: true, errors: $this->errors))?->instance() instanceof _document;
|
||||||
})(),
|
})(),
|
||||||
'errors' => null,
|
'errors' => null,
|
||||||
default => throw new exception("Параметр не найден: $parameter")
|
default => throw new exception("Параметр не найден: $parameter")
|
|
@ -4,6 +4,9 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace mirzaev\site\account\models;
|
namespace mirzaev\site\account\models;
|
||||||
|
|
||||||
|
// Project files
|
||||||
|
use mirzaev\site\account\models\traits\instance;
|
||||||
|
|
||||||
// Фреймворк ArangoDB
|
// Фреймворк ArangoDB
|
||||||
use mirzaev\arangodb\collection,
|
use mirzaev\arangodb\collection,
|
||||||
mirzaev\arangodb\document;
|
mirzaev\arangodb\document;
|
||||||
|
@ -22,15 +25,17 @@ use exception;
|
||||||
*/
|
*/
|
||||||
final class account extends core
|
final class account extends core
|
||||||
{
|
{
|
||||||
|
use instance;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Коллекция
|
* Коллекция
|
||||||
*/
|
*/
|
||||||
public const COLLECTION = 'account';
|
final public const COLLECTION = 'account';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Инстанция документа аккаунта в базе данных
|
* Инстанция документа аккаунта в базе данных
|
||||||
*/
|
*/
|
||||||
public ?_document $document;
|
protected readonly _document $document;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Конструктор
|
* Конструктор
|
||||||
|
@ -112,7 +117,8 @@ final class account extends core
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
} else throw new exception('Неправильный пароль');
|
} else throw new exception('Неправильный пароль');
|
||||||
} throw new exception('Неправильный пароль');
|
}
|
||||||
|
throw new exception('Неправильный пароль');
|
||||||
} else {
|
} else {
|
||||||
// Не найден аккаунт
|
// Не найден аккаунт
|
||||||
|
|
||||||
|
@ -174,7 +180,6 @@ final class account extends core
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Найти по входному псевдониму
|
* Найти по входному псевдониму
|
||||||
*
|
*
|
||||||
|
@ -186,15 +191,15 @@ final class account extends core
|
||||||
public static function login(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::$arangodb->session, self::COLLECTION)) {
|
||||||
// Инициализирована коллекция
|
// Инициализирована коллекция
|
||||||
|
|
||||||
// Инициализация инстанции аккаунта
|
// Инициализация инстанции аккаунта
|
||||||
$instance = new self;
|
$account = new self;
|
||||||
|
|
||||||
// Поиск инстанции аккаунта в базе данных
|
// Поиск инстанции аккаунта в базе данных
|
||||||
$instance->document = collection::search(
|
$instance = $account->instance(collection::search(
|
||||||
static::$db->session,
|
static::$arangodb->session,
|
||||||
sprintf(
|
sprintf(
|
||||||
<<<'AQL'
|
<<<'AQL'
|
||||||
FOR d IN %s
|
FOR d IN %s
|
||||||
|
@ -204,10 +209,10 @@ final class account extends core
|
||||||
self::COLLECTION,
|
self::COLLECTION,
|
||||||
$login
|
$login
|
||||||
)
|
)
|
||||||
);
|
));
|
||||||
|
|
||||||
if ($instance->document instanceof _document) return $instance;
|
// Возврат (успех)
|
||||||
else throw new exception('Не удалось найти инстанцию аккаунта в базе данных');
|
return $instance instanceof _document ? $account : throw new exception('Не удалось найти инстанцию аккаунта в базе данных');
|
||||||
} else throw new exception('Не удалось инициализировать коллекцию');
|
} else throw new exception('Не удалось инициализировать коллекцию');
|
||||||
} catch (exception $e) {
|
} catch (exception $e) {
|
||||||
// Запись в реестр ошибок
|
// Запись в реестр ошибок
|
||||||
|
@ -233,8 +238,8 @@ final class account extends core
|
||||||
public static function create(array $data = [], array &$errors = []): bool
|
public static function create(array $data = [], array &$errors = []): bool
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
if (collection::init(static::$db->session, self::COLLECTION))
|
if (collection::init(static::$arangodb->session, self::COLLECTION))
|
||||||
if (document::write(static::$db->session, self::COLLECTION, $data)) return true;
|
if (document::write(static::$arangodb->session, self::COLLECTION, $data)) return true;
|
||||||
else throw new exception('Не удалось создать аккаунт');
|
else throw new exception('Не удалось создать аккаунт');
|
||||||
else throw new exception('Не удалось инициализировать коллекцию');
|
else throw new exception('Не удалось инициализировать коллекцию');
|
||||||
} catch (exception $e) {
|
} catch (exception $e) {
|
|
@ -0,0 +1,245 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace mirzaev\site\account\models;
|
||||||
|
|
||||||
|
// Фреймворк PHP
|
||||||
|
use mirzaev\minimal\model;
|
||||||
|
|
||||||
|
// Фреймворк ArangoDB
|
||||||
|
use mirzaev\arangodb\connection as arangodb;
|
||||||
|
|
||||||
|
// Встроенные библиотеки
|
||||||
|
use exception,
|
||||||
|
redis,
|
||||||
|
redisexception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ядро моделей
|
||||||
|
*
|
||||||
|
* @package mirzaev\site\account\models
|
||||||
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
|
*/
|
||||||
|
class core extends model
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Постфикс
|
||||||
|
*/
|
||||||
|
final public const POSTFIX = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Путь до файла с настройками подключения к базе данных ArangoDB
|
||||||
|
*/
|
||||||
|
final public const ARANGODB = '../settings/arangodb.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Путь до файла с настройками подключения к базе данных Redis
|
||||||
|
*/
|
||||||
|
final public const REDIS = '../settings/redis.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Соединение с базой данных ArangoDB
|
||||||
|
*/
|
||||||
|
protected static arangodb $arangodb;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Соединение с базой данных Redis
|
||||||
|
*/
|
||||||
|
protected static redis $redis;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Конструктор
|
||||||
|
*
|
||||||
|
* @param bool $initialize Инициализировать контроллер?
|
||||||
|
* @param ?arangodb $arangodb Инстанция соединения с базой данных ArangoDB
|
||||||
|
* @param ?redis $redis Инстанция соединения с базой данных Redis
|
||||||
|
*/
|
||||||
|
public function __construct(bool $initialize = true, ?arangodb $arangodb = null, ?redis $redis = null)
|
||||||
|
{
|
||||||
|
parent::__construct($initialize);
|
||||||
|
|
||||||
|
if ($initialize) {
|
||||||
|
// Запрошена инициализация
|
||||||
|
|
||||||
|
if (isset($arangodb)) {
|
||||||
|
// Получена инстанция соединения с базой данных
|
||||||
|
|
||||||
|
// Запись и инициализация соединения с базой данных
|
||||||
|
$this->__set('arangodb', $arangodb);
|
||||||
|
} else {
|
||||||
|
// Не получена инстанция соединения с базой данных
|
||||||
|
|
||||||
|
// Инициализация соединения с базой данных по умолчанию
|
||||||
|
$this->__get('arangodb');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($redis)) {
|
||||||
|
// Получена инстанция соединения с базой данных
|
||||||
|
|
||||||
|
// Запись и инициализация соединения с базой данных
|
||||||
|
$this->__set('redis', $redis);
|
||||||
|
} else {
|
||||||
|
// Не получена инстанция соединения с базой данных
|
||||||
|
|
||||||
|
// Инициализация соединения с базой данных по умолчанию
|
||||||
|
$this->__get('redis');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Записать свойство
|
||||||
|
*
|
||||||
|
* @param string $name Название
|
||||||
|
* @param mixed $value Значение
|
||||||
|
*/
|
||||||
|
public function __set(string $name, mixed $value = null): void
|
||||||
|
{
|
||||||
|
match ($name) {
|
||||||
|
'arangodb' => (function () use ($value) {
|
||||||
|
if ($this->__isset('arangodb')) {
|
||||||
|
// Свойство уже было инициализировано
|
||||||
|
|
||||||
|
// Выброс исключения (неудача)
|
||||||
|
throw new exception('Запрещено реинициализировать соединение с базой данных ArangoDB ($this::$arangodb)', 500);
|
||||||
|
} else {
|
||||||
|
// Свойство ещё не было инициализировано
|
||||||
|
|
||||||
|
if ($value instanceof arangodb) {
|
||||||
|
// Передано подходящее значение
|
||||||
|
|
||||||
|
// Запись свойства (успех)
|
||||||
|
self::$arangodb = $value;
|
||||||
|
} else {
|
||||||
|
// Передано неподходящее значение
|
||||||
|
|
||||||
|
// Выброс исключения (неудача)
|
||||||
|
throw new exception('Соединение с базой данных ArangoDB ($this::$arangodb) должно быть инстанцией mirzaev\arangodb\connection', 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
'redis' => (function () use ($value) {
|
||||||
|
if ($this->__isset('redis')) {
|
||||||
|
// Свойство уже было инициализировано
|
||||||
|
|
||||||
|
// Выброс исключения (неудача)
|
||||||
|
throw new exception('Запрещено реинициализировать соединение с базой данных Redis ($this::$redis)', 500);
|
||||||
|
} else {
|
||||||
|
// Свойство ещё не было инициализировано
|
||||||
|
|
||||||
|
if ($value instanceof redis) {
|
||||||
|
// Передано подходящее значение
|
||||||
|
|
||||||
|
// Запись свойства (успех)
|
||||||
|
self::$redis = $value;
|
||||||
|
} else {
|
||||||
|
// Передано неподходящее значение
|
||||||
|
|
||||||
|
// Выброс исключения (неудача)
|
||||||
|
throw new exception('Соединение с базой данных Redis ($this::$arangodb) должно быть инстанцией redis', 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
|
||||||
|
default => parent::__set($name, $value)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Прочитать свойство
|
||||||
|
*
|
||||||
|
* @param string $name Название
|
||||||
|
*
|
||||||
|
* @return mixed Содержимое
|
||||||
|
*/
|
||||||
|
public function __get(string $name): mixed
|
||||||
|
{
|
||||||
|
return match ($name) {
|
||||||
|
'arangodb' => (function () {
|
||||||
|
try {
|
||||||
|
if (!$this->__isset('arangodb')) {
|
||||||
|
// Свойство не инициализировано
|
||||||
|
|
||||||
|
// Инициализация значения по умолчанию исходя из настроек
|
||||||
|
$this->__set('arangodb', new arangodb(require static::ARANGODB));
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::$arangodb;
|
||||||
|
} catch (exception) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
'redis' => (function () {
|
||||||
|
try {
|
||||||
|
if (!$this->__isset('redis')) {
|
||||||
|
// Свойство не инициализировано
|
||||||
|
|
||||||
|
// Инициализация настроек
|
||||||
|
[$connect, $authentication] = require static::REDIS;
|
||||||
|
|
||||||
|
// Инициализация инстанции redis
|
||||||
|
$redis = new redis;
|
||||||
|
|
||||||
|
// Подключение к базе данных redis
|
||||||
|
$redis->pconnect(...$connect);
|
||||||
|
|
||||||
|
// Аутентификация
|
||||||
|
$redis->auth($authentication);
|
||||||
|
|
||||||
|
// Выбор базы данных
|
||||||
|
$redis->select(1);
|
||||||
|
|
||||||
|
// Инициализация значения по умолчанию исходя из настроек
|
||||||
|
$this->__set('redis', $redis);
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::$redis;
|
||||||
|
} catch (redisexception) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
default => parent::__get($name)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверить свойство на инициализированность
|
||||||
|
*
|
||||||
|
* @param string $name Название
|
||||||
|
*/
|
||||||
|
public function __isset(string $name): bool
|
||||||
|
{
|
||||||
|
return match ($name) {
|
||||||
|
default => parent::__isset($name)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Удалить свойство
|
||||||
|
*
|
||||||
|
* @param string $name Название
|
||||||
|
*/
|
||||||
|
public function __unset(string $name): void
|
||||||
|
{
|
||||||
|
match ($name) {
|
||||||
|
default => parent::__isset($name)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Статический вызов
|
||||||
|
*
|
||||||
|
* @param string $name Название
|
||||||
|
* @param array $arguments Параметры
|
||||||
|
*/
|
||||||
|
public static function __callStatic(string $name, array $arguments): mixed
|
||||||
|
{
|
||||||
|
match ($name) {
|
||||||
|
'arangodb' => (new static)->__get('arangodb'),
|
||||||
|
'redis' => (new static)->__get('redis'),
|
||||||
|
default => throw new exception("Не найдено свойство или функция: $name", 500)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,12 +4,12 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace mirzaev\site\account\models;
|
namespace mirzaev\site\account\models;
|
||||||
|
|
||||||
// Файлы проекта
|
// Project files
|
||||||
use mirzaev\site\account\models\account;
|
use mirzaev\site\account\models\account,
|
||||||
|
mirzaev\site\account\models\traits\instance;
|
||||||
|
|
||||||
// Фреймворк ArangoDB
|
// Фреймворк ArangoDB
|
||||||
use mirzaev\arangodb\collection,
|
use mirzaev\arangodb\collection;
|
||||||
mirzaev\arangodb\document;
|
|
||||||
|
|
||||||
// Библиотека для ArangoDB
|
// Библиотека для ArangoDB
|
||||||
use ArangoDBClient\Document as _document;
|
use ArangoDBClient\Document as _document;
|
||||||
|
@ -25,36 +25,38 @@ use exception;
|
||||||
*/
|
*/
|
||||||
final class invite extends core
|
final class invite extends core
|
||||||
{
|
{
|
||||||
|
use instance;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Коллекция
|
* Коллекция
|
||||||
*/
|
*/
|
||||||
public const COLLECTION = 'invite';
|
final public const COLLECTION = 'invite';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Инстанция документа приглашения в базе данных
|
* Инстанция документа приглашения в базе данных
|
||||||
*/
|
*/
|
||||||
public ?_document $document;
|
protected readonly _document $document;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Прочитать
|
* Прочитать
|
||||||
*
|
*
|
||||||
* @param string $invite Ключ приглашения
|
* @param string $key Ключ приглашения
|
||||||
* @param array &$errors Реестр ошибок
|
* @param array &$errors Реестр ошибок
|
||||||
*
|
*
|
||||||
* @return ?self Инстанция приглашения, если оно найдено
|
* @return ?self Инстанция приглашения, если оно найдено
|
||||||
*/
|
*/
|
||||||
public static function read(string $invite, array &$errors = []): ?self
|
public static function read(string $key, array &$errors = []): ?self
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
if (collection::init(static::$db->session, self::COLLECTION)) {
|
if (collection::init(static::$arangodb->session, self::COLLECTION)) {
|
||||||
// Инициализирована коллекция
|
// Инициализирована коллекция
|
||||||
|
|
||||||
// Инициализация инстанции приглашения
|
// Инициализация инстанции приглашения
|
||||||
$instance = new self;
|
$invite = new self;
|
||||||
|
|
||||||
// Поиск приглашения
|
// Поиск приглашения
|
||||||
$instance->document = collection::search(
|
$instance = $invite->instance(collection::search(
|
||||||
static::$db->session,
|
static::$arangodb->session,
|
||||||
sprintf(
|
sprintf(
|
||||||
<<<AQL
|
<<<AQL
|
||||||
FOR d IN %s
|
FOR d IN %s
|
||||||
|
@ -62,13 +64,13 @@ final class invite extends core
|
||||||
RETURN d
|
RETURN d
|
||||||
AQL,
|
AQL,
|
||||||
self::COLLECTION,
|
self::COLLECTION,
|
||||||
$invite
|
$key
|
||||||
)
|
)
|
||||||
);
|
));
|
||||||
|
|
||||||
if ($instance->document instanceof _document) return $instance;
|
// Exit (success)
|
||||||
else throw new exception('Не удалось найти инстанцию приглашения в базе данных');
|
return $instance instanceof _document ? $invite : throw new exception('Не удалось найти инстанцию приглашения в базе данных');
|
||||||
} throw new exception('Не удалось инициализировать коллекцию');
|
} else throw new exception('Не удалось инициализировать коллекцию');
|
||||||
} catch (exception $e) {
|
} catch (exception $e) {
|
||||||
// Запись в реестр ошибок
|
// Запись в реестр ошибок
|
||||||
$errors[] = [
|
$errors[] = [
|
|
@ -0,0 +1,411 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace mirzaev\site\account\models;
|
||||||
|
|
||||||
|
// Файлы проекта
|
||||||
|
use mirzaev\site\account\models\account;
|
||||||
|
|
||||||
|
// Фреймворк ArangoDB
|
||||||
|
use mirzaev\arangodb\collection,
|
||||||
|
mirzaev\arangodb\document;
|
||||||
|
|
||||||
|
// Библиотека для ArangoDB
|
||||||
|
use ArangoDBClient\Document as _document;
|
||||||
|
|
||||||
|
// Встроенные библиотеки
|
||||||
|
use exception,
|
||||||
|
redis;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Модель сессий
|
||||||
|
*
|
||||||
|
* @package mirzaev\site\account\models
|
||||||
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
|
*/
|
||||||
|
final class session extends core
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Collection name in ArangoDB
|
||||||
|
*/
|
||||||
|
final public const COLLECTION = 'session';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Session data in JSON format
|
||||||
|
*
|
||||||
|
* Used as a cache in Redis
|
||||||
|
*/
|
||||||
|
protected readonly string $json;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Инстанция документа сессии в базе данных
|
||||||
|
*
|
||||||
|
* Used as a permanent storage in ArangoDB
|
||||||
|
*/
|
||||||
|
protected readonly _document $document;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Конструктор
|
||||||
|
*
|
||||||
|
* Инициализация сессии и запись в свойство $this->document
|
||||||
|
*
|
||||||
|
* @param ?string $hash Хеш сессии в базе данных
|
||||||
|
* @param ?int $expires Дата окончания работы сессии (используется при создании новой сессии)
|
||||||
|
* @param array &$errors Реестр ошибок
|
||||||
|
*
|
||||||
|
* @return static Инстанция сессии
|
||||||
|
*/
|
||||||
|
public function __construct(?string $hash = null, ?int $expires = null, array &$errors = [])
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if (collection::init(static::$arangodb->session, self::COLLECTION)) {
|
||||||
|
// Инициализирована коллекция
|
||||||
|
|
||||||
|
if (isset($hash)) {
|
||||||
|
// Received session hash
|
||||||
|
|
||||||
|
|
||||||
|
if ($session = collection::search($this::$arangodb->session, sprintf(
|
||||||
|
<<<AQL
|
||||||
|
FOR d IN %s
|
||||||
|
FILTER d.ip == '%s' && d.expires > %d && d.status == 'active'
|
||||||
|
RETURN d
|
||||||
|
AQL,
|
||||||
|
self::COLLECTION,
|
||||||
|
$_SERVER['REMOTE_ADDR'],
|
||||||
|
time()
|
||||||
|
))) {
|
||||||
|
// Найдена сессия по данным пользователя
|
||||||
|
|
||||||
|
// Запись в свойство
|
||||||
|
$this->document = $session;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Не найдена сессия
|
||||||
|
|
||||||
|
// Запись сессии в базу данных
|
||||||
|
$_id = document::write($this::$arangodb->session, self::COLLECTION, [
|
||||||
|
'status' => 'active',
|
||||||
|
'expires' => $expires ?? time() + 604800,
|
||||||
|
'ip' => $_SERVER['REMOTE_ADDR']
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($session = collection::search($this::$arangodb->session, sprintf(
|
||||||
|
<<<AQL
|
||||||
|
FOR d IN %s
|
||||||
|
FILTER d._id == '$_id' && d.expires > %d && d.status == 'active'
|
||||||
|
RETURN d
|
||||||
|
AQL,
|
||||||
|
self::COLLECTION,
|
||||||
|
time()
|
||||||
|
))) {
|
||||||
|
// Найдена только что созданная сессия
|
||||||
|
|
||||||
|
// Запись хеша
|
||||||
|
$session->hash = sodium_bin2hex(sodium_crypto_generichash($_id));
|
||||||
|
|
||||||
|
if (document::update($this::$arangodb->session, $session)) {
|
||||||
|
// Записано обновление
|
||||||
|
|
||||||
|
// Запись в свойство
|
||||||
|
$this->document = $session;
|
||||||
|
} 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()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function search(string $hash, array &$errors = []): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if (static::$redis->exist($hash) === 1) {
|
||||||
|
// Confirmed the existence of session data in Redis (cache)
|
||||||
|
|
||||||
|
// Search the session data in Redis
|
||||||
|
$json = static::$redis->get($hash);
|
||||||
|
|
||||||
|
// Session data not found? Then search in ArangoDB
|
||||||
|
if ($json === false) goto search_arangodb;
|
||||||
|
|
||||||
|
if ($json['expires'] > time() && $json['status'] === 'active') {
|
||||||
|
// The session is active
|
||||||
|
|
||||||
|
// Write the session data to the property
|
||||||
|
$this->json = $json;
|
||||||
|
|
||||||
|
// Exit (success)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Not confirmed the existence of session data in Redis (cache)
|
||||||
|
|
||||||
|
search_arangodb:
|
||||||
|
|
||||||
|
// Search the session data in ArangoDB
|
||||||
|
$_document = collection::search(static::$arangodb->session, sprintf(
|
||||||
|
<<<AQL
|
||||||
|
FOR d IN %s
|
||||||
|
FILTER d.hash == '$hash' && d.expires > %d && d.status == 'active'
|
||||||
|
RETURN d
|
||||||
|
AQL,
|
||||||
|
self::COLLECTION,
|
||||||
|
time()
|
||||||
|
));
|
||||||
|
|
||||||
|
if ($_document instanceof _document) {
|
||||||
|
// The session found and active
|
||||||
|
|
||||||
|
// Write the session data to the property
|
||||||
|
$this->document = $_document;
|
||||||
|
|
||||||
|
// Exit (success)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (exception $e) {
|
||||||
|
// Write to the errors registry
|
||||||
|
$errors[] = [
|
||||||
|
'text' => $e->getMessage(),
|
||||||
|
'file' => $e->getFile(),
|
||||||
|
'line' => $e->getLine(),
|
||||||
|
'stack' => $e->getTrace()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit (fail)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function __destruct()
|
||||||
|
{
|
||||||
|
// Закрыть сессию
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Инициализировать связь сессии с аккаунтом
|
||||||
|
*
|
||||||
|
* Ищет связь сессии с аккаунтом, если не находит, то создаёт её
|
||||||
|
*
|
||||||
|
* @param account $account Инстанция аккаунта
|
||||||
|
* @param array &$errors Реестр ошибок
|
||||||
|
*
|
||||||
|
* @return bool Связан аккаунт?
|
||||||
|
*/
|
||||||
|
public function connect(account $account, array &$errors = []): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
collection::init($this::$arangodb->session, self::COLLECTION)
|
||||||
|
&& collection::init($this::$arangodb->session, account::COLLECTION)
|
||||||
|
&& collection::init($this::$arangodb->session, self::COLLECTION . '_edge_' . account::COLLECTION, true)
|
||||||
|
) {
|
||||||
|
// Инициализирована коллекция
|
||||||
|
|
||||||
|
if (
|
||||||
|
collection::search($this::$arangodb->session, sprintf(
|
||||||
|
<<<AQL
|
||||||
|
FOR document IN %s
|
||||||
|
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($this::$arangodb->session, self::COLLECTION . '_edge_' . account::COLLECTION, [
|
||||||
|
'_from' => $this->document->getId(),
|
||||||
|
'_to' => $account->getId()
|
||||||
|
])
|
||||||
|
) {
|
||||||
|
// Найдено, либо создано ребро: session -> account
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} else throw new exception('Не удалось создать ребро: session -> account');
|
||||||
|
} else throw new exception('Не удалось инициализировать коллекцию');
|
||||||
|
} catch (exception $e) {
|
||||||
|
// Запись в реестр ошибок
|
||||||
|
$errors[] = [
|
||||||
|
'text' => $e->getMessage(),
|
||||||
|
'file' => $e->getFile(),
|
||||||
|
'line' => $e->getLine(),
|
||||||
|
'stack' => $e->getTrace()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Найти связанный аккаунт
|
||||||
|
*
|
||||||
|
* @param array &$errors Реестр ошибок
|
||||||
|
*
|
||||||
|
* @return ?account Инстанция аккаунта, если удалось найти
|
||||||
|
*/
|
||||||
|
public function account(array &$errors = []): ?account
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
collection::init($this::$arangodb->session, self::COLLECTION)
|
||||||
|
&& collection::init($this::$arangodb->session, account::COLLECTION)
|
||||||
|
&& collection::init($this::$arangodb->session, self::COLLECTION . '_edge_' . account::COLLECTION, true)
|
||||||
|
) {
|
||||||
|
// Инициализированы коллекции
|
||||||
|
|
||||||
|
// Инициализация инстанции аккаунта
|
||||||
|
$account = new account;
|
||||||
|
|
||||||
|
// Поиск инстанции аккаунта в базе данных
|
||||||
|
$instance = $account->instance(collection::search($this::$arangodb->session, sprintf(
|
||||||
|
<<<AQL
|
||||||
|
FOR document IN %s
|
||||||
|
LET edge = (
|
||||||
|
FOR edge IN %s
|
||||||
|
FILTER edge._from == '%s'
|
||||||
|
SORT edge._key DESC
|
||||||
|
LIMIT 1
|
||||||
|
RETURN edge
|
||||||
|
)
|
||||||
|
FILTER document._id == edge[0]._to
|
||||||
|
LIMIT 1
|
||||||
|
RETURN document
|
||||||
|
AQL,
|
||||||
|
account::COLLECTION,
|
||||||
|
self::COLLECTION . '_edge_' . account::COLLECTION,
|
||||||
|
$this->getId()
|
||||||
|
)));
|
||||||
|
|
||||||
|
// Возврат (успех)
|
||||||
|
return $instance instanceof _document ? $account : 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 array $data Данные для записи
|
||||||
|
* @param array &$errors Реестр ошибок
|
||||||
|
*
|
||||||
|
* @return bool Записаны данные в буфер сессии?
|
||||||
|
*/
|
||||||
|
public function write(array $data, array &$errors = []): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if (collection::init($this::$arangodb->session, self::COLLECTION)) {
|
||||||
|
// Инициализирована коллекция
|
||||||
|
|
||||||
|
// Проверка инициализированности инстанции документа из базы данных
|
||||||
|
if (!isset($this->document)) throw new exception('Не инициализирована инстанция документа из базы данных');
|
||||||
|
|
||||||
|
// Запись параметров в инстанцию документа из базы данных
|
||||||
|
$this->document->buffer = array_replace_recursive($this->document->buffer ?? [], $data);
|
||||||
|
|
||||||
|
// Запись в базу данных и возврат (успех)
|
||||||
|
return document::update($this::$arangodb->session, $this->document) ? 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Записать
|
||||||
|
*
|
||||||
|
* Записывает свойство в инстанцию документа сессии из базы данных
|
||||||
|
*
|
||||||
|
* @param string $name Название
|
||||||
|
* @param mixed $value Содержимое
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __set(string $name, mixed $value = null): void
|
||||||
|
{
|
||||||
|
$this->document->{$name} = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Прочитать
|
||||||
|
*
|
||||||
|
* Читает свойство из инстанции документа сессии из базы данных
|
||||||
|
*
|
||||||
|
* @param string $name Название
|
||||||
|
*
|
||||||
|
* @return mixed Данные свойства инстанции сессии или инстанции документа сессии из базы данных
|
||||||
|
*/
|
||||||
|
public function __get(string $name): mixed
|
||||||
|
{
|
||||||
|
return $this->document->{$name};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверить инициализированность
|
||||||
|
*
|
||||||
|
* Проверяет инициализированность свойства в инстанции документа сессии из базы данных
|
||||||
|
*
|
||||||
|
* @param string $name Название
|
||||||
|
*
|
||||||
|
* @return bool Свойство инициализировано?
|
||||||
|
*/
|
||||||
|
public function __isset(string $name): bool
|
||||||
|
{
|
||||||
|
return isset($this->document->{$name});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Удалить
|
||||||
|
*
|
||||||
|
* Деинициализировать свойство в инстанции документа сессии из базы данных
|
||||||
|
*
|
||||||
|
* @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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace mirzaev\site\account\models\traits;
|
||||||
|
|
||||||
|
// Library of ArangoDB
|
||||||
|
use ArangoDBClient\Document as _document;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trait with instance of document in database handler
|
||||||
|
*
|
||||||
|
* @package mirzaev\site\account\models\treits
|
||||||
|
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||||
|
*/
|
||||||
|
trait instance
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Инициализация инстанции документа в базе данных
|
||||||
|
*
|
||||||
|
* @param ?_document $document Инстанция документа в базе данных для записи
|
||||||
|
*
|
||||||
|
* @return ?_document Инстанция документа в базе данных, если инициализирована
|
||||||
|
*/
|
||||||
|
public function instance(?_document $document = null): ?_document
|
||||||
|
{
|
||||||
|
// Проверка инициализированности и возврат (успех)
|
||||||
|
if (isset($this->document)) return $this->document;
|
||||||
|
|
||||||
|
// Проверка инстанции документа в базе данных для записи и возврат (провал)
|
||||||
|
if ($document === null) return null;
|
||||||
|
|
||||||
|
// Запись в свойство и возврат (успех)
|
||||||
|
return $this->document = $document;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,277 @@
|
||||||
|
@keyframes glare {
|
||||||
|
2%,
|
||||||
|
100% {
|
||||||
|
left: 130%;
|
||||||
|
bottom: -200%;
|
||||||
|
width: 120px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
z-index: 1000;
|
||||||
|
top: 20%;
|
||||||
|
position: relative;
|
||||||
|
height: unset;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: unset;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.panel {
|
||||||
|
--display: flex;
|
||||||
|
z-index: 1000;
|
||||||
|
width: 400px;
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.column > section.panel {
|
||||||
|
position: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.panel.medium {
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.panel.small {
|
||||||
|
width: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.panel#mnemonic {
|
||||||
|
margin-left: -570px;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.panel#classic {
|
||||||
|
margin-left: 570px;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.panel > section.body > ul {
|
||||||
|
margin: 0 5%;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
list-style: square;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.panel > section.body > ul > li {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
word-break: break-word;
|
||||||
|
animation-duration: 0.35s;
|
||||||
|
animation-name: uprise;
|
||||||
|
animation-fill-mode: forwards;
|
||||||
|
animation-timing-function: cubic-bezier(0.47, 0, 0.74, 0.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: 0.35s;
|
||||||
|
animation-name: uprise;
|
||||||
|
animation-fill-mode: forwards;
|
||||||
|
animation-timing-function: cubic-bezier(0.47, 0, 0.74, 0.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 {
|
||||||
|
z-index: 1000;
|
||||||
|
height: 50px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: end;
|
||||||
|
animation-duration: 120s;
|
||||||
|
border-radius: 3px 3px 0 0;
|
||||||
|
background-color: var(--background-above);
|
||||||
|
}
|
||||||
|
|
||||||
|
section#profile > section.header {
|
||||||
|
margin-left: -50px;
|
||||||
|
height: 100px;
|
||||||
|
padding: 30px 0;
|
||||||
|
clip-path: url(#profile-header-mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
section#profile > section.header > img.avatar {
|
||||||
|
z-index: 1500;
|
||||||
|
left: 6px;
|
||||||
|
top: 36px;
|
||||||
|
width: 88px;
|
||||||
|
height: 88px;
|
||||||
|
position: absolute;
|
||||||
|
margin: auto;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
image-rendering: smooth;
|
||||||
|
box-shadow: 0px 0px 12px 0px rgba(0, 0, 0, 0.5);
|
||||||
|
-webkit-box-shadow: 0px 0px 12px 0px rgba(0, 0, 0, 0.5);
|
||||||
|
-moz-box-shadow: 0px 0px 12px 0px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
section#profile > section.header > img.avatar:hover {
|
||||||
|
left: 0;
|
||||||
|
top: 30px;
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.3);
|
||||||
|
-webkit-box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.3);
|
||||||
|
-moz-box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
section#profile > section.header > img.cover {
|
||||||
|
z-index: -5000;
|
||||||
|
left: -50px;
|
||||||
|
top: 0;
|
||||||
|
position: absolute;
|
||||||
|
width: calc(100% + 100px);
|
||||||
|
height: 100%;
|
||||||
|
object-position: 0px 30%;
|
||||||
|
object-fit: cover;
|
||||||
|
clip-path: polygon(
|
||||||
|
50px 0,
|
||||||
|
calc(100% - 50px) 0,
|
||||||
|
calc(100% - 50px) 100%,
|
||||||
|
50px 100%
|
||||||
|
);
|
||||||
|
border-radius: 0 0 3px 3px;
|
||||||
|
background: var(--background-above);
|
||||||
|
}
|
||||||
|
|
||||||
|
section#profile > section.header > div.glare {
|
||||||
|
z-index: 3000;
|
||||||
|
left: -30px;
|
||||||
|
top: -300px;
|
||||||
|
width: 30px;
|
||||||
|
height: 400%;
|
||||||
|
position: absolute;
|
||||||
|
rotate: 25deg;
|
||||||
|
opacity: 0.2;
|
||||||
|
filter: unset;
|
||||||
|
pointer-events: none;
|
||||||
|
animation-name: glare;
|
||||||
|
animation-duration: 32s;
|
||||||
|
animation-delay: 2s;
|
||||||
|
animation-fill-mode: forwards;
|
||||||
|
animation-timing-function: linear;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
section#profile > section.header > div {
|
||||||
|
animation-duration: 80s;
|
||||||
|
}
|
||||||
|
|
||||||
|
section#profile > section.header > a {
|
||||||
|
margin: auto;
|
||||||
|
width: 100%;
|
||||||
|
margin-left: 110px;
|
||||||
|
padding-bottom: 0.5ex;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow-x: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
font-size: 1.3em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--text-inverse);
|
||||||
|
text-shadow: 0 0 8px #00000080;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.panel > section.header > :is(h1, h2, h3) {
|
||||||
|
margin-bottom: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.panel > section.body {
|
||||||
|
padding: 20px 30px;
|
||||||
|
gap: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border-radius: 0 0 3px 3px;
|
||||||
|
background-color: var(--background-above);
|
||||||
|
}
|
||||||
|
|
||||||
|
section.panel > section.postscript {
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
section#profile > section.body > ul {
|
||||||
|
margin: unset;
|
||||||
|
margin-left: 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
section#profile > section.body ul ul {
|
||||||
|
padding-top: 1ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
section#profile > section.body ul li:not(:last-child) {
|
||||||
|
margin-bottom: 1ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
section#profile > section.body div.buttons {
|
||||||
|
margin-top: 10px;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
section#profile > section.body div.buttons > button {
|
||||||
|
padding: 1ex 2ex;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
background-color: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
section#profile > section.body div.buttons > button:hover {
|
||||||
|
color: var(--text-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
section#profile > section.body div.buttons > button:active {
|
||||||
|
color: var(--text-active);
|
||||||
|
transition: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
section#profile > section.body div.buttons > button:first-of-type {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: 5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
section#profile > section.body div.buttons > button:last-of-type {
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
section#profile > section.body div.buttons > button.accept {
|
||||||
|
padding: 1ex 5ex;
|
||||||
|
color: var(--text-inverse);
|
||||||
|
background-color: #63954d;
|
||||||
|
}
|
||||||
|
|
||||||
|
section#profile > section.body div.buttons > button.accept:hover {
|
||||||
|
color: var(--text-inverse-above);
|
||||||
|
background-color: #6fa259;
|
||||||
|
}
|
||||||
|
|
||||||
|
section#profile > section.body div.buttons > button.accept:active {
|
||||||
|
background-color: #63954d;
|
||||||
|
}
|
|
@ -0,0 +1,290 @@
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
--background-above-1: #fff;
|
||||||
|
--background-above: #fff6f6;
|
||||||
|
--background: #e8dada;
|
||||||
|
--background-below: #d7c5c5;
|
||||||
|
--background-inverse: #221e1e;
|
||||||
|
--background-inverse-dark: #120f0f;
|
||||||
|
--node-background-important: #c3eac3;
|
||||||
|
--node-background-completed: #b0c0b0;
|
||||||
|
--node-background: #bdb;
|
||||||
|
--connection: #b2b7b2;
|
||||||
|
--connection-completed: #d1d1d1;
|
||||||
|
--text: #151313;
|
||||||
|
--text-hover: #463e3e;
|
||||||
|
--text-active: #0e0e0e;
|
||||||
|
--text-inverse-above: #fff;
|
||||||
|
--text-inverse: #efefef;
|
||||||
|
--text-inverse-below: #d0d0d0;
|
||||||
|
--text-red: #f8a2a2;
|
||||||
|
--text-red-hover: #ffbcbc;
|
||||||
|
--text-red-active: #e69191;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--background-above-1: #322d2d;
|
||||||
|
--background-above: #2b2525;
|
||||||
|
--background: #221e1e;
|
||||||
|
--background-below: #121010;
|
||||||
|
--node-background: #221e1e;
|
||||||
|
--text: #e6e6e6;
|
||||||
|
--text-hover: #fff;
|
||||||
|
--text-active: #d0d0d0;
|
||||||
|
--text-inverse: #020202;
|
||||||
|
--red-light-1: #dc4343;
|
||||||
|
--red-light: #bf3737;
|
||||||
|
--red: #a43333;
|
||||||
|
--red-dark: #8d2a2a;
|
||||||
|
--input-error: #6c2424;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes page-background-gradient {
|
||||||
|
25% {
|
||||||
|
left: -350%;
|
||||||
|
top: 0%;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
left: 0%;
|
||||||
|
top: 0%;
|
||||||
|
}
|
||||||
|
|
||||||
|
75% {
|
||||||
|
left: 0%;
|
||||||
|
top: -350%;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
left: -350%;
|
||||||
|
top: -350%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--link: #3c76ff;
|
||||||
|
--link-hover: #6594ff;
|
||||||
|
--link-active: #3064dd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unselectable {
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-khtml-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden:not(.animation) {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
text-decoration: none;
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--text);
|
||||||
|
font-family: Fira, sans-serif;
|
||||||
|
transition: 0.1s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre,
|
||||||
|
code {
|
||||||
|
font-family: Hack, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
input[type="submit"] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--link);
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: var(--link-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
a:active {
|
||||||
|
color: var(--link-active);
|
||||||
|
transition: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
position: relative;
|
||||||
|
height: 26px;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label > i:first-child {
|
||||||
|
left: 8px;
|
||||||
|
top: calc((26px - var(--height)) / 2);
|
||||||
|
position: absolute !important;
|
||||||
|
margin: auto;
|
||||||
|
color: #8c7d7d;
|
||||||
|
}
|
||||||
|
|
||||||
|
label * {
|
||||||
|
/* color: var(--text-inverse); */
|
||||||
|
}
|
||||||
|
|
||||||
|
label > input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 8px;
|
||||||
|
background-color: var(--background-above-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
label > input + button {
|
||||||
|
background-color: var(--red);
|
||||||
|
}
|
||||||
|
|
||||||
|
i + input {
|
||||||
|
padding-left: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.error {
|
||||||
|
animation-duration: 1s;
|
||||||
|
animation-name: input-error;
|
||||||
|
animation-fill-mode: forwards;
|
||||||
|
animation-timing-function: ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.header > h1 {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
line-height: 1.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.header > :is(h2, h3) {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
line-height: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
background-color: var(--background);
|
||||||
|
}
|
||||||
|
|
||||||
|
body > div.background {
|
||||||
|
z-index: -50000;
|
||||||
|
left: -350%;
|
||||||
|
top: -350%;
|
||||||
|
width: 500%;
|
||||||
|
height: 500%;
|
||||||
|
position: absolute;
|
||||||
|
filter: blur(200px);
|
||||||
|
animation-duration: 15s;
|
||||||
|
animation-name: page-background-gradient;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
animation-timing-function: linear;
|
||||||
|
background-image: radial-gradient(
|
||||||
|
circle,
|
||||||
|
var(--background-above) 0%,
|
||||||
|
rgba(0, 0, 0, 0) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
aside {
|
||||||
|
z-index: 500;
|
||||||
|
grid-column: 1/ 4;
|
||||||
|
grid-row: 2;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
z-index: 5000;
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
header > menu {
|
||||||
|
margin: unset;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-grow: 1;
|
||||||
|
background-color: var(--background-light-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
header > menu a {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
header > menu a:last-child {
|
||||||
|
margin-bottom: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
header > menu a svg {
|
||||||
|
margin-right: 8px;
|
||||||
|
height: 1.2rem;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
header > menu a:hover svg {
|
||||||
|
margin-left: -5px;
|
||||||
|
margin-right: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header > menu a svg path {
|
||||||
|
fill: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
header > section {
|
||||||
|
background-color: var(--background-light-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
header :is(button, a[type="button"], input[type="submit"]) {
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: var(--red);
|
||||||
|
transition: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
header :is(button, a[type="button"], input[type="submit"]) {
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
header :is(button, a[type="button"], input[type="submit"]):hover {
|
||||||
|
background-color: var(--red-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
header :is(button, a[type="button"], input[type="submit"]):active {
|
||||||
|
background-color: var(--red-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
header > nav {
|
||||||
|
margin-top: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
z-index: 1000;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
z-index: 3000;
|
||||||
|
position: absolute;
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue