generated from mirzaev/pot
	DUMB MOVING STARTED (DEVELOPING)
This commit is contained in:
		| @@ -1,3 +1,2 @@ | |||||||
| # site-account | # accounts | ||||||
|  | Accounts system site of the Svoboda organization | ||||||
| Site for intersite authentication |  | ||||||
|   | |||||||
| @@ -1,15 +1,16 @@ | |||||||
| { | { | ||||||
|   "name": "mirzaev/site-account", |   "name": "svoboda/accounts", | ||||||
|   "description": "API for intersite authentication", |  | ||||||
|   "readme": "README.md", |  | ||||||
|   "keywords": [ |  | ||||||
|     "site", |  | ||||||
|     "api", |  | ||||||
|     "authentication" |  | ||||||
|   ], |  | ||||||
|   "type": "site", |   "type": "site", | ||||||
|   "homepage": "https://git.mirzaev.sexy/mirzaev/site-account", |   "description": "Accounts system site of the Svoboda organization", | ||||||
|  |   "keywords": [ | ||||||
|  |     "accounts", | ||||||
|  |     "svoboda", | ||||||
|  |     "anarchism", | ||||||
|  | 		"minimal" | ||||||
|  |   ], | ||||||
|  |   "readme": "README.md", | ||||||
|   "license": "WTFPL", |   "license": "WTFPL", | ||||||
|  |   "homepage": "https://git.svoboda.works/svoboda/accounts", | ||||||
|   "authors": [ |   "authors": [ | ||||||
|     { |     { | ||||||
|       "name": "Arsen Mirzaev Tatyano-Muradovich", |       "name": "Arsen Mirzaev Tatyano-Muradovich", | ||||||
| @@ -20,23 +21,19 @@ | |||||||
|   ], |   ], | ||||||
|   "support": { |   "support": { | ||||||
|     "email": "arsen@mirzaev.sexy", |     "email": "arsen@mirzaev.sexy", | ||||||
|     "wiki": "https://git.mirzaev.sexy/mirzaev/site-account/wiki", |     "wiki": "https://git.svoboda.works/svoboda/accounts/wiki", | ||||||
|     "issues": "https://git.mirzaev.sexy/mirzaev/site-account/issues" |     "issues": "https://git.svoboda.works/svoboda/accounts/issues" | ||||||
|   }, |   }, | ||||||
|   "funding": [ |   "funding": [ | ||||||
|     { |     { | ||||||
|       "type": "funding", |       "type": "funding", | ||||||
|       "url": "https://fund.mirzaev.sexy" |       "url": "https://fund.svoboda.works" | ||||||
|     } |     } | ||||||
|   ], |   ], | ||||||
|   "require": { |   "require": { | ||||||
|     "php": "~8.2", |     "php": "~8.4", | ||||||
|     "ext-sodium": "~8.2", |     "ext-sodium": "~8.4", | ||||||
|     "mirzaev/minimal": "^2.0.x-dev", |     "mirzaev/minimal": "^3.4.0", | ||||||
|     "mirzaev/accounts": "~1.2.x-dev", |  | ||||||
|     "mirzaev/arangodb": "^1.0.0", |  | ||||||
|     "mirzaev/vk": "^5.0", |  | ||||||
|     "triagens/arangodb": "~3.9.x-dev", |  | ||||||
|     "twig/twig": "^3.4", |     "twig/twig": "^3.4", | ||||||
|     "guzzlehttp/guzzle": "^7.5", |     "guzzlehttp/guzzle": "^7.5", | ||||||
|     "scripturadesign/markov": "^2.0" |     "scripturadesign/markov": "^2.0" | ||||||
| @@ -46,12 +43,15 @@ | |||||||
|   }, |   }, | ||||||
|   "autoload": { |   "autoload": { | ||||||
|     "psr-4": { |     "psr-4": { | ||||||
|       "mirzaev\\site\\account\\": "mirzaev/site/account/system" |       "svoboda\\accounts\\": "svoboda/accounts/system" | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   "autoload-dev": { |   "autoload-dev": { | ||||||
|     "psr-4": { |     "psr-4": { | ||||||
|       "mirzaev\\site\\account\\tests\\": "mirzaev/site/account/tests" |       "svoboda\\accounts\\tests\\": "svoboda/accounts/tests" | ||||||
|     } |     } | ||||||
|  |   }, | ||||||
|  | 	"scripts": { | ||||||
|  | 		"pre-update-cmd": "./install.sh" | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										994
									
								
								composer.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										994
									
								
								composer.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										1
									
								
								damper.mjs
									
									
									
									
									
										Submodule
									
								
							
							
								
								
								
								
								
							
						
						
									
										1
									
								
								damper.mjs
									
									
									
									
									
										Submodule
									
								
							 Submodule damper.mjs added at 68589e968c
									
								
							
							
								
								
									
										53
									
								
								examples/nginx/account.svoboda.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								examples/nginx/account.svoboda.conf
									
									
									
									
									
										Normal 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
									
								
							
							
								
								
								
								
								
							
						
						
									
										1
									
								
								graph.mjs
									
									
									
									
									
										Submodule
									
								
							 Submodule graph.mjs added at 0300f33765
									
								
							
							
								
								
									
										1
									
								
								hotline.mjs
									
									
									
									
									
										Submodule
									
								
							
							
								
								
								
								
								
							
						
						
									
										1
									
								
								hotline.mjs
									
									
									
									
									
										Submodule
									
								
							 Submodule hotline.mjs added at 81aca40016
									
								
							
							
								
								
									
										37
									
								
								install.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								install.sh
									
									
									
									
									
										Normal 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 | ||||||
| @@ -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 |  | ||||||
							
								
								
									
										16
									
								
								psalm.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								psalm.xml
									
									
									
									
									
										Normal 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> | ||||||
| @@ -13,9 +13,6 @@ use mirzaev\site\account\views\templater, | |||||||
| // Фреймворк PHP
 | // Фреймворк PHP
 | ||||||
| use mirzaev\minimal\controller; | use mirzaev\minimal\controller; | ||||||
| 
 | 
 | ||||||
| // Встроенные библиотеки
 |  | ||||||
| use exception; |  | ||||||
| 
 |  | ||||||
| /** | /** | ||||||
|  * Ядро контроллеров |  * Ядро контроллеров | ||||||
|  * |  * | ||||||
| @@ -24,25 +21,25 @@ use exception; | |||||||
|  */ |  */ | ||||||
| class core extends controller | class core extends controller | ||||||
| { | { | ||||||
|  |   /** | ||||||
|  |    * Постфикс | ||||||
|  |    */ | ||||||
|  |   final public const POSTFIX = ''; | ||||||
|  | 
 | ||||||
|   /** |   /** | ||||||
|    * Инстанция сессии |    * Инстанция сессии | ||||||
|    */ |    */ | ||||||
|   public session $session; |   protected readonly session $session; | ||||||
| 
 | 
 | ||||||
|   /** |   /** | ||||||
|    * Инстанция аккаунта |    * Инстанция аккаунта | ||||||
|    */ |    */ | ||||||
|   public ?account $account; |   protected readonly ?account $account; | ||||||
| 
 |  | ||||||
|   /** |  | ||||||
|    * Постфикс |  | ||||||
|    */ |  | ||||||
|   public string $postfix = ''; |  | ||||||
| 
 | 
 | ||||||
|   /** |   /** | ||||||
|    * Реестр ошибок |    * Реестр ошибок | ||||||
|    */ |    */ | ||||||
|   public array $errors = [ |   protected array $errors = [ | ||||||
|     'session' => [], |     'session' => [], | ||||||
|     'account' => [] |     'account' => [] | ||||||
|   ]; |   ]; | ||||||
| @@ -63,7 +60,7 @@ class core extends controller | |||||||
|       new models(); |       new models(); | ||||||
| 
 | 
 | ||||||
|       // Инициализация даты до которой будет активна сессия
 |       // Инициализация даты до которой будет активна сессия
 | ||||||
|       $expires = time() + 604800; |       $expires = strtotime( '+1 week' ); | ||||||
| 
 | 
 | ||||||
|       // Инициализация значения по умолчанию
 |       // Инициализация значения по умолчанию
 | ||||||
|       $_COOKIE["session"] ??= null; |       $_COOKIE["session"] ??= null; | ||||||
| @@ -10,6 +10,9 @@ use mirzaev\site\account\controllers\core, | |||||||
|   mirzaev\site\account\models\invite, |   mirzaev\site\account\models\invite, | ||||||
|   mirzaev\site\account\models\account; |   mirzaev\site\account\models\account; | ||||||
| 
 | 
 | ||||||
|  | // Библиотека для ArangoDB
 | ||||||
|  | use ArangoDBClient\Document as _document; | ||||||
|  | 
 | ||||||
| // Встроенные библиотеки
 | // Встроенные библиотеки
 | ||||||
| use exception; | use exception; | ||||||
| 
 | 
 | ||||||
| @@ -53,19 +56,31 @@ final class session extends core | |||||||
|       if ($length > 100) throw new exception('Входной псевдоним не может быть длиннее 100 символов'); |       if ($length > 100) throw new exception('Входной псевдоним не может быть длиннее 100 символов'); | ||||||
|       if (preg_match_all('/[^\w\s\r\n\t\0]+/u', $parameters['login'], $matches) > 0) throw new exception('Нельзя использовать символы: ' . implode(', ', ...$matches)); |       if (preg_match_all('/[^\w\s\r\n\t\0]+/u', $parameters['login'], $matches) > 0) throw new exception('Нельзя использовать символы: ' . implode(', ', ...$matches)); | ||||||
| 
 | 
 | ||||||
|  |       if ($remember = isset($parameters['remember']) && $parameters['remember'] === '1') { | ||||||
|  |         // Запрошено запоминание
 | ||||||
|  | 
 | ||||||
|  |         // Запись в cookie
 | ||||||
|  |         setcookie('entry_login', $parameters['login'], [ | ||||||
|  |           'expires' => strtotime('+30 minutes'), | ||||||
|  |           'path' => '/', | ||||||
|  |           'secure' => true, | ||||||
|  |           'httponly' => true, | ||||||
|  |           'samesite' => 'strict' | ||||||
|  |         ]); | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|       // Поиск аккаунта
 |       // Поиск аккаунта
 | ||||||
|       $account = account::login($parameters['login']); |       $account = account::login($parameters['login']); | ||||||
| 
 | 
 | ||||||
|       // Генерация ответа по запрашиваемым параметрам
 |       // Генерация ответа по запрашиваемым параметрам
 | ||||||
|       foreach ($return as $parameter) match ($parameter) { |       foreach ($return as $parameter) match ($parameter) { | ||||||
|         'exist' => $buffer['exist'] = isset($account->document), |         'exist' => $buffer['exist'] = isset($account) && $account->instance() instanceof _document, | ||||||
|         'account' => (function () use ($parameters, &$buffer) { |         'account' => (function () use ($parameters, $remember, &$buffer) { | ||||||
|           // Запись в буфер сессии
 |           // Запись в буфер сессии
 | ||||||
|           if (isset($parameters['remember']) && $parameters['remember'] === '1') |           if ($remember) $this->session->write(['entry' => ['login' => $parameters['login']]], $this->errors); | ||||||
|             $this->session->write(['entry' => ['login' => $parameters['login']]], $this->errors); |  | ||||||
| 
 | 
 | ||||||
|           // Поиск аккаунта и запись в буфер вывода
 |           // Поиск аккаунта и запись в буфер вывода
 | ||||||
|           $buffer['account'] = isset((new account($this->session, authenticate: true, errors: $this->errors))->document); |           $buffer['account'] = (new account($this->session, authenticate: true, errors: $this->errors))?->instance() instanceof _document; | ||||||
|         })(), |         })(), | ||||||
|         'errors' => null, |         'errors' => null, | ||||||
|         default => throw new exception("Параметр не найден: $parameter") |         default => throw new exception("Параметр не найден: $parameter") | ||||||
| @@ -102,7 +117,7 @@ final class session extends core | |||||||
|     flush(); |     flush(); | ||||||
| 
 | 
 | ||||||
|     // Запись в буфер сессии
 |     // Запись в буфер сессии
 | ||||||
|     if (!in_array('account', $return, true) && isset($parameters['remember']) && $parameters['remember'] === '1') |     if (!in_array('account', $return, true) && ($remember ?? false)) | ||||||
|       $this->session->write(['entry' => ['login' => $parameters['login']]]); |       $this->session->write(['entry' => ['login' => $parameters['login']]]); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @@ -135,13 +150,13 @@ final class session extends core | |||||||
|       // Генерация ответа по запрашиваемым параметрам
 |       // Генерация ответа по запрашиваемым параметрам
 | ||||||
|       foreach ($return as $parameter) match ($parameter) { |       foreach ($return as $parameter) match ($parameter) { | ||||||
|         'verify' => $buffer['verify'] = true, |         'verify' => $buffer['verify'] = true, | ||||||
|         'account' => (function() use ($parameters, &$buffer) { |         'account' => (function () use ($parameters, &$buffer) { | ||||||
|           // Запись в буфер сессии
 |           // Запись в буфер сессии
 | ||||||
|           if (isset($parameters['remember']) && $parameters['remember'] === '1') |           if (isset($parameters['remember']) && $parameters['remember'] === '1') | ||||||
|             $this->session->write(['entry' => ['password' => $parameters['password']]], $this->errors); |             $this->session->write(['entry' => ['password' => $parameters['password']]], $this->errors); | ||||||
| 
 | 
 | ||||||
|           // Поиск аккаунта и запись в буфер вывода
 |           // Поиск аккаунта и запись в буфер вывода
 | ||||||
|           $buffer['account'] = isset((new account($this->session, authenticate: true, register: true, errors: $this->errors))->document); |           $buffer['account'] = (new account($this->session, authenticate: true, register: true, errors: $this->errors))?->instance() instanceof _document; | ||||||
|         })(), |         })(), | ||||||
|         'errors' => null, |         'errors' => null, | ||||||
|         default => throw new exception("Параметр не найден: $parameter") |         default => throw new exception("Параметр не найден: $parameter") | ||||||
| @@ -216,7 +231,7 @@ final class session extends core | |||||||
| 
 | 
 | ||||||
|       // Генерация ответа по запрашиваемым параметрам
 |       // Генерация ответа по запрашиваемым параметрам
 | ||||||
|       foreach ($return as $parameter) match ($parameter) { |       foreach ($return as $parameter) match ($parameter) { | ||||||
|         'exist' => $buffer['exist'] = isset($invite->document), |         'exist' => $buffer['exist'] = isset($invite) && $invite->instance() instanceof _document, | ||||||
|         // from временное решение пока не будет разработана система сессий
 |         // from временное решение пока не будет разработана система сессий
 | ||||||
|         'from' => $buffer['from'] = ['login' => 'mirzaev'] ?? $invite->from(), |         'from' => $buffer['from'] = ['login' => 'mirzaev'] ?? $invite->from(), | ||||||
|         'account' => (function () use ($parameters, &$buffer) { |         'account' => (function () use ($parameters, &$buffer) { | ||||||
| @@ -225,7 +240,7 @@ final class session extends core | |||||||
|             $this->session->write(['entry' => ['invite' => $parameters['invite']]], $this->errors); |             $this->session->write(['entry' => ['invite' => $parameters['invite']]], $this->errors); | ||||||
| 
 | 
 | ||||||
|           // Поиск аккаунта и запись в буфер вывода
 |           // Поиск аккаунта и запись в буфер вывода
 | ||||||
|           $buffer['account'] = isset((new account($this->session, authenticate: true, errors: $this->errors))->document); |           $buffer['account'] = (new account($this->session, authenticate: true, errors: $this->errors))?->instance() instanceof _document; | ||||||
|         })(), |         })(), | ||||||
|         'errors' => null, |         'errors' => null, | ||||||
|         default => throw new exception("Параметр не найден: $parameter") |         default => throw new exception("Параметр не найден: $parameter") | ||||||
| @@ -4,6 +4,9 @@ declare(strict_types=1); | |||||||
| 
 | 
 | ||||||
| namespace mirzaev\site\account\models; | namespace mirzaev\site\account\models; | ||||||
| 
 | 
 | ||||||
|  | // Project files
 | ||||||
|  | use mirzaev\site\account\models\traits\instance; | ||||||
|  | 
 | ||||||
| // Фреймворк ArangoDB
 | // Фреймворк ArangoDB
 | ||||||
| use mirzaev\arangodb\collection, | use mirzaev\arangodb\collection, | ||||||
|   mirzaev\arangodb\document; |   mirzaev\arangodb\document; | ||||||
| @@ -22,15 +25,17 @@ use exception; | |||||||
|  */ |  */ | ||||||
| final class account extends core | final class account extends core | ||||||
| { | { | ||||||
|  |   use instance; | ||||||
|  | 
 | ||||||
|   /** |   /** | ||||||
|    * Коллекция |    * Коллекция | ||||||
|    */ |    */ | ||||||
|   public const COLLECTION = 'account'; |   final public const COLLECTION = 'account'; | ||||||
| 
 | 
 | ||||||
|   /** |   /** | ||||||
|    * Инстанция документа аккаунта в базе данных |    * Инстанция документа аккаунта в базе данных | ||||||
|    */ |    */ | ||||||
|   public ?_document $document; |   protected readonly _document $document; | ||||||
| 
 | 
 | ||||||
|   /** |   /** | ||||||
|    * Конструктор |    * Конструктор | ||||||
| @@ -112,7 +117,8 @@ final class account extends core | |||||||
| 
 | 
 | ||||||
|                       return $this; |                       return $this; | ||||||
|                     } else throw new exception('Неправильный пароль'); |                     } else throw new exception('Неправильный пароль'); | ||||||
|                   } throw new exception('Неправильный пароль'); |                   } | ||||||
|  |                   throw new exception('Неправильный пароль'); | ||||||
|                 } else { |                 } else { | ||||||
|                   // Не найден аккаунт
 |                   // Не найден аккаунт
 | ||||||
| 
 | 
 | ||||||
| @@ -174,7 +180,6 @@ final class account extends core | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|   /** |   /** | ||||||
|    * Найти по входному псевдониму |    * Найти по входному псевдониму | ||||||
|    * |    * | ||||||
| @@ -186,15 +191,15 @@ final class account extends core | |||||||
|   public static function login(string $login, array &$errors = []): ?self |   public static function login(string $login, array &$errors = []): ?self | ||||||
|   { |   { | ||||||
|     try { |     try { | ||||||
|       if (collection::init(static::$db->session, self::COLLECTION)) { |       if (collection::init(static::$arangodb->session, self::COLLECTION)) { | ||||||
|         // Инициализирована коллекция
 |         // Инициализирована коллекция
 | ||||||
| 
 | 
 | ||||||
|         // Инициализация инстанции аккаунта
 |         // Инициализация инстанции аккаунта
 | ||||||
|         $instance = new self; |         $account = new self; | ||||||
| 
 | 
 | ||||||
|         // Поиск инстанции аккаунта в базе данных
 |         // Поиск инстанции аккаунта в базе данных
 | ||||||
|         $instance->document = collection::search( |         $instance = $account->instance(collection::search( | ||||||
|           static::$db->session, |           static::$arangodb->session, | ||||||
|           sprintf( |           sprintf( | ||||||
|             <<<'AQL' |             <<<'AQL' | ||||||
|               FOR d IN %s |               FOR d IN %s | ||||||
| @@ -204,10 +209,10 @@ final class account extends core | |||||||
|             self::COLLECTION, |             self::COLLECTION, | ||||||
|             $login |             $login | ||||||
|           ) |           ) | ||||||
|         ); |         )); | ||||||
| 
 | 
 | ||||||
|         if ($instance->document instanceof _document) return $instance; |         // Возврат (успех)
 | ||||||
|         else throw new exception('Не удалось найти инстанцию аккаунта в базе данных'); |         return $instance instanceof _document ? $account : throw new exception('Не удалось найти инстанцию аккаунта в базе данных'); | ||||||
|       } else throw new exception('Не удалось инициализировать коллекцию'); |       } else throw new exception('Не удалось инициализировать коллекцию'); | ||||||
|     } catch (exception $e) { |     } catch (exception $e) { | ||||||
|       // Запись в реестр ошибок
 |       // Запись в реестр ошибок
 | ||||||
| @@ -233,8 +238,8 @@ final class account extends core | |||||||
|   public static function create(array $data = [], array &$errors = []): bool |   public static function create(array $data = [], array &$errors = []): bool | ||||||
|   { |   { | ||||||
|     try { |     try { | ||||||
|       if (collection::init(static::$db->session, self::COLLECTION)) |       if (collection::init(static::$arangodb->session, self::COLLECTION)) | ||||||
|         if (document::write(static::$db->session, self::COLLECTION, $data)) return true; |         if (document::write(static::$arangodb->session, self::COLLECTION, $data)) return true; | ||||||
|         else throw new exception('Не удалось создать аккаунт'); |         else throw new exception('Не удалось создать аккаунт'); | ||||||
|       else throw new exception('Не удалось инициализировать коллекцию'); |       else throw new exception('Не удалось инициализировать коллекцию'); | ||||||
|     } catch (exception $e) { |     } catch (exception $e) { | ||||||
							
								
								
									
										245
									
								
								svoboda/accounts/system/models/core.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										245
									
								
								svoboda/accounts/system/models/core.php
									
									
									
									
									
										Normal 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) | ||||||
|  | 		}; | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -4,12 +4,12 @@ declare(strict_types=1); | |||||||
| 
 | 
 | ||||||
| namespace mirzaev\site\account\models; | namespace mirzaev\site\account\models; | ||||||
| 
 | 
 | ||||||
| // Файлы проекта
 | // Project files
 | ||||||
| use mirzaev\site\account\models\account; | use mirzaev\site\account\models\account, | ||||||
|  |   mirzaev\site\account\models\traits\instance; | ||||||
| 
 | 
 | ||||||
| // Фреймворк ArangoDB
 | // Фреймворк ArangoDB
 | ||||||
| use mirzaev\arangodb\collection, | use mirzaev\arangodb\collection; | ||||||
|   mirzaev\arangodb\document; |  | ||||||
| 
 | 
 | ||||||
| // Библиотека для ArangoDB
 | // Библиотека для ArangoDB
 | ||||||
| use ArangoDBClient\Document as _document; | use ArangoDBClient\Document as _document; | ||||||
| @@ -25,36 +25,38 @@ use exception; | |||||||
|  */ |  */ | ||||||
| final class invite extends core | final class invite extends core | ||||||
| { | { | ||||||
|  |   use instance; | ||||||
|  | 
 | ||||||
|   /** |   /** | ||||||
|    * Коллекция |    * Коллекция | ||||||
|    */ |    */ | ||||||
|   public const COLLECTION = 'invite'; |   final public const COLLECTION = 'invite'; | ||||||
| 
 | 
 | ||||||
|   /** |   /** | ||||||
|    * Инстанция документа приглашения в базе данных |    * Инстанция документа приглашения в базе данных | ||||||
|    */ |    */ | ||||||
|   public ?_document $document; |   protected readonly _document $document; | ||||||
| 
 | 
 | ||||||
|   /** |   /** | ||||||
|    * Прочитать |    * Прочитать | ||||||
|    * |    * | ||||||
|    * @param string $invite Ключ приглашения |    * @param string $key Ключ приглашения | ||||||
|    * @param array &$errors Реестр ошибок |    * @param array &$errors Реестр ошибок | ||||||
|    * |    * | ||||||
|    * @return ?self Инстанция приглашения, если оно найдено |    * @return ?self Инстанция приглашения, если оно найдено | ||||||
|    */ |    */ | ||||||
|   public static function read(string $invite, array &$errors = []): ?self |   public static function read(string $key, array &$errors = []): ?self | ||||||
|   { |   { | ||||||
|     try { |     try { | ||||||
|       if (collection::init(static::$db->session, self::COLLECTION)) { |       if (collection::init(static::$arangodb->session, self::COLLECTION)) { | ||||||
|         // Инициализирована коллекция
 |         // Инициализирована коллекция
 | ||||||
| 
 | 
 | ||||||
|         // Инициализация инстанции приглашения
 |         // Инициализация инстанции приглашения
 | ||||||
|         $instance = new self; |         $invite = new self; | ||||||
| 
 | 
 | ||||||
|         // Поиск приглашения
 |         // Поиск приглашения
 | ||||||
|         $instance->document = collection::search( |         $instance = $invite->instance(collection::search( | ||||||
|           static::$db->session, |           static::$arangodb->session, | ||||||
|           sprintf( |           sprintf( | ||||||
|             <<<AQL |             <<<AQL | ||||||
|               FOR d IN %s |               FOR d IN %s | ||||||
| @@ -62,13 +64,13 @@ final class invite extends core | |||||||
|                 RETURN d |                 RETURN d | ||||||
|             AQL, |             AQL, | ||||||
|             self::COLLECTION, |             self::COLLECTION, | ||||||
|             $invite |             $key | ||||||
|           ) |           ) | ||||||
|         ); |         )); | ||||||
| 
 | 
 | ||||||
|         if ($instance->document instanceof _document) return $instance; |         // Exit (success)
 | ||||||
|         else throw new exception('Не удалось найти инстанцию приглашения в базе данных'); |         return $instance instanceof _document ? $invite : throw new exception('Не удалось найти инстанцию приглашения в базе данных'); | ||||||
|       } throw new exception('Не удалось инициализировать коллекцию'); |       } else throw new exception('Не удалось инициализировать коллекцию'); | ||||||
|     } catch (exception $e) { |     } catch (exception $e) { | ||||||
|       // Запись в реестр ошибок
 |       // Запись в реестр ошибок
 | ||||||
|       $errors[] = [ |       $errors[] = [ | ||||||
							
								
								
									
										411
									
								
								svoboda/accounts/system/models/session.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										411
									
								
								svoboda/accounts/system/models/session.php
									
									
									
									
									
										Normal 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); | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										36
									
								
								svoboda/accounts/system/models/traits/instance.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								svoboda/accounts/system/models/traits/instance.php
									
									
									
									
									
										Normal 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; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										277
									
								
								svoboda/accounts/system/public/css/account.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										277
									
								
								svoboda/accounts/system/public/css/account.css
									
									
									
									
									
										Normal 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; | ||||||
|  | } | ||||||
							
								
								
									
										290
									
								
								svoboda/accounts/system/public/css/main.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										290
									
								
								svoboda/accounts/system/public/css/main.css
									
									
									
									
									
										Normal 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
		Reference in New Issue
	
	Block a user