generated from mirzaev/pot
DUMB MOVING STARTED (DEVELOPING)
This commit is contained in:
parent
87bd15640a
commit
f70f2c86ea
|
@ -1,3 +1,2 @@
|
|||
# site-account
|
||||
|
||||
Site for intersite authentication
|
||||
# accounts
|
||||
Accounts system site of the Svoboda organization
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
{
|
||||
"name": "mirzaev/site-account",
|
||||
"description": "API for intersite authentication",
|
||||
"readme": "README.md",
|
||||
"keywords": [
|
||||
"site",
|
||||
"api",
|
||||
"authentication"
|
||||
],
|
||||
"name": "svoboda/accounts",
|
||||
"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",
|
||||
"homepage": "https://git.svoboda.works/svoboda/accounts",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Arsen Mirzaev Tatyano-Muradovich",
|
||||
|
@ -20,23 +21,19 @@
|
|||
],
|
||||
"support": {
|
||||
"email": "arsen@mirzaev.sexy",
|
||||
"wiki": "https://git.mirzaev.sexy/mirzaev/site-account/wiki",
|
||||
"issues": "https://git.mirzaev.sexy/mirzaev/site-account/issues"
|
||||
"wiki": "https://git.svoboda.works/svoboda/accounts/wiki",
|
||||
"issues": "https://git.svoboda.works/svoboda/accounts/issues"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"type": "funding",
|
||||
"url": "https://fund.mirzaev.sexy"
|
||||
"url": "https://fund.svoboda.works"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "~8.2",
|
||||
"ext-sodium": "~8.2",
|
||||
"mirzaev/minimal": "^2.0.x-dev",
|
||||
"mirzaev/accounts": "~1.2.x-dev",
|
||||
"mirzaev/arangodb": "^1.0.0",
|
||||
"mirzaev/vk": "^5.0",
|
||||
"triagens/arangodb": "~3.9.x-dev",
|
||||
"php": "~8.4",
|
||||
"ext-sodium": "~8.4",
|
||||
"mirzaev/minimal": "^3.4.0",
|
||||
"twig/twig": "^3.4",
|
||||
"guzzlehttp/guzzle": "^7.5",
|
||||
"scripturadesign/markov": "^2.0"
|
||||
|
@ -46,12 +43,15 @@
|
|||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"mirzaev\\site\\account\\": "mirzaev/site/account/system"
|
||||
"svoboda\\accounts\\": "svoboda/accounts/system"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"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
|
||||
use mirzaev\minimal\controller;
|
||||
|
||||
// Встроенные библиотеки
|
||||
use exception;
|
||||
|
||||
/**
|
||||
* Ядро контроллеров
|
||||
*
|
||||
|
@ -24,25 +21,25 @@ use exception;
|
|||
*/
|
||||
class core extends controller
|
||||
{
|
||||
/**
|
||||
* Постфикс
|
||||
*/
|
||||
final public const POSTFIX = '';
|
||||
|
||||
/**
|
||||
* Инстанция сессии
|
||||
*/
|
||||
public session $session;
|
||||
protected readonly session $session;
|
||||
|
||||
/**
|
||||
* Инстанция аккаунта
|
||||
*/
|
||||
public ?account $account;
|
||||
|
||||
/**
|
||||
* Постфикс
|
||||
*/
|
||||
public string $postfix = '';
|
||||
protected readonly ?account $account;
|
||||
|
||||
/**
|
||||
* Реестр ошибок
|
||||
*/
|
||||
public array $errors = [
|
||||
protected array $errors = [
|
||||
'session' => [],
|
||||
'account' => []
|
||||
];
|
||||
|
@ -63,7 +60,7 @@ class core extends controller
|
|||
new models();
|
||||
|
||||
// Инициализация даты до которой будет активна сессия
|
||||
$expires = time() + 604800;
|
||||
$expires = strtotime( '+1 week' );
|
||||
|
||||
// Инициализация значения по умолчанию
|
||||
$_COOKIE["session"] ??= null;
|
|
@ -10,6 +10,9 @@ use mirzaev\site\account\controllers\core,
|
|||
mirzaev\site\account\models\invite,
|
||||
mirzaev\site\account\models\account;
|
||||
|
||||
// Библиотека для ArangoDB
|
||||
use ArangoDBClient\Document as _document;
|
||||
|
||||
// Встроенные библиотеки
|
||||
use exception;
|
||||
|
||||
|
@ -53,19 +56,31 @@ final class session extends core
|
|||
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 ($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']);
|
||||
|
||||
// Генерация ответа по запрашиваемым параметрам
|
||||
foreach ($return as $parameter) match ($parameter) {
|
||||
'exist' => $buffer['exist'] = isset($account->document),
|
||||
'account' => (function () use ($parameters, &$buffer) {
|
||||
'exist' => $buffer['exist'] = isset($account) && $account->instance() instanceof _document,
|
||||
'account' => (function () use ($parameters, $remember, &$buffer) {
|
||||
// Запись в буфер сессии
|
||||
if (isset($parameters['remember']) && $parameters['remember'] === '1')
|
||||
$this->session->write(['entry' => ['login' => $parameters['login']]], $this->errors);
|
||||
if ($remember) $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,
|
||||
default => throw new exception("Параметр не найден: $parameter")
|
||||
|
@ -102,7 +117,7 @@ final class session extends core
|
|||
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']]]);
|
||||
}
|
||||
|
||||
|
@ -135,13 +150,13 @@ final class session extends core
|
|||
// Генерация ответа по запрашиваемым параметрам
|
||||
foreach ($return as $parameter) match ($parameter) {
|
||||
'verify' => $buffer['verify'] = true,
|
||||
'account' => (function() use ($parameters, &$buffer) {
|
||||
'account' => (function () use ($parameters, &$buffer) {
|
||||
// Запись в буфер сессии
|
||||
if (isset($parameters['remember']) && $parameters['remember'] === '1')
|
||||
$this->session->write(['entry' => ['password' => $parameters['password']]], $this->errors);
|
||||
|
||||
// Поиск аккаунта и запись в буфер вывода
|
||||
$buffer['account'] = isset((new account($this->session, authenticate: true, register: true, errors: $this->errors))->document);
|
||||
$buffer['account'] = (new account($this->session, authenticate: true, register: true, errors: $this->errors))?->instance() instanceof _document;
|
||||
})(),
|
||||
'errors' => null,
|
||||
default => throw new exception("Параметр не найден: $parameter")
|
||||
|
@ -216,7 +231,7 @@ final class session extends core
|
|||
|
||||
// Генерация ответа по запрашиваемым параметрам
|
||||
foreach ($return as $parameter) match ($parameter) {
|
||||
'exist' => $buffer['exist'] = isset($invite->document),
|
||||
'exist' => $buffer['exist'] = isset($invite) && $invite->instance() instanceof _document,
|
||||
// from временное решение пока не будет разработана система сессий
|
||||
'from' => $buffer['from'] = ['login' => 'mirzaev'] ?? $invite->from(),
|
||||
'account' => (function () use ($parameters, &$buffer) {
|
||||
|
@ -225,7 +240,7 @@ final class session extends core
|
|||
$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,
|
||||
default => throw new exception("Параметр не найден: $parameter")
|
|
@ -4,6 +4,9 @@ declare(strict_types=1);
|
|||
|
||||
namespace mirzaev\site\account\models;
|
||||
|
||||
// Project files
|
||||
use mirzaev\site\account\models\traits\instance;
|
||||
|
||||
// Фреймворк ArangoDB
|
||||
use mirzaev\arangodb\collection,
|
||||
mirzaev\arangodb\document;
|
||||
|
@ -22,15 +25,17 @@ use exception;
|
|||
*/
|
||||
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;
|
||||
} else throw new exception('Неправильный пароль');
|
||||
} throw new exception('Неправильный пароль');
|
||||
}
|
||||
throw new exception('Неправильный пароль');
|
||||
} 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
|
||||
{
|
||||
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(
|
||||
static::$db->session,
|
||||
$instance = $account->instance(collection::search(
|
||||
static::$arangodb->session,
|
||||
sprintf(
|
||||
<<<'AQL'
|
||||
FOR d IN %s
|
||||
|
@ -204,10 +209,10 @@ final class account extends core
|
|||
self::COLLECTION,
|
||||
$login
|
||||
)
|
||||
);
|
||||
));
|
||||
|
||||
if ($instance->document instanceof _document) return $instance;
|
||||
else throw new exception('Не удалось найти инстанцию аккаунта в базе данных');
|
||||
// Возврат (успех)
|
||||
return $instance instanceof _document ? $account : throw new exception('Не удалось найти инстанцию аккаунта в базе данных');
|
||||
} else throw new exception('Не удалось инициализировать коллекцию');
|
||||
} catch (exception $e) {
|
||||
// Запись в реестр ошибок
|
||||
|
@ -233,8 +238,8 @@ final class account extends core
|
|||
public static function create(array $data = [], array &$errors = []): bool
|
||||
{
|
||||
try {
|
||||
if (collection::init(static::$db->session, self::COLLECTION))
|
||||
if (document::write(static::$db->session, self::COLLECTION, $data)) return true;
|
||||
if (collection::init(static::$arangodb->session, self::COLLECTION))
|
||||
if (document::write(static::$arangodb->session, self::COLLECTION, $data)) return true;
|
||||
else throw new exception('Не удалось создать аккаунт');
|
||||
else throw new exception('Не удалось инициализировать коллекцию');
|
||||
} 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;
|
||||
|
||||
// Файлы проекта
|
||||
use mirzaev\site\account\models\account;
|
||||
// Project files
|
||||
use mirzaev\site\account\models\account,
|
||||
mirzaev\site\account\models\traits\instance;
|
||||
|
||||
// Фреймворк ArangoDB
|
||||
use mirzaev\arangodb\collection,
|
||||
mirzaev\arangodb\document;
|
||||
use mirzaev\arangodb\collection;
|
||||
|
||||
// Библиотека для ArangoDB
|
||||
use ArangoDBClient\Document as _document;
|
||||
|
@ -25,36 +25,38 @@ use exception;
|
|||
*/
|
||||
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 Реестр ошибок
|
||||
*
|
||||
* @return ?self Инстанция приглашения, если оно найдено
|
||||
*/
|
||||
public static function read(string $invite, array &$errors = []): ?self
|
||||
public static function read(string $key, array &$errors = []): ?self
|
||||
{
|
||||
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(
|
||||
static::$db->session,
|
||||
$instance = $invite->instance(collection::search(
|
||||
static::$arangodb->session,
|
||||
sprintf(
|
||||
<<<AQL
|
||||
FOR d IN %s
|
||||
|
@ -62,13 +64,13 @@ final class invite extends core
|
|||
RETURN d
|
||||
AQL,
|
||||
self::COLLECTION,
|
||||
$invite
|
||||
$key
|
||||
)
|
||||
);
|
||||
));
|
||||
|
||||
if ($instance->document instanceof _document) return $instance;
|
||||
else throw new exception('Не удалось найти инстанцию приглашения в базе данных');
|
||||
} throw new exception('Не удалось инициализировать коллекцию');
|
||||
// Exit (success)
|
||||
return $instance instanceof _document ? $invite : throw new exception('Не удалось найти инстанцию приглашения в базе данных');
|
||||
} else throw new exception('Не удалось инициализировать коллекцию');
|
||||
} catch (exception $e) {
|
||||
// Запись в реестр ошибок
|
||||
$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