DUMB MOVING STARTED (DEVELOPING)

This commit is contained in:
Arsen Mirzaev Tatyano-Muradovich 2025-01-12 21:21:13 +07:00
parent 87bd15640a
commit f70f2c86ea
180 changed files with 2035 additions and 1993 deletions

View File

@ -1,3 +1,2 @@
# site-account
Site for intersite authentication
# accounts
Accounts system site of the Svoboda organization

View File

@ -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"
}
}

994
composer.lock generated

File diff suppressed because it is too large Load Diff

1
damper.mjs Submodule

@ -0,0 +1 @@
Subproject commit 68589e968cbc043f35c2948a9c90293b6f5f9cb9

View File

@ -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";
}
}

1
graph.mjs Submodule

@ -0,0 +1 @@
Subproject commit 0300f3376550b9d0a07d1c41db88452af67e366b

1
hotline.mjs Submodule

@ -0,0 +1 @@
Subproject commit 81aca4001629e8f3cab8a849c1e892dbac74c88a

37
install.sh Normal file
View File

@ -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

View File

@ -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)
};
}
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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();

View File

@ -1 +0,0 @@
arangodb.php

16
psalm.xml Normal file
View File

@ -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>

View File

@ -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;

View File

@ -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")

View File

@ -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) {

View File

@ -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)
};
}
}

View File

@ -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[] = [

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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