diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..dd01dc8 --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,52 @@ +name: Docker + +on: + push: + branches: [ main ] + tags: [ 'v*.*' ] + pull_request: + branches: [ main ] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Log into registry ${{ env.REGISTRY }} + if: github.event_name != 'pull_request' + uses: docker/login-action@v1 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # https://github.com/docker/metadata-action + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v3 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + # https://github.com/docker/build-push-action + - name: Build and push Docker image + uses: docker/build-push-action@v2 + with: + file: docker/Dockerfile + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: | + "COMPOSER_VERSION=2.1.5" + "PHP_VERSION=7.4" + "DEBIAN_RELEASE=buster" diff --git a/.gitignore b/.gitignore index 84b9a7c..4c169ac 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,7 @@ vendor webroot/theme/node_modules webroot/scss/*.css .vscode +docker/run/ +.phpunit.result.cache +config.json +phpunit.xml diff --git a/INSTALL/INSTALL.md b/INSTALL/INSTALL.md index a962de1..f4cef5f 100644 --- a/INSTALL/INSTALL.md +++ b/INSTALL/INSTALL.md @@ -1,8 +1,9 @@ ## Requirements An Ubuntu server (18.04/20.04 should both work fine) - though other linux installations should work too. -- apache2, mysql/mariadb, sqlite need to be installed and running +- apache2 (or nginx), mysql/mariadb, sqlite need to be installed and running - php extensions for intl, mysql, sqlite3, mbstring, xml need to be installed and running +- php extention for curl (not required but makes composer run a little faster) - composer ## Network requirements @@ -17,8 +18,16 @@ Cerebrate communicates via HTTPS so in order to be able to connect to other cere ## Cerebrate installation instructions It should be sufficient to issue the following command to install the dependencies: + +- for apache + ```bash -sudo apt install apache2 mariadb-server git composer php-intl php-mbstring php-dom php-xml unzip php-ldap php-sqlite3 sqlite libapache2-mod-php php-mysql +sudo apt install apache2 mariadb-server git composer php-intl php-mbstring php-dom php-xml unzip php-ldap php-sqlite3 php-curl sqlite libapache2-mod-php php-mysql +``` + +- for nginx +```bash +sudo apt install nginx mariadb-server git composer php-intl php-mbstring php-dom php-xml unzip php-ldap php-sqlite3 sqlite php-fpm php-curl php-mysql ``` Clone this repository (for example into /var/www/cerebrate) @@ -32,12 +41,19 @@ sudo -u www-data git clone https://github.com/cerebrate-project/cerebrate.git /v Run composer ```bash +sudo mkdir -p /var/www/.composer +sudo chown www-data:www-data /var/www/.composer cd /var/www/cerebrate -sudo -u www-data composer install +sudo -H -u www-data composer install ``` Create a database for cerebrate +With a fresh install of Ubuntu sudo to the (system) root user before logging in as the mysql root +```Bash +sudo -i mysql -u root +``` + From SQL shell: ```mysql mysql @@ -46,6 +62,7 @@ CREATE USER 'cerebrate'@'localhost' IDENTIFIED BY 'YOUR_PASSWORD'; GRANT USAGE ON *.* to cerebrate@localhost; GRANT ALL PRIVILEGES ON cerebrate.* to cerebrate@localhost; FLUSH PRIVILEGES; +QUIT; ``` Or from Bash: @@ -57,12 +74,6 @@ sudo mysql -e "GRANT ALL PRIVILEGES ON cerebrate.* to cerebrate@localhost;" sudo mysql -e "FLUSH PRIVILEGES;" ``` -Load the default table structure into the database - -```bash -sudo mysql -u cerebrate -p cerebrate < /var/www/cerebrate/INSTALL/mysql.sql -``` - create your local configuration and set the db credentials ```bash @@ -71,7 +82,7 @@ sudo -u www-data cp -a /var/www/cerebrate/config/config.example.json /var/www/ce sudo -u www-data vim /var/www/cerebrate/config/app_local.php ``` -mod_rewrite needs to be enabled: +mod_rewrite needs to be enabled if __using apache__: ```bash sudo a2enmod rewrite @@ -91,9 +102,9 @@ This would be, when following the steps above: Run the database schema migrations ```bash -/var/www/cerebrate/bin/cake migrations migrate -/var/www/cerebrate/bin/cake migrations migrate -p tags -/var/www/cerebrate/bin/cake migrations migrate -p ADmad/SocialAuth +sudo -u www-data /var/www/cerebrate/bin/cake migrations migrate +sudo -u www-data /var/www/cerebrate/bin/cake migrations migrate -p tags +sudo -u www-data /var/www/cerebrate/bin/cake migrations migrate -p ADmad/SocialAuth ``` Clean cakephp caches @@ -104,16 +115,31 @@ sudo rm /var/www/cerebrate/tmp/cache/persistent/* Create an apache config file for cerebrate / ssh key and point the document root to /var/www/cerebrate/webroot and you're good to go -For development installs the following can be done: +For development installs the following can be done for either apache or nginx: ```bash +# Apache # This configuration is purely meant for local installations for development / testing # Using HTTP on an unhardened apache is by no means meant to be used in any production environment -sudo cp /var/www/cerebrate/INSTALL/cerebrate_dev.conf /etc/apache2/sites-available/ -sudo ln -s /etc/apache2/sites-available/cerebrate_dev.conf /etc/apache2/sites-enabled/ +sudo cp /var/www/cerebrate/INSTALL/cerebrate_apache_dev.conf /etc/apache2/sites-available/ +sudo ln -s /etc/apache2/sites-available/cerebrate_apache_dev.conf /etc/apache2/sites-enabled/ sudo service apache2 restart ``` +OR + +```bash +# NGINX +# This configuration is purely meant for local installations for development / testing +# Using HTTP on an unhardened apache is by no means meant to be used in any production environment +sudo cp /var/www/cerebrate/INSTALL/cerebrate_nginx.conf /etc/nginx/sites-available/ +sudo ln -s /etc/nginx/sites-available/cerebrate_nginx.conf /etc/nginx/sites-enabled/ +sudo systemctl disable apache2 # may be required if apache is using port +sudo service nginx restart +sudo systemctl enable nginx + +``` + Now you can point your browser to: http://localhost:8000 To log in use the default credentials below: diff --git a/INSTALL/cerebrate_dev.conf b/INSTALL/cerebrate_apache_dev.conf old mode 100755 new mode 100644 similarity index 100% rename from INSTALL/cerebrate_dev.conf rename to INSTALL/cerebrate_apache_dev.conf diff --git a/INSTALL/cerebrate_nginx.conf b/INSTALL/cerebrate_nginx.conf new file mode 100644 index 0000000..59ad861 --- /dev/null +++ b/INSTALL/cerebrate_nginx.conf @@ -0,0 +1,37 @@ +## Cerebrate Nginx Web Server Configuration +server { + listen 8000; + # listen 443 ssl; + + root /var/www/cerebrate/webroot; + error_log /var/log/nginx/cerebrate_error.log; + access_log /var/log/nginx/cerebrate_access.log; + + # Add index.php to the list if you are using PHP + index index.html index.htm index.nginx-debian.html index.php; + + server_name _; + + # Configure Crypto Keys/Certificates/DH + # If enabling this setting change port above, should also set the server name + # ssl_certificate /path/to/ssl/cert; + # ssl_certificate_key /path/to/ssl/cert; + + # enable HSTS + # add_header Strict-Transport-Security "max-age=15768000; includeSubdomains"; + # add_header X-Frame-Options SAMEORIGIN; + + location / { + try_files $uri $uri/ /index.php?$args; + } + + location ~ \.php$ { + try_files $uri =404; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass unix:/var/run/php/php7.4-fpm.sock; + fastcgi_index index.php; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param PATH_INFO $fastcgi_path_info; + } +} diff --git a/INSTALL/mysql.sql b/INSTALL/mysql.sql deleted file mode 100644 index 88a3040..0000000 --- a/INSTALL/mysql.sql +++ /dev/null @@ -1,402 +0,0 @@ --- MySQL dump 10.16 Distrib 10.1.44-MariaDB, for debian-linux-gnu (x86_64) --- --- Host: localhost Database: cerebrate --- ------------------------------------------------------ --- Server version 10.1.44-MariaDB-0ubuntu0.18.04.1 - -/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; -/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; -/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; -/*!40101 SET NAMES utf8mb4 */; -/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; -/*!40103 SET TIME_ZONE='+00:00' */; -/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; -/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; -/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; -/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; - --- --- Table structure for table `alignment_tags` --- - -DROP TABLE IF EXISTS `alignment_tags`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `alignment_tags` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `alignment_id` int(10) unsigned NOT NULL, - `tag_id` int(10) unsigned NOT NULL, - PRIMARY KEY (`id`), - KEY `alignment_id` (`alignment_id`), - KEY `tag_id` (`tag_id`), - CONSTRAINT `alignment_tags_ibfk_1` FOREIGN KEY (`alignment_id`) REFERENCES `alignments` (`id`), - CONSTRAINT `alignment_tags_ibfk_10` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`), - CONSTRAINT `alignment_tags_ibfk_11` FOREIGN KEY (`alignment_id`) REFERENCES `alignments` (`id`), - CONSTRAINT `alignment_tags_ibfk_12` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`), - CONSTRAINT `alignment_tags_ibfk_2` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`), - CONSTRAINT `alignment_tags_ibfk_3` FOREIGN KEY (`alignment_id`) REFERENCES `alignments` (`id`), - CONSTRAINT `alignment_tags_ibfk_4` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`), - CONSTRAINT `alignment_tags_ibfk_5` FOREIGN KEY (`alignment_id`) REFERENCES `alignments` (`id`), - CONSTRAINT `alignment_tags_ibfk_6` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`), - CONSTRAINT `alignment_tags_ibfk_7` FOREIGN KEY (`alignment_id`) REFERENCES `alignments` (`id`), - CONSTRAINT `alignment_tags_ibfk_8` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`), - CONSTRAINT `alignment_tags_ibfk_9` FOREIGN KEY (`alignment_id`) REFERENCES `alignments` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `alignments` --- - -DROP TABLE IF EXISTS `alignments`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `alignments` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `individual_id` int(10) unsigned NOT NULL, - `organisation_id` int(10) unsigned NOT NULL, - `type` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT 'member', - PRIMARY KEY (`id`), - KEY `individual_id` (`individual_id`), - KEY `organisation_id` (`organisation_id`), - CONSTRAINT `alignments_ibfk_1` FOREIGN KEY (`individual_id`) REFERENCES `individuals` (`id`), - CONSTRAINT `alignments_ibfk_2` FOREIGN KEY (`organisation_id`) REFERENCES `organisations` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `auth_keys` --- - -DROP TABLE IF EXISTS `auth_keys`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `auth_keys` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `uuid` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL, - `authkey` varchar(72) CHARACTER SET ascii DEFAULT NULL, - `authkey_start` varchar(4) CHARACTER SET ascii DEFAULT NULL, - `authkey_end` varchar(4) CHARACTER SET ascii DEFAULT NULL, - `created` int(10) unsigned NOT NULL, - `expiration` int(10) unsigned NOT NULL, - `user_id` int(10) unsigned NOT NULL, - `comment` text COLLATE utf8mb4_unicode_ci, - PRIMARY KEY (`id`), - KEY `authkey_start` (`authkey_start`), - KEY `authkey_end` (`authkey_end`), - KEY `created` (`created`), - KEY `expiration` (`expiration`), - KEY `user_id` (`user_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `broods` --- - -DROP TABLE IF EXISTS `broods`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `broods` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `uuid` varchar(40) CHARACTER SET ascii DEFAULT NULL, - `name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL, - `url` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL, - `description` text COLLATE utf8mb4_unicode_ci, - `organisation_id` int(10) unsigned NOT NULL, - `trusted` tinyint(1) DEFAULT NULL, - `pull` tinyint(1) DEFAULT NULL, - `skip_proxy` tinyint(1) DEFAULT NULL, - `authkey` varchar(40) CHARACTER SET ascii DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `uuid` (`uuid`), - KEY `name` (`name`), - KEY `url` (`url`), - KEY `authkey` (`authkey`), - KEY `organisation_id` (`organisation_id`), - FOREIGN KEY (`organisation_id`) REFERENCES `organisations` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `encryption_keys` --- - -DROP TABLE IF EXISTS `encryption_keys`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `encryption_keys` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `uuid` varchar(40) CHARACTER SET ascii DEFAULT NULL, - `type` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL, - `encryption_key` text COLLATE utf8mb4_unicode_ci, - `revoked` tinyint(1) DEFAULT NULL, - `expires` int(10) unsigned DEFAULT NULL, - `owner_id` int(10) unsigned DEFAULT NULL, - `owner_type` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL, - PRIMARY KEY (`id`), - KEY `uuid` (`uuid`), - KEY `type` (`type`), - KEY `expires` (`expires`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `individual_encryption_keys` --- - -DROP TABLE IF EXISTS `individual_encryption_keys`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `individual_encryption_keys` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `individual_id` int(10) unsigned NOT NULL, - `encryption_key_id` int(10) unsigned NOT NULL, - PRIMARY KEY (`id`), - KEY `individual_id` (`individual_id`), - KEY `encryption_key_id` (`encryption_key_id`), - CONSTRAINT `individual_encryption_keys_ibfk_1` FOREIGN KEY (`individual_id`) REFERENCES `individuals` (`id`), - CONSTRAINT `individual_encryption_keys_ibfk_2` FOREIGN KEY (`encryption_key_id`) REFERENCES `encryption_keys` (`id`), - CONSTRAINT `individual_encryption_keys_ibfk_3` FOREIGN KEY (`individual_id`) REFERENCES `individuals` (`id`), - CONSTRAINT `individual_encryption_keys_ibfk_4` FOREIGN KEY (`encryption_key_id`) REFERENCES `encryption_keys` (`id`), - CONSTRAINT `individual_encryption_keys_ibfk_5` FOREIGN KEY (`individual_id`) REFERENCES `individuals` (`id`), - CONSTRAINT `individual_encryption_keys_ibfk_6` FOREIGN KEY (`encryption_key_id`) REFERENCES `encryption_keys` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `individuals` --- - -DROP TABLE IF EXISTS `individuals`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `individuals` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `uuid` varchar(40) CHARACTER SET ascii DEFAULT NULL, - `email` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL, - `first_name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL, - `last_name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL, - `position` text COLLATE utf8mb4_unicode_ci, - PRIMARY KEY (`id`), - KEY `uuid` (`uuid`), - KEY `email` (`email`), - KEY `first_name` (`first_name`), - KEY `last_name` (`last_name`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `local_tools` --- - -DROP TABLE IF EXISTS `local_tools`; -CREATE TABLE `local_tools` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL, - `connector` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL, - `settings` text COLLATE utf8mb4_unicode_ci, - `exposed` tinyint(1) NOT NULL, - `description` text COLLATE utf8mb4_unicode_ci, - PRIMARY KEY (`id`), - KEY `name` (`name`), - KEY `connector` (`connector`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - --- --- Table structure for table `organisation_encryption_keys` --- - -DROP TABLE IF EXISTS `organisation_encryption_keys`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `organisation_encryption_keys` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `organisation_id` int(10) unsigned NOT NULL, - `encryption_key_id` int(10) unsigned NOT NULL, - PRIMARY KEY (`id`), - KEY `organisation_id` (`organisation_id`), - KEY `encryption_key_id` (`encryption_key_id`), - CONSTRAINT `organisation_encryption_keys_ibfk_1` FOREIGN KEY (`organisation_id`) REFERENCES `organisations` (`id`), - CONSTRAINT `organisation_encryption_keys_ibfk_2` FOREIGN KEY (`encryption_key_id`) REFERENCES `encryption_keys` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `organisations` --- - -DROP TABLE IF EXISTS `organisations`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `organisations` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `uuid` varchar(40) CHARACTER SET ascii DEFAULT NULL, - `name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL, - `url` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `nationality` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `sector` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `type` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `contacts` text COLLATE utf8mb4_unicode_ci, - PRIMARY KEY (`id`), - KEY `uuid` (`uuid`), - KEY `name` (`name`), - KEY `url` (`url`), - KEY `nationality` (`nationality`), - KEY `sector` (`sector`), - KEY `type` (`type`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `roles` --- - -DROP TABLE IF EXISTS `roles`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `roles` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `uuid` varchar(40) CHARACTER SET ascii DEFAULT NULL, - `name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL, - `is_default` tinyint(1) DEFAULT NULL, - `perm_admin` tinyint(1) DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `name` (`name`), - KEY `uuid` (`uuid`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `tags` --- - -DROP TABLE IF EXISTS `tags`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `tags` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL, - `description` text COLLATE utf8mb4_unicode_ci, - `colour` varchar(6) CHARACTER SET ascii NOT NULL, - PRIMARY KEY (`id`), - KEY `name` (`name`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `users` --- - -DROP TABLE IF EXISTS `users`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `users` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `uuid` varchar(40) CHARACTER SET ascii DEFAULT NULL, - `username` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `password` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `role_id` int(11) unsigned NOT NULL, - `individual_id` int(11) unsigned NOT NULL, - `disabled` tinyint(1) DEFAULT '0', - PRIMARY KEY (`id`), - KEY `uuid` (`uuid`), - KEY `role_id` (`role_id`), - KEY `individual_id` (`individual_id`), - CONSTRAINT `users_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`), - CONSTRAINT `users_ibfk_2` FOREIGN KEY (`individual_id`) REFERENCES `individuals` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -/*!40101 SET character_set_client = @saved_cs_client */; -/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; - -CREATE TABLE `sharing_groups` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `uuid` varchar(40) CHARACTER SET ascii DEFAULT NULL, - `name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL, - `releasability` text DEFAULT NULL, - `description` text DEFAULT NULL, - `organisation_id` int(10) unsigned NOT NULL, - `user_id` int(10) unsigned NOT NULL, - `active` tinyint(1) DEFAULT '1', - `local` tinyint(1) DEFAULT '1', - PRIMARY KEY (`id`), - KEY `uuid` (`uuid`), - KEY `user_id` (`user_id`), - KEY `organisation_id` (`organisation_id`), - KEY `name` (`name`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - -CREATE TABLE `sgo` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `sharing_group_id` int(10) unsigned NOT NULL, - `organisation_id` int(10) unsigned NOT NULL, - `deleted` tinyint(1) DEFAULT 0, - PRIMARY KEY (`id`), - KEY `sharing_group_id` (`sharing_group_id`), - KEY `organisation_id` (`organisation_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - -CREATE TABLE `meta_fields` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `scope` varchar(191) NOT NULL, - `parent_id` int(10) unsigned NOT NULL, - `field` varchar(191) NOT NULL, - `value` varchar(191) NOT NULL, - `uuid` varchar(40) CHARACTER SET ascii DEFAULT NULL, - `meta_template_id` int(10) unsigned NOT NULL, - `meta_template_field_id` int(10) unsigned NOT NULL, - `is_default` tinyint(1) NOT NULL DEFAULT 0, - PRIMARY KEY (`id`), - KEY `scope` (`scope`), - KEY `uuid` (`uuid`), - KEY `parent_id` (`parent_id`), - KEY `field` (`field`), - KEY `value` (`value`), - KEY `meta_template_id` (`meta_template_id`), - KEY `meta_template_field_id` (`meta_template_field_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - -CREATE TABLE `meta_templates` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `scope` varchar(191) NOT NULL, - `name` varchar(191) NOT NULL, - `namespace` varchar(191) NOT NULL, - `description` text, - `version` varchar(191) NOT NULL, - `uuid` varchar(40) CHARACTER SET ascii, - `source` varchar(191), - `enabled` tinyint(1) DEFAULT 0, - `is_default` tinyint(1) NOT NULL DEFAULT 0, - PRIMARY KEY (`id`), - KEY `scope` (`scope`), - KEY `source` (`source`), - KEY `name` (`name`), - KEY `namespace` (`namespace`), - KEY `version` (`version`), - KEY `uuid` (`uuid`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - -CREATE TABLE `meta_template_fields` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `field` varchar(191) NOT NULL, - `type` varchar(191) NOT NULL, - `meta_template_id` int(10) unsigned NOT NULL, - `regex` text, - `multiple` tinyint(1) DEFAULT 0, - `enabled` tinyint(1) DEFAULT 0, - PRIMARY KEY (`id`), - CONSTRAINT `meta_template_id` FOREIGN KEY (`meta_template_id`) REFERENCES `meta_templates` (`id`), - KEY `field` (`field`), - KEY `type` (`type`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - - -/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; -/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; -/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; -/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; -/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; -/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; -/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; - --- Dump completed on 2020-06-22 14:30:02 diff --git a/README.md b/README.md index c2dfa33..0114d21 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,31 @@ # cerebrate -The Cerebrate Sync Platform core software. Cerebrate is an open-source platform meant to act as a trusted contact information provider and interconnection orchestrator for other security tools. +Cerebrate is an [open-source platform](https://github.com/cerebrate-project) meant to act as a trusted contact information provider and interconnection orchestrator for other security tools (such as [MISP](https://www.misp-project.org/)). -It is currently being built under the MeliCERTes v2 project and is heavily work in progress. +# Features -# Current features +- Advanced repository to manage individuals and organisations; +- Key store for public encryption and signing cryptographic keys (e.g. PGP); +- Distributed synchronisation model where multiple Cerebrate instances can be interconnected amongst organisations and/or departments; +- Management of individuals and their affiliations to each organisations; +- Advanced API and CLI to integrate with existing tools (e.g. importing existing directory information); +- Dynamic model for creating new organisational structures; +- Support existing organisational structures such as [FIRST.org](https://www.first.org/) directory, EU [CSIRTs network](https://csirtsnetwork.eu/); +- Local tooling interconnection to easily connect existing tools with their native protocols; -- Repository of organisations and individuals -- Maintain signing and encryption keys -- Maintain affiliations between organisations and individuals +Cerebrate is developed in the scope of the MeliCERTes v2 project. ## Screenshots +![Dashboard](https://www.cerebrate-project.org/assets/images/screenshots/Screenshot%20from%202021-10-19%2016-31-56.png) + List of individuals along with their affiliations -![List of individuals](/documentation/images/individuals.png) +![List of individuals](https://www.cerebrate-project.org/assets/images/screenshots/Screenshot%20from%202021-10-19%2016-32-35.png) Adding organisations -![Adding an organisation](/documentation/images/add_org.png) +![Adding an organisation](https://www.cerebrate-project.org/assets/images/screenshots/Screenshot%20from%202021-10-19%2016-33-04.png) Everything is available via the API, here an example of a search query for all international organisations in the DB. @@ -28,6 +35,10 @@ Managing public keys and assigning them to users both for communication and vali ![Encryption key management](/documentation/images/add_encryption_key.png) +Dynamic model for creating new organisation structre + +![Meta Field Templates](https://www.cerebrate-project.org/assets/images/screenshots/Screenshot%20from%202021-10-19%2016-38-21.png) + # Requirements and installation The platform is built on CakePHP 4 along with Bootstrap 4 and shares parts of the code-base with [MISP](https://www.github.com/MISP). @@ -45,6 +56,7 @@ For installation via docker, refer to the [cerebrate-docker](https://github.com/ ~~~~ The software is released under the AGPLv3. - Copyright (C) 2019, 2020 Andras Iklody + Copyright (C) 2019, 2021 Andras Iklody + Copyright (C) 2020-2021 Sami Mokaddem Copyright (C) CIRCL - Computer Incident Response Center Luxembourg ~~~~ diff --git a/composer.json b/composer.json index 42a49b2..f51bc6f 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,7 @@ "admad/cakephp-social-auth": "^1.1", "cakephp/authentication": "^2.0", "cakephp/authorization": "^2.0", - "cakephp/cakephp": "^4.0", + "cakephp/cakephp": "^4.3", "cakephp/migrations": "^3.0", "cakephp/plugin-installer": "^1.2", "erusev/parsedown": "^1.7", @@ -19,9 +19,12 @@ "cakephp/bake": "^2.0.3", "cakephp/cakephp-codesniffer": "~4.0.0", "cakephp/debug_kit": "^4.0", + "fzaninotto/faker": "^1.9", "josegonzalez/dotenv": "^3.2", + "league/openapi-psr7-validator": "^0.16.4", "phpunit/phpunit": "^8.5", - "psy/psysh": "@stable" + "psy/psysh": "@stable", + "wiremock-php/wiremock-php": "^2.32" }, "suggest": { "markstory/asset_compress": "An asset compression plugin which provides file concatenation and a flexible filter system for preprocessing and minification.", @@ -44,7 +47,6 @@ "scripts": { "post-install-cmd": "App\\Console\\Installer::postInstall", "post-create-project-cmd": "App\\Console\\Installer::postInstall", - "post-autoload-dump": "Cake\\Composer\\Installer\\PluginInstaller::postAutoloadDump", "check": [ "@test", "@cs-check" @@ -52,11 +54,15 @@ "cs-check": "phpcs --colors -p --standard=vendor/cakephp/cakephp-codesniffer/CakePHP src/ tests/", "cs-fix": "phpcbf --colors --standard=vendor/cakephp/cakephp-codesniffer/CakePHP src/ tests/", "stan": "phpstan analyse src/", - "test": "phpunit --colors=always" + "test": [ + "sh ./tests/Helper/wiremock/start.sh", + "phpunit", + "sh ./tests/Helper/wiremock/stop.sh" + ] }, "prefer-stable": true, "config": { "sort-packages": true }, "minimum-stability": "dev" -} +} \ No newline at end of file diff --git a/config/Migrations/20210311000000_InitialSchema.php b/config/Migrations/20210311000000_InitialSchema.php new file mode 100644 index 0000000..d1e2ed1 --- /dev/null +++ b/config/Migrations/20210311000000_InitialSchema.php @@ -0,0 +1,1464 @@ +execute('SET unique_checks=0; SET foreign_key_checks=0;'); + $this->execute("ALTER DATABASE CHARACTER SET 'utf8mb4';"); + $this->execute("ALTER DATABASE COLLATE='utf8mb4_general_ci';"); + $this->table('broods', [ + 'id' => false, + 'primary_key' => ['id'], + 'engine' => 'InnoDB', + 'encoding' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'comment' => '', + 'row_format' => 'DYNAMIC', + ]) + ->addColumn('id', 'integer', [ + 'null' => false, + 'limit' => '10', + 'signed' => false, + 'identity' => 'enable', + ]) + ->addColumn('uuid', 'string', [ + 'null' => true, + 'default' => null, + 'limit' => 40, + 'collation' => 'ascii_general_ci', + 'encoding' => 'ascii', + 'after' => 'id', + ]) + ->addColumn('name', 'string', [ + 'null' => false, + 'limit' => 191, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'uuid', + ]) + ->addColumn('url', 'string', [ + 'null' => false, + 'limit' => 191, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'name', + ]) + ->addColumn('description', 'text', [ + 'null' => true, + 'default' => null, + 'limit' => 65535, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'url', + ]) + ->addColumn('organisation_id', 'integer', [ + 'null' => false, + 'limit' => '10', + 'signed' => false, + 'after' => 'description', + ]) + ->addColumn('trusted', 'boolean', [ + 'null' => true, + 'default' => null, + 'limit' => MysqlAdapter::INT_TINY, + 'after' => 'organisation_id', + ]) + ->addColumn('pull', 'boolean', [ + 'null' => true, + 'default' => null, + 'limit' => MysqlAdapter::INT_TINY, + 'after' => 'trusted', + ]) + ->addColumn('skip_proxy', 'boolean', [ + 'null' => true, + 'default' => null, + 'limit' => MysqlAdapter::INT_TINY, + 'after' => 'pull', + ]) + ->addColumn('authkey', 'string', [ + 'null' => true, + 'default' => null, + 'limit' => 40, + 'collation' => 'ascii_general_ci', + 'encoding' => 'ascii', + 'after' => 'skip_proxy', + ]) + ->addIndex(['uuid'], [ + 'name' => 'uuid', + 'unique' => false, + ]) + ->addIndex(['name'], [ + 'name' => 'name', + 'unique' => false, + ]) + ->addIndex(['url'], [ + 'name' => 'url', + 'unique' => false, + ]) + ->addIndex(['authkey'], [ + 'name' => 'authkey', + 'unique' => false, + ]) + ->addIndex(['organisation_id'], [ + 'name' => 'organisation_id', + 'unique' => false, + ]) + ->addForeignKey('organisation_id', 'organisations', 'id', [ + 'constraint' => 'broods_ibfk_1', + 'update' => 'RESTRICT', + 'delete' => 'RESTRICT', + ]) + ->create(); + $this->table('sharing_groups', [ + 'id' => false, + 'primary_key' => ['id'], + 'engine' => 'InnoDB', + 'encoding' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'comment' => '', + 'row_format' => 'DYNAMIC', + ]) + ->addColumn('id', 'integer', [ + 'null' => false, + 'limit' => '10', + 'signed' => false, + 'identity' => 'enable', + ]) + ->addColumn('uuid', 'string', [ + 'null' => true, + 'default' => null, + 'limit' => 40, + 'collation' => 'ascii_general_ci', + 'encoding' => 'ascii', + 'after' => 'id', + ]) + ->addColumn('name', 'string', [ + 'null' => false, + 'limit' => 191, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'uuid', + ]) + ->addColumn('releasability', 'text', [ + 'null' => true, + 'default' => null, + 'limit' => 65535, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'name', + ]) + ->addColumn('description', 'text', [ + 'null' => true, + 'default' => null, + 'limit' => 65535, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'releasability', + ]) + ->addColumn('organisation_id', 'integer', [ + 'null' => false, + 'limit' => '10', + 'signed' => false, + 'after' => 'description', + ]) + ->addColumn('user_id', 'integer', [ + 'null' => false, + 'limit' => '10', + 'signed' => false, + 'after' => 'organisation_id', + ]) + ->addColumn('active', 'boolean', [ + 'null' => true, + 'default' => '1', + 'limit' => MysqlAdapter::INT_TINY, + 'after' => 'user_id', + ]) + ->addColumn('local', 'boolean', [ + 'null' => true, + 'default' => '1', + 'limit' => MysqlAdapter::INT_TINY, + 'after' => 'active', + ]) + ->addIndex(['uuid'], [ + 'name' => 'uuid', + 'unique' => false, + ]) + ->addIndex(['user_id'], [ + 'name' => 'user_id', + 'unique' => false, + ]) + ->addIndex(['organisation_id'], [ + 'name' => 'organisation_id', + 'unique' => false, + ]) + ->addIndex(['name'], [ + 'name' => 'name', + 'unique' => false, + ]) + ->create(); + $this->table('alignment_tags', [ + 'id' => false, + 'primary_key' => ['id'], + 'engine' => 'InnoDB', + 'encoding' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'comment' => '', + 'row_format' => 'DYNAMIC', + ]) + ->addColumn('id', 'integer', [ + 'null' => false, + 'limit' => '10', + 'signed' => false, + 'identity' => 'enable', + ]) + ->addColumn('alignment_id', 'integer', [ + 'null' => false, + 'limit' => '10', + 'signed' => false, + 'after' => 'id', + ]) + ->addColumn('tag_id', 'integer', [ + 'null' => false, + 'limit' => '10', + 'signed' => false, + 'after' => 'alignment_id', + ]) + ->addIndex(['alignment_id'], [ + 'name' => 'alignment_id', + 'unique' => false, + ]) + ->addIndex(['tag_id'], [ + 'name' => 'tag_id', + 'unique' => false, + ]) + ->addForeignKey('alignment_id', 'alignments', 'id', [ + 'constraint' => 'alignment_tags_ibfk_1', + 'update' => 'RESTRICT', + 'delete' => 'RESTRICT', + ]) + ->addForeignKey('tag_id', 'tags', 'id', [ + 'constraint' => 'alignment_tags_ibfk_10', + 'update' => 'RESTRICT', + 'delete' => 'RESTRICT', + ]) + ->addForeignKey('alignment_id', 'alignments', 'id', [ + 'constraint' => 'alignment_tags_ibfk_11', + 'update' => 'RESTRICT', + 'delete' => 'RESTRICT', + ]) + ->addForeignKey('tag_id', 'tags', 'id', [ + 'constraint' => 'alignment_tags_ibfk_12', + 'update' => 'RESTRICT', + 'delete' => 'RESTRICT', + ]) + ->addForeignKey('tag_id', 'tags', 'id', [ + 'constraint' => 'alignment_tags_ibfk_2', + 'update' => 'RESTRICT', + 'delete' => 'RESTRICT', + ]) + ->addForeignKey('alignment_id', 'alignments', 'id', [ + 'constraint' => 'alignment_tags_ibfk_3', + 'update' => 'RESTRICT', + 'delete' => 'RESTRICT', + ]) + ->addForeignKey('tag_id', 'tags', 'id', [ + 'constraint' => 'alignment_tags_ibfk_4', + 'update' => 'RESTRICT', + 'delete' => 'RESTRICT', + ]) + ->addForeignKey('alignment_id', 'alignments', 'id', [ + 'constraint' => 'alignment_tags_ibfk_5', + 'update' => 'RESTRICT', + 'delete' => 'RESTRICT', + ]) + ->addForeignKey('tag_id', 'tags', 'id', [ + 'constraint' => 'alignment_tags_ibfk_6', + 'update' => 'RESTRICT', + 'delete' => 'RESTRICT', + ]) + ->addForeignKey('alignment_id', 'alignments', 'id', [ + 'constraint' => 'alignment_tags_ibfk_7', + 'update' => 'RESTRICT', + 'delete' => 'RESTRICT', + ]) + ->addForeignKey('tag_id', 'tags', 'id', [ + 'constraint' => 'alignment_tags_ibfk_8', + 'update' => 'RESTRICT', + 'delete' => 'RESTRICT', + ]) + ->addForeignKey('alignment_id', 'alignments', 'id', [ + 'constraint' => 'alignment_tags_ibfk_9', + 'update' => 'RESTRICT', + 'delete' => 'RESTRICT', + ]) + ->create(); + $this->table('meta_templates', [ + 'id' => false, + 'primary_key' => ['id'], + 'engine' => 'InnoDB', + 'encoding' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'comment' => '', + 'row_format' => 'DYNAMIC', + ]) + ->addColumn('id', 'integer', [ + 'null' => false, + 'limit' => '10', + 'signed' => false, + 'identity' => 'enable', + ]) + ->addColumn('scope', 'string', [ + 'null' => false, + 'limit' => 191, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'id', + ]) + ->addColumn('name', 'string', [ + 'null' => false, + 'limit' => 191, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'scope', + ]) + ->addColumn('namespace', 'string', [ + 'null' => false, + 'limit' => 191, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'name', + ]) + ->addColumn('description', 'text', [ + 'null' => true, + 'default' => null, + 'limit' => 65535, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'namespace', + ]) + ->addColumn('version', 'string', [ + 'null' => false, + 'limit' => 191, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'description', + ]) + ->addColumn('uuid', 'string', [ + 'null' => true, + 'default' => null, + 'limit' => 40, + 'collation' => 'ascii_general_ci', + 'encoding' => 'ascii', + 'after' => 'version', + ]) + ->addColumn('source', 'string', [ + 'null' => true, + 'default' => null, + 'limit' => 191, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'uuid', + ]) + ->addColumn('enabled', 'boolean', [ + 'null' => true, + 'default' => '0', + 'limit' => MysqlAdapter::INT_TINY, + 'after' => 'source', + ]) + ->addColumn('is_default', 'boolean', [ + 'null' => false, + 'default' => '0', + 'limit' => MysqlAdapter::INT_TINY, + 'after' => 'enabled', + ]) + ->addIndex(['scope'], [ + 'name' => 'scope', + 'unique' => false, + ]) + ->addIndex(['source'], [ + 'name' => 'source', + 'unique' => false, + ]) + ->addIndex(['name'], [ + 'name' => 'name', + 'unique' => false, + ]) + ->addIndex(['namespace'], [ + 'name' => 'namespace', + 'unique' => false, + ]) + ->addIndex(['version'], [ + 'name' => 'version', + 'unique' => false, + ]) + ->addIndex(['uuid'], [ + 'name' => 'uuid', + 'unique' => false, + ]) + ->create(); + $this->table('individuals', [ + 'id' => false, + 'primary_key' => ['id'], + 'engine' => 'InnoDB', + 'encoding' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'comment' => '', + 'row_format' => 'DYNAMIC', + ]) + ->addColumn('id', 'integer', [ + 'null' => false, + 'limit' => '10', + 'signed' => false, + 'identity' => 'enable', + ]) + ->addColumn('uuid', 'string', [ + 'null' => true, + 'default' => null, + 'limit' => 40, + 'collation' => 'ascii_general_ci', + 'encoding' => 'ascii', + 'after' => 'id', + ]) + ->addColumn('email', 'string', [ + 'null' => false, + 'limit' => 191, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'uuid', + ]) + ->addColumn('first_name', 'string', [ + 'null' => false, + 'limit' => 191, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'email', + ]) + ->addColumn('last_name', 'string', [ + 'null' => false, + 'limit' => 191, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'first_name', + ]) + ->addColumn('position', 'text', [ + 'null' => true, + 'default' => null, + 'limit' => 65535, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'last_name', + ]) + ->addIndex(['uuid'], [ + 'name' => 'uuid', + 'unique' => false, + ]) + ->addIndex(['email'], [ + 'name' => 'email', + 'unique' => false, + ]) + ->addIndex(['first_name'], [ + 'name' => 'first_name', + 'unique' => false, + ]) + ->addIndex(['last_name'], [ + 'name' => 'last_name', + 'unique' => false, + ]) + ->create(); + $this->table('organisations', [ + 'id' => false, + 'primary_key' => ['id'], + 'engine' => 'InnoDB', + 'encoding' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'comment' => '', + 'row_format' => 'DYNAMIC', + ]) + ->addColumn('id', 'integer', [ + 'null' => false, + 'limit' => '10', + 'signed' => false, + 'identity' => 'enable', + ]) + ->addColumn('uuid', 'string', [ + 'null' => true, + 'default' => null, + 'limit' => 40, + 'collation' => 'ascii_general_ci', + 'encoding' => 'ascii', + 'after' => 'id', + ]) + ->addColumn('name', 'string', [ + 'null' => false, + 'limit' => 191, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'uuid', + ]) + ->addColumn('url', 'string', [ + 'null' => true, + 'default' => null, + 'limit' => 191, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'name', + ]) + ->addColumn('nationality', 'string', [ + 'null' => true, + 'default' => null, + 'limit' => 191, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'url', + ]) + ->addColumn('sector', 'string', [ + 'null' => true, + 'default' => null, + 'limit' => 191, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'nationality', + ]) + ->addColumn('type', 'string', [ + 'null' => true, + 'default' => null, + 'limit' => 191, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'sector', + ]) + ->addColumn('contacts', 'text', [ + 'null' => true, + 'default' => null, + 'limit' => 65535, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'type', + ]) + ->addIndex(['uuid'], [ + 'name' => 'uuid', + 'unique' => false, + ]) + ->addIndex(['name'], [ + 'name' => 'name', + 'unique' => false, + ]) + ->addIndex(['url'], [ + 'name' => 'url', + 'unique' => false, + ]) + ->addIndex(['nationality'], [ + 'name' => 'nationality', + 'unique' => false, + ]) + ->addIndex(['sector'], [ + 'name' => 'sector', + 'unique' => false, + ]) + ->addIndex(['type'], [ + 'name' => 'type', + 'unique' => false, + ]) + ->create(); + $this->table('encryption_keys', [ + 'id' => false, + 'primary_key' => ['id'], + 'engine' => 'InnoDB', + 'encoding' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'comment' => '', + 'row_format' => 'DYNAMIC', + ]) + ->addColumn('id', 'integer', [ + 'null' => false, + 'limit' => '10', + 'signed' => false, + 'identity' => 'enable', + ]) + ->addColumn('uuid', 'string', [ + 'null' => true, + 'default' => null, + 'limit' => 40, + 'collation' => 'ascii_general_ci', + 'encoding' => 'ascii', + 'after' => 'id', + ]) + ->addColumn('type', 'string', [ + 'null' => false, + 'limit' => 191, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'uuid', + ]) + ->addColumn('encryption_key', 'text', [ + 'null' => true, + 'default' => null, + 'limit' => 65535, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'type', + ]) + ->addColumn('revoked', 'boolean', [ + 'null' => true, + 'default' => null, + 'limit' => MysqlAdapter::INT_TINY, + 'after' => 'encryption_key', + ]) + ->addColumn('expires', 'integer', [ + 'null' => true, + 'default' => null, + 'limit' => '10', + 'signed' => false, + 'after' => 'revoked', + ]) + ->addColumn('owner_id', 'integer', [ + 'null' => true, + 'default' => null, + 'limit' => '10', + 'signed' => false, + 'after' => 'expires', + ]) + ->addColumn('owner_type', 'string', [ + 'null' => false, + 'limit' => 20, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'owner_id', + ]) + ->addIndex(['uuid'], [ + 'name' => 'uuid', + 'unique' => false, + ]) + ->addIndex(['type'], [ + 'name' => 'type', + 'unique' => false, + ]) + ->addIndex(['expires'], [ + 'name' => 'expires', + 'unique' => false, + ]) + ->create(); + $this->table('meta_fields', [ + 'id' => false, + 'primary_key' => ['id'], + 'engine' => 'InnoDB', + 'encoding' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'comment' => '', + 'row_format' => 'DYNAMIC', + ]) + ->addColumn('id', 'integer', [ + 'null' => false, + 'limit' => '10', + 'signed' => false, + 'identity' => 'enable', + ]) + ->addColumn('scope', 'string', [ + 'null' => false, + 'limit' => 191, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'id', + ]) + ->addColumn('parent_id', 'integer', [ + 'null' => false, + 'limit' => '10', + 'signed' => false, + 'after' => 'scope', + ]) + ->addColumn('field', 'string', [ + 'null' => false, + 'limit' => 191, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'parent_id', + ]) + ->addColumn('value', 'string', [ + 'null' => false, + 'limit' => 191, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'field', + ]) + ->addColumn('uuid', 'string', [ + 'null' => true, + 'default' => null, + 'limit' => 40, + 'collation' => 'ascii_general_ci', + 'encoding' => 'ascii', + 'after' => 'value', + ]) + ->addColumn('meta_template_id', 'integer', [ + 'null' => false, + 'limit' => '10', + 'signed' => false, + 'after' => 'uuid', + ]) + ->addColumn('meta_template_field_id', 'integer', [ + 'null' => false, + 'limit' => '10', + 'signed' => false, + 'after' => 'meta_template_id', + ]) + ->addColumn('is_default', 'boolean', [ + 'null' => false, + 'default' => '0', + 'limit' => MysqlAdapter::INT_TINY, + 'after' => 'meta_template_field_id', + ]) + ->addIndex(['scope'], [ + 'name' => 'scope', + 'unique' => false, + ]) + ->addIndex(['uuid'], [ + 'name' => 'uuid', + 'unique' => false, + ]) + ->addIndex(['parent_id'], [ + 'name' => 'parent_id', + 'unique' => false, + ]) + ->addIndex(['field'], [ + 'name' => 'field', + 'unique' => false, + ]) + ->addIndex(['value'], [ + 'name' => 'value', + 'unique' => false, + ]) + ->addIndex(['meta_template_id'], [ + 'name' => 'meta_template_id', + 'unique' => false, + ]) + ->addIndex(['meta_template_field_id'], [ + 'name' => 'meta_template_field_id', + 'unique' => false, + ]) + ->create(); + $this->table('audit_logs', [ + 'id' => false, + 'primary_key' => ['id'], + 'engine' => 'InnoDB', + 'encoding' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'comment' => '', + 'row_format' => 'DYNAMIC', + ]) + ->addColumn('id', 'integer', [ + 'null' => false, + 'limit' => '10', + 'signed' => false, + 'identity' => 'enable', + ]) + ->addColumn('created', 'datetime', [ + 'null' => false, + 'after' => 'id', + ]) + ->addColumn('user_id', 'integer', [ + 'null' => true, + 'default' => null, + 'limit' => '10', + 'signed' => false, + 'after' => 'created', + ]) + ->addColumn('authkey_id', 'integer', [ + 'null' => true, + 'default' => null, + 'limit' => '10', + 'signed' => false, + 'after' => 'user_id', + ]) + ->addColumn('request_ip', 'varbinary', [ + 'null' => true, + 'default' => null, + 'limit' => 16, + 'after' => 'authkey_id', + ]) + ->addColumn('request_type', 'integer', [ + 'null' => false, + 'limit' => MysqlAdapter::INT_TINY, + 'after' => 'request_ip', + ]) + ->addColumn('request_id', 'string', [ + 'null' => true, + 'default' => null, + 'limit' => 191, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'request_type', + ]) + ->addColumn('request_action', 'string', [ + 'null' => false, + 'limit' => 20, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'request_id', + ]) + ->addColumn('model', 'string', [ + 'null' => false, + 'limit' => 80, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'request_action', + ]) + ->addColumn('model_id', 'integer', [ + 'null' => true, + 'default' => null, + 'limit' => '10', + 'signed' => false, + 'after' => 'model', + ]) + ->addColumn('model_title', 'text', [ + 'null' => true, + 'default' => null, + 'limit' => 65535, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'model_id', + ]) + ->addColumn('change', 'blob', [ + 'null' => true, + 'default' => null, + 'limit' => MysqlAdapter::BLOB_REGULAR, + 'after' => 'model_title', + ]) + ->addIndex(['user_id'], [ + 'name' => 'user_id', + 'unique' => false, + ]) + ->addIndex(['request_ip'], [ + 'name' => 'request_ip', + 'unique' => false, + ]) + ->addIndex(['model'], [ + 'name' => 'model', + 'unique' => false, + ]) + ->addIndex(['request_action'], [ + 'name' => 'request_action', + 'unique' => false, + ]) + ->addIndex(['model_id'], [ + 'name' => 'model_id', + 'unique' => false, + ]) + ->addIndex(['created'], [ + 'name' => 'created', + 'unique' => false, + ]) + ->create(); + $this->table('organisation_encryption_keys', [ + 'id' => false, + 'primary_key' => ['id'], + 'engine' => 'InnoDB', + 'encoding' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'comment' => '', + 'row_format' => 'DYNAMIC', + ]) + ->addColumn('id', 'integer', [ + 'null' => false, + 'limit' => '10', + 'signed' => false, + 'identity' => 'enable', + ]) + ->addColumn('organisation_id', 'integer', [ + 'null' => false, + 'limit' => '10', + 'signed' => false, + 'after' => 'id', + ]) + ->addColumn('encryption_key_id', 'integer', [ + 'null' => false, + 'limit' => '10', + 'signed' => false, + 'after' => 'organisation_id', + ]) + ->addIndex(['organisation_id'], [ + 'name' => 'organisation_id', + 'unique' => false, + ]) + ->addIndex(['encryption_key_id'], [ + 'name' => 'encryption_key_id', + 'unique' => false, + ]) + ->addForeignKey('organisation_id', 'organisations', 'id', [ + 'constraint' => 'organisation_encryption_keys_ibfk_1', + 'update' => 'RESTRICT', + 'delete' => 'RESTRICT', + ]) + ->addForeignKey('encryption_key_id', 'encryption_keys', 'id', [ + 'constraint' => 'organisation_encryption_keys_ibfk_2', + 'update' => 'RESTRICT', + 'delete' => 'RESTRICT', + ]) + ->create(); + $this->table('users', [ + 'id' => false, + 'primary_key' => ['id'], + 'engine' => 'InnoDB', + 'encoding' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'comment' => '', + 'row_format' => 'DYNAMIC', + ]) + ->addColumn('id', 'integer', [ + 'null' => false, + 'limit' => '10', + 'signed' => false, + 'identity' => 'enable', + ]) + ->addColumn('uuid', 'string', [ + 'null' => true, + 'default' => null, + 'limit' => 40, + 'collation' => 'ascii_general_ci', + 'encoding' => 'ascii', + 'after' => 'id', + ]) + ->addColumn('username', 'string', [ + 'null' => true, + 'default' => null, + 'limit' => 191, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'uuid', + ]) + ->addColumn('password', 'string', [ + 'null' => true, + 'default' => null, + 'limit' => 191, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'username', + ]) + ->addColumn('role_id', 'integer', [ + 'null' => false, + 'limit' => MysqlAdapter::INT_REGULAR, + 'signed' => false, + 'after' => 'password', + ]) + ->addColumn('individual_id', 'integer', [ + 'null' => false, + 'limit' => MysqlAdapter::INT_REGULAR, + 'signed' => false, + 'after' => 'role_id', + ]) + ->addColumn('disabled', 'boolean', [ + 'null' => true, + 'default' => '0', + 'limit' => MysqlAdapter::INT_TINY, + 'after' => 'individual_id', + ]) + ->addIndex(['uuid'], [ + 'name' => 'uuid', + 'unique' => false, + ]) + ->addIndex(['role_id'], [ + 'name' => 'role_id', + 'unique' => false, + ]) + ->addIndex(['individual_id'], [ + 'name' => 'individual_id', + 'unique' => false, + ]) + ->addForeignKey('role_id', 'roles', 'id', [ + 'constraint' => 'users_ibfk_1', + 'update' => 'RESTRICT', + 'delete' => 'RESTRICT', + ]) + ->addForeignKey('individual_id', 'individuals', 'id', [ + 'constraint' => 'users_ibfk_2', + 'update' => 'RESTRICT', + 'delete' => 'RESTRICT', + ]) + ->create(); + $this->table('roles', [ + 'id' => false, + 'primary_key' => ['id'], + 'engine' => 'InnoDB', + 'encoding' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'comment' => '', + 'row_format' => 'DYNAMIC', + ]) + ->addColumn('id', 'integer', [ + 'null' => false, + 'limit' => '10', + 'signed' => false, + 'identity' => 'enable', + ]) + ->addColumn('uuid', 'string', [ + 'null' => true, + 'default' => null, + 'limit' => 40, + 'collation' => 'ascii_general_ci', + 'encoding' => 'ascii', + 'after' => 'id', + ]) + ->addColumn('name', 'string', [ + 'null' => false, + 'limit' => 191, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'uuid', + ]) + ->addColumn('is_default', 'boolean', [ + 'null' => true, + 'default' => null, + 'limit' => MysqlAdapter::INT_TINY, + 'after' => 'name', + ]) + ->addColumn('perm_admin', 'boolean', [ + 'null' => true, + 'default' => null, + 'limit' => MysqlAdapter::INT_TINY, + 'after' => 'is_default', + ]) + ->addIndex(['name'], [ + 'name' => 'name', + 'unique' => false, + ]) + ->addIndex(['uuid'], [ + 'name' => 'uuid', + 'unique' => false, + ]) + ->create(); + $this->table('tags', [ + 'id' => false, + 'primary_key' => ['id'], + 'engine' => 'InnoDB', + 'encoding' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'comment' => '', + 'row_format' => 'DYNAMIC', + ]) + ->addColumn('id', 'integer', [ + 'null' => false, + 'limit' => '10', + 'signed' => false, + 'identity' => 'enable', + ]) + ->addColumn('name', 'string', [ + 'null' => false, + 'limit' => 191, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'id', + ]) + ->addColumn('description', 'text', [ + 'null' => true, + 'default' => null, + 'limit' => 65535, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'name', + ]) + ->addColumn('colour', 'string', [ + 'null' => false, + 'limit' => 6, + 'collation' => 'ascii_general_ci', + 'encoding' => 'ascii', + 'after' => 'description', + ]) + ->addIndex(['name'], [ + 'name' => 'name', + 'unique' => false, + ]) + ->create(); + $this->table('individual_encryption_keys', [ + 'id' => false, + 'primary_key' => ['id'], + 'engine' => 'InnoDB', + 'encoding' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'comment' => '', + 'row_format' => 'DYNAMIC', + ]) + ->addColumn('id', 'integer', [ + 'null' => false, + 'limit' => '10', + 'signed' => false, + 'identity' => 'enable', + ]) + ->addColumn('individual_id', 'integer', [ + 'null' => false, + 'limit' => '10', + 'signed' => false, + 'after' => 'id', + ]) + ->addColumn('encryption_key_id', 'integer', [ + 'null' => false, + 'limit' => '10', + 'signed' => false, + 'after' => 'individual_id', + ]) + ->addIndex(['individual_id'], [ + 'name' => 'individual_id', + 'unique' => false, + ]) + ->addIndex(['encryption_key_id'], [ + 'name' => 'encryption_key_id', + 'unique' => false, + ]) + ->addForeignKey('individual_id', 'individuals', 'id', [ + 'constraint' => 'individual_encryption_keys_ibfk_1', + 'update' => 'RESTRICT', + 'delete' => 'RESTRICT', + ]) + ->addForeignKey('encryption_key_id', 'encryption_keys', 'id', [ + 'constraint' => 'individual_encryption_keys_ibfk_2', + 'update' => 'RESTRICT', + 'delete' => 'RESTRICT', + ]) + ->addForeignKey('individual_id', 'individuals', 'id', [ + 'constraint' => 'individual_encryption_keys_ibfk_3', + 'update' => 'RESTRICT', + 'delete' => 'RESTRICT', + ]) + ->addForeignKey('encryption_key_id', 'encryption_keys', 'id', [ + 'constraint' => 'individual_encryption_keys_ibfk_4', + 'update' => 'RESTRICT', + 'delete' => 'RESTRICT', + ]) + ->addForeignKey('individual_id', 'individuals', 'id', [ + 'constraint' => 'individual_encryption_keys_ibfk_5', + 'update' => 'RESTRICT', + 'delete' => 'RESTRICT', + ]) + ->addForeignKey('encryption_key_id', 'encryption_keys', 'id', [ + 'constraint' => 'individual_encryption_keys_ibfk_6', + 'update' => 'RESTRICT', + 'delete' => 'RESTRICT', + ]) + ->create(); + $this->table('meta_template_fields', [ + 'id' => false, + 'primary_key' => ['id'], + 'engine' => 'InnoDB', + 'encoding' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'comment' => '', + 'row_format' => 'DYNAMIC', + ]) + ->addColumn('id', 'integer', [ + 'null' => false, + 'limit' => '10', + 'signed' => false, + 'identity' => 'enable', + ]) + ->addColumn('field', 'string', [ + 'null' => false, + 'limit' => 191, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'id', + ]) + ->addColumn('type', 'string', [ + 'null' => false, + 'limit' => 191, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'field', + ]) + ->addColumn('meta_template_id', 'integer', [ + 'null' => false, + 'limit' => '10', + 'signed' => false, + 'after' => 'type', + ]) + ->addColumn('regex', 'text', [ + 'null' => true, + 'default' => null, + 'limit' => 65535, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'meta_template_id', + ]) + ->addColumn('multiple', 'boolean', [ + 'null' => true, + 'default' => '0', + 'limit' => MysqlAdapter::INT_TINY, + 'after' => 'regex', + ]) + ->addColumn('enabled', 'boolean', [ + 'null' => true, + 'default' => '0', + 'limit' => MysqlAdapter::INT_TINY, + 'after' => 'multiple', + ]) + ->addIndex(['meta_template_id'], [ + 'name' => 'meta_template_id', + 'unique' => false, + ]) + ->addIndex(['field'], [ + 'name' => 'field', + 'unique' => false, + ]) + ->addIndex(['type'], [ + 'name' => 'type', + 'unique' => false, + ]) + ->addForeignKey('meta_template_id', 'meta_templates', 'id', [ + 'constraint' => 'meta_template_id', + 'update' => 'RESTRICT', + 'delete' => 'RESTRICT', + ]) + ->create(); + $this->table('auth_keys', [ + 'id' => false, + 'primary_key' => ['id'], + 'engine' => 'InnoDB', + 'encoding' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'comment' => '', + 'row_format' => 'DYNAMIC', + ]) + ->addColumn('id', 'integer', [ + 'null' => false, + 'limit' => '10', + 'signed' => false, + 'identity' => 'enable', + ]) + ->addColumn('uuid', 'string', [ + 'null' => false, + 'limit' => 40, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'id', + ]) + ->addColumn('authkey', 'string', [ + 'null' => true, + 'default' => null, + 'limit' => 72, + 'collation' => 'ascii_general_ci', + 'encoding' => 'ascii', + 'after' => 'uuid', + ]) + ->addColumn('authkey_start', 'string', [ + 'null' => true, + 'default' => null, + 'limit' => 4, + 'collation' => 'ascii_general_ci', + 'encoding' => 'ascii', + 'after' => 'authkey', + ]) + ->addColumn('authkey_end', 'string', [ + 'null' => true, + 'default' => null, + 'limit' => 4, + 'collation' => 'ascii_general_ci', + 'encoding' => 'ascii', + 'after' => 'authkey_start', + ]) + ->addColumn('created', 'integer', [ + 'null' => false, + 'limit' => '10', + 'signed' => false, + 'after' => 'authkey_end', + ]) + ->addColumn('expiration', 'integer', [ + 'null' => false, + 'limit' => '10', + 'signed' => false, + 'after' => 'created', + ]) + ->addColumn('user_id', 'integer', [ + 'null' => false, + 'limit' => '10', + 'signed' => false, + 'after' => 'expiration', + ]) + ->addColumn('comment', 'text', [ + 'null' => true, + 'default' => null, + 'limit' => 65535, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'user_id', + ]) + ->addIndex(['authkey_start'], [ + 'name' => 'authkey_start', + 'unique' => false, + ]) + ->addIndex(['authkey_end'], [ + 'name' => 'authkey_end', + 'unique' => false, + ]) + ->addIndex(['created'], [ + 'name' => 'created', + 'unique' => false, + ]) + ->addIndex(['expiration'], [ + 'name' => 'expiration', + 'unique' => false, + ]) + ->addIndex(['user_id'], [ + 'name' => 'user_id', + 'unique' => false, + ]) + ->create(); + $this->table('local_tools', [ + 'id' => false, + 'primary_key' => ['id'], + 'engine' => 'InnoDB', + 'encoding' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'comment' => '', + 'row_format' => 'DYNAMIC', + ]) + ->addColumn('id', 'integer', [ + 'null' => false, + 'limit' => '10', + 'signed' => false, + 'identity' => 'enable', + ]) + ->addColumn('name', 'string', [ + 'null' => false, + 'limit' => 191, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'id', + ]) + ->addColumn('connector', 'string', [ + 'null' => false, + 'limit' => 191, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'name', + ]) + ->addColumn('settings', 'text', [ + 'null' => true, + 'default' => null, + 'limit' => 65535, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'connector', + ]) + ->addColumn('exposed', 'boolean', [ + 'null' => false, + 'limit' => MysqlAdapter::INT_TINY, + 'after' => 'settings', + ]) + ->addColumn('description', 'text', [ + 'null' => true, + 'default' => null, + 'limit' => 65535, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'exposed', + ]) + ->addIndex(['name'], [ + 'name' => 'name', + 'unique' => false, + ]) + ->addIndex(['connector'], [ + 'name' => 'connector', + 'unique' => false, + ]) + ->create(); + $this->table('alignments', [ + 'id' => false, + 'primary_key' => ['id'], + 'engine' => 'InnoDB', + 'encoding' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'comment' => '', + 'row_format' => 'DYNAMIC', + ]) + ->addColumn('id', 'integer', [ + 'null' => false, + 'limit' => '10', + 'signed' => false, + 'identity' => 'enable', + ]) + ->addColumn('individual_id', 'integer', [ + 'null' => false, + 'limit' => '10', + 'signed' => false, + 'after' => 'id', + ]) + ->addColumn('organisation_id', 'integer', [ + 'null' => false, + 'limit' => '10', + 'signed' => false, + 'after' => 'individual_id', + ]) + ->addColumn('type', 'string', [ + 'null' => true, + 'default' => 'member', + 'limit' => 191, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + 'after' => 'organisation_id', + ]) + ->addIndex(['individual_id'], [ + 'name' => 'individual_id', + 'unique' => false, + ]) + ->addIndex(['organisation_id'], [ + 'name' => 'organisation_id', + 'unique' => false, + ]) + ->addForeignKey('individual_id', 'individuals', 'id', [ + 'constraint' => 'alignments_ibfk_1', + 'update' => 'RESTRICT', + 'delete' => 'RESTRICT', + ]) + ->addForeignKey('organisation_id', 'organisations', 'id', [ + 'constraint' => 'alignments_ibfk_2', + 'update' => 'RESTRICT', + 'delete' => 'RESTRICT', + ]) + ->create(); + $this->table('sgo', [ + 'id' => false, + 'primary_key' => ['id'], + 'engine' => 'InnoDB', + 'encoding' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'comment' => '', + 'row_format' => 'DYNAMIC', + ]) + ->addColumn('id', 'integer', [ + 'null' => false, + 'limit' => '10', + 'signed' => false, + 'identity' => 'enable', + ]) + ->addColumn('sharing_group_id', 'integer', [ + 'null' => false, + 'limit' => '10', + 'signed' => false, + 'after' => 'id', + ]) + ->addColumn('organisation_id', 'integer', [ + 'null' => false, + 'limit' => '10', + 'signed' => false, + 'after' => 'sharing_group_id', + ]) + ->addColumn('deleted', 'boolean', [ + 'null' => true, + 'default' => '0', + 'limit' => MysqlAdapter::INT_TINY, + 'after' => 'organisation_id', + ]) + ->addIndex(['sharing_group_id'], [ + 'name' => 'sharing_group_id', + 'unique' => false, + ]) + ->addIndex(['organisation_id'], [ + 'name' => 'organisation_id', + 'unique' => false, + ]) + ->create(); + $this->execute('SET unique_checks=1; SET foreign_key_checks=1;'); + } +} diff --git a/config/Migrations/20211117135403_audit_logs.php b/config/Migrations/20211117135403_audit_logs.php new file mode 100644 index 0000000..c44847b --- /dev/null +++ b/config/Migrations/20211117135403_audit_logs.php @@ -0,0 +1,96 @@ +hasTable('audit_logs'); + if (!$exists) { + $table = $this->table('audit_logs', [ + 'signed' => false, + 'collation' => 'utf8mb4_unicode_ci' + ]); + $table + ->addColumn('id', 'integer', [ + 'autoIncrement' => true, + 'limit' => 10, + 'signed' => false, + ]) + ->addPrimaryKey('id') + ->addColumn('user_id', 'integer', [ + 'default' => null, + 'null' => true, + 'signed' => false, + 'length' => 10 + ]) + ->addColumn('authkey_id', 'integer', [ + 'default' => null, + 'null' => true, + 'signed' => false, + 'length' => 10 + ]) + ->addColumn('request_ip', 'varbinary', [ + 'default' => null, + 'null' => true, + 'length' => 16 + ]) + ->addColumn('request_type', 'boolean', [ + 'null' => false + ]) + ->addColumn('request_id', 'integer', [ + 'default' => null, + 'null' => true, + 'signed' => false, + 'length' => 10 + ]) + ->addColumn('request_action', 'string', [ + 'null' => false, + 'length' => 20 + ]) + ->addColumn('model', 'string', [ + 'null' => false, + 'length' => 80 + ]) + ->addColumn('model_id', 'integer', [ + 'default' => null, + 'null' => true, + 'signed' => false, + 'length' => 10 + ]) + ->addColumn('model_title', 'text', [ + 'default' => null, + 'null' => true + ]) + ->addColumn('change', 'blob', [ + ]) + ->addColumn('created', 'datetime', [ + 'default' => null, + 'null' => false, + ]) + ->addIndex('user_id') + ->addIndex('request_ip') + ->addIndex('model') + ->addIndex('model_id') + ->addIndex('request_action') + ->addIndex('created'); + $table->create(); + } + } +} diff --git a/config/Migrations/20211123152707_user_org.php b/config/Migrations/20211123152707_user_org.php new file mode 100644 index 0000000..47d8a5b --- /dev/null +++ b/config/Migrations/20211123152707_user_org.php @@ -0,0 +1,43 @@ +table('users')->hasColumn('organisation_id'); + if (!$exists) { + $this->table('users') + ->addColumn('organisation_id', 'integer', [ + 'default' => null, + 'null' => true, + 'signed' => false, + 'length' => 10 + ]) + ->addIndex('organisation_id') + ->update(); + } + $q1 = $this->getQueryBuilder(); + $org_id = $q1->select(['min(id)'])->from('organisations')->execute()->fetchAll()[0][0]; + if (!empty($org_id)) { + $q2 = $this->getQueryBuilder(); + $q2->update('users') + ->set('organisation_id', $org_id) + ->where(['organisation_id IS NULL']) + ->execute(); + } + } +} diff --git a/config/Migrations/20211124234433_audit_changed.php b/config/Migrations/20211124234433_audit_changed.php new file mode 100644 index 0000000..607c630 --- /dev/null +++ b/config/Migrations/20211124234433_audit_changed.php @@ -0,0 +1,28 @@ +table('audit_logs')->hasColumn('change'); + if ($exists) { + $this->table('audit_logs') + ->renameColumn('change', 'changed') + ->update(); + } + } +} diff --git a/config/Migrations/schema-dump-default.lock b/config/Migrations/schema-dump-default.lock index 291060c..8c9385c 100644 Binary files a/config/Migrations/schema-dump-default.lock and b/config/Migrations/schema-dump-default.lock differ diff --git a/config/app_local.example.php b/config/app_local.example.php index 637128c..1ec0f4a 100644 --- a/config/app_local.example.php +++ b/config/app_local.example.php @@ -5,6 +5,16 @@ * Note: It is not recommended to commit files with credentials such as app_local.php * into source code version control. */ + +// set the baseurl here if you want to set it manually +$baseurl = env('CEREBRATE_BASEURL', false); + + +// Do not modify the this block +$temp = parse_url($baseurl); +$base = empty($temp['path']) ? false : $temp['path']; +// end of block + return [ /* * Debug Level: @@ -89,4 +99,8 @@ return [ 'url' => env('EMAIL_TRANSPORT_DEFAULT_URL', null), ], ], + 'Cerebrate' => [ + 'open' => [], + 'dark' => 0 + ] ]; diff --git a/debian/install b/debian/install index 2ee55e9..12d7cc8 100755 --- a/debian/install +++ b/debian/install @@ -7,4 +7,3 @@ webroot /usr/share/php-cerebrate config /usr/share/php-cerebrate debian/cerebrate.local.conf /etc/apache2/sites-available/ debian/config.php /etc/cerebrate/ -INSTALL/mysql.sql => /usr/share/dbconfig-common/data/php-cerebrate/install/mysql diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..6e0cb03 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,79 @@ +ARG COMPOSER_VERSION +ARG PHP_VERSION +ARG DEBIAN_RELEASE + +FROM php:${PHP_VERSION}-apache-${DEBIAN_RELEASE} + +# we need some extra libs to be installed in the runtime +RUN apt-get update && \ + apt-get install -y --no-install-recommends curl git zip unzip && \ + apt-get install -y --no-install-recommends libicu-dev libxml2-dev && \ + docker-php-ext-install intl pdo pdo_mysql mysqli simplexml && \ + apt-get remove -y --purge libicu-dev libxml2-dev && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +COPY composer.json composer.json + +# install composer as root +ARG COMPOSER_VERSION +RUN curl -sL https://getcomposer.org/installer | \ + php -- --install-dir=/usr/bin/ --filename=composer --version=${COMPOSER_VERSION} + +# switch back to unprivileged user for composer install +USER www-data + +RUN composer install \ + --no-interaction \ + --no-plugins \ + --no-scripts \ + --prefer-dist + +# web server configuration +USER root + +# allow .htaccess overrides and push them +RUN a2enmod rewrite +RUN sed -i -r '/DocumentRoot/a \\t\n\t\tAllowOverride all\n\t' /etc/apache2/sites-available/000-default.conf +COPY --chown=www-data docker/etc/DocumentRoot.htaccess /var/www/html/.htaccess +COPY --chown=www-data docker/etc/webroot.htaccess /var/www/html/webroot/.htaccess + +# passing environment variables through apache +RUN a2enmod env +RUN echo 'PassEnv CEREBRATE_DB_HOST' >> /etc/apache2/conf-enabled/environment.conf +RUN echo 'PassEnv CEREBRATE_DB_NAME' >> /etc/apache2/conf-enabled/environment.conf +RUN echo 'PassEnv CEREBRATE_DB_PASSWORD' >> /etc/apache2/conf-enabled/environment.conf +RUN echo 'PassEnv CEREBRATE_DB_PORT' >> /etc/apache2/conf-enabled/environment.conf +RUN echo 'PassEnv CEREBRATE_DB_SCHEMA' >> /etc/apache2/conf-enabled/environment.conf +RUN echo 'PassEnv CEREBRATE_DB_USERNAME' >> /etc/apache2/conf-enabled/environment.conf +RUN echo 'PassEnv CEREBRATE_EMAIL_HOST' >> /etc/apache2/conf-enabled/environment.conf +RUN echo 'PassEnv CEREBRATE_EMAIL_PASSWORD' >> /etc/apache2/conf-enabled/environment.conf +RUN echo 'PassEnv CEREBRATE_EMAIL_PORT' >> /etc/apache2/conf-enabled/environment.conf +RUN echo 'PassEnv CEREBRATE_EMAIL_TLS' >> /etc/apache2/conf-enabled/environment.conf +RUN echo 'PassEnv CEREBRATE_EMAIL_USERNAME' >> /etc/apache2/conf-enabled/environment.conf +RUN echo 'PassEnv CEREBRATE_SECURITY_SALT' >> /etc/apache2/conf-enabled/environment.conf + +# entrypoint +COPY docker/entrypoint.sh /entrypoint.sh +RUN chmod 755 /entrypoint.sh + +# copy actual codebase +COPY --chown=www-data . /var/www/html + +# last checks with unprivileged user +USER www-data + +# CakePHP seems to not handle very well externally installed components +# this will chown/chmod/symlink all in place for its own good +RUN composer install --no-interaction + +# app config override making use of environment variables +COPY --chown=www-data docker/etc/app_local.php /var/www/html/config/app_local.php +# version 1.0 addition requires a config/config.json file +# can still be overriden by a docker volume +RUN cp -a /var/www/html/config/config.example.json /var/www/html/config/config.json + +# also can be overridin by a docker volume +RUN mkdir -p /var/www/html/logs + +ENTRYPOINT [ "/entrypoint.sh" ] diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..9bf0154 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,30 @@ +# Actual data and volumes + +The actual database will be located in `./run/database` exposed with the +following volume `- ./run/database:/var/lib/mysql` + +Application logs (CakePHP / Cerebrate) will be stored in `./run/logs`, +volume `- ./run/logs:/var/www/html/logs` + +You're free to change those parameters if you're using Swarm, Kubernetes or +your favorite config management tool to deploy this stack + +# Building yourself + +You can create the following Makefile in basedir of this repository +and issue `make image` + +``` +COMPOSER_VERSION?=2.1.5 +PHP_VERSION?=7.4 +DEBIAN_RELEASE?=buster +IMAGE_NAME?=cerebrate:latest + +image: + docker build -t $(IMAGE_NAME) \ + -f docker/Dockerfile \ + --build-arg COMPOSER_VERSION=$(COMPOSER_VERSION) \ + --build-arg PHP_VERSION=$(PHP_VERSION) \ + --build-arg DEBIAN_RELEASE=$(DEBIAN_RELEASE) \ + . +``` diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..821ab5f --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,29 @@ +version: "3" +services: + database: + image: mariadb:10.6 + restart: always + volumes: + - ./run/database:/var/lib/mysql + environment: + MARIADB_RANDOM_ROOT_PASSWORD: "yes" + MYSQL_DATABASE: "cerebrate" + MYSQL_USER: "cerebrate" + MYSQL_PASSWORD: "etarberec" + www: + image: ghcr.io/cerebrate-project/cerebrate:main + ports: + - "8080:80" + volumes: + - ./run/logs:/var/www/html/logs + - ./wait-for-it.sh:/usr/local/bin/wait-for-it.sh:ro + entrypoint: wait-for-it.sh -t 0 -h database -p 3306 -- /entrypoint.sh + environment: + DEBUG: "true" + CEREBRATE_DB_USERNAME: "cerebrate" + CEREBRATE_DB_PASSWORD: "etarberec" + CEREBRATE_DB_NAME: "cerebrate" + CEREBRATE_DB_HOST: database + CEREBRATE_SECURITY_SALT: supersecret + depends_on: + - database diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100755 index 0000000..e4c1bfd --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,28 @@ +#!/bin/sh + +set -e + + +run_all_migrations() { + ./bin/cake migrations migrate + ./bin/cake migrations migrate -p tags + ./bin/cake migrations migrate -p ADmad/SocialAuth +} + +delete_model_cache() { + echo >&2 "Deleting cakephp cache..." + rm -rf ./tmp/cache/models/* + rm -rf ./tmp/cache/persistent/* +} + +delete_model_cache + +# waiting for DB to come up +for try in 1 2 3 4 5 6; do + echo >&2 "migration - attempt $try" + run_all_migrations && break || true + sleep 5 + [ "$try" = "6" ] && exit 1 +done + +exec /usr/local/bin/apache2-foreground "$@" diff --git a/docker/etc/DocumentRoot.htaccess b/docker/etc/DocumentRoot.htaccess new file mode 100644 index 0000000..ef9940b --- /dev/null +++ b/docker/etc/DocumentRoot.htaccess @@ -0,0 +1,3 @@ +RewriteEngine on +RewriteRule ^$ webroot/ [L] +RewriteRule (.*) webroot/$1 [L] diff --git a/docker/etc/app_local.php b/docker/etc/app_local.php new file mode 100644 index 0000000..ac1d212 --- /dev/null +++ b/docker/etc/app_local.php @@ -0,0 +1,44 @@ + env('CEREBRATE_DB_USERNAME', 'cerebrate'), + 'password' => env('CEREBRATE_DB_PASSWORD', ''), + 'host' => env('CEREBRATE_DB_HOST', 'localhost'), + 'database' => env('CEREBRATE_DB_NAME', 'cerebrate'), +]; + +// non-default port can be set on demand - otherwise the DB driver will choose the default +if (!empty(env('CEREBRATE_DB_PORT'))) { + $db['port'] = env('CEREBRATE_DB_PORT'); +} + +// If not using the default 'public' schema with the PostgreSQL driver set it here. +if (!empty(env('CEREBRATE_DB_SCHEMA'))) { + $db['schema'] = env('CEREBRATE_DB_SCHEMA'); +} + +return [ + 'debug' => filter_var(env('DEBUG', false), FILTER_VALIDATE_BOOLEAN), + + 'Security' => [ + 'salt' => env('CEREBRATE_SECURITY_SALT'), + ], + + 'Datasources' => [ + 'default' => $db, + ], + + 'EmailTransport' => [ + 'default' => [ + // host could be ssl://smtp.gmail.com then set port to 465 + 'host' => env('CEREBRATE_EMAIL_HOST', 'localhost'), + 'port' => env('CEREBRATE_EMAIL_PORT', 25), + 'username' => env('CEREBRATE_EMAIL_USERNAME', null), + 'password' => env('CEREBRATE_EMAIL_PASSWORD', null), + 'tls' => env('CEREBRATE_EMAIL_TLS', null) + ], + ], + 'Cerebrate' => [ + 'open' => [], + 'dark' => 0 + ] +]; diff --git a/docker/etc/webroot.htaccess b/docker/etc/webroot.htaccess new file mode 100644 index 0000000..879f805 --- /dev/null +++ b/docker/etc/webroot.htaccess @@ -0,0 +1,3 @@ +RewriteEngine On +RewriteCond %{REQUEST_FILENAME} !-f +RewriteRule ^ index.php [L] diff --git a/docker/wait-for-it.sh b/docker/wait-for-it.sh new file mode 100755 index 0000000..65b7f1f --- /dev/null +++ b/docker/wait-for-it.sh @@ -0,0 +1,186 @@ +#!/usr/bin/env bash +# Use this script to test if a given TCP host/port are available + +# The MIT License (MIT) +# Copyright (c) 2016 Giles Hall +# Source: https://github.com/vishnubob/wait-for-it + +WAITFORIT_cmdname=${0##*/} + +echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } + +usage() +{ + cat << USAGE >&2 +Usage: + $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args] + -h HOST | --host=HOST Host or IP under test + -p PORT | --port=PORT TCP port under test + Alternatively, you specify the host and port as host:port + -s | --strict Only execute subcommand if the test succeeds + -q | --quiet Don't output any status messages + -t TIMEOUT | --timeout=TIMEOUT + Timeout in seconds, zero for no timeout + -- COMMAND ARGS Execute command with args after the test finishes +USAGE + exit 1 +} + +wait_for() +{ + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + else + echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout" + fi + WAITFORIT_start_ts=$(date +%s) + while : + do + if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then + nc -z $WAITFORIT_HOST $WAITFORIT_PORT + WAITFORIT_result=$? + else + (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1 + WAITFORIT_result=$? + fi + if [[ $WAITFORIT_result -eq 0 ]]; then + WAITFORIT_end_ts=$(date +%s) + echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" + break + fi + sleep 1 + done + return $WAITFORIT_result +} + +wait_for_wrapper() +{ + # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 + if [[ $WAITFORIT_QUIET -eq 1 ]]; then + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + else + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + fi + WAITFORIT_PID=$! + trap "kill -INT -$WAITFORIT_PID" INT + wait $WAITFORIT_PID + WAITFORIT_RESULT=$? + if [[ $WAITFORIT_RESULT -ne 0 ]]; then + echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + fi + return $WAITFORIT_RESULT +} + +# process arguments +while [[ $# -gt 0 ]] +do + case "$1" in + *:* ) + WAITFORIT_hostport=(${1//:/ }) + WAITFORIT_HOST=${WAITFORIT_hostport[0]} + WAITFORIT_PORT=${WAITFORIT_hostport[1]} + shift 1 + ;; + --child) + WAITFORIT_CHILD=1 + shift 1 + ;; + -q | --quiet) + WAITFORIT_QUIET=1 + shift 1 + ;; + -s | --strict) + WAITFORIT_STRICT=1 + shift 1 + ;; + -h) + WAITFORIT_HOST="$2" + if [[ $WAITFORIT_HOST == "" ]]; then break; fi + shift 2 + ;; + --host=*) + WAITFORIT_HOST="${1#*=}" + shift 1 + ;; + -p) + WAITFORIT_PORT="$2" + if [[ $WAITFORIT_PORT == "" ]]; then break; fi + shift 2 + ;; + --port=*) + WAITFORIT_PORT="${1#*=}" + shift 1 + ;; + -t) + WAITFORIT_TIMEOUT="$2" + if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi + shift 2 + ;; + --timeout=*) + WAITFORIT_TIMEOUT="${1#*=}" + shift 1 + ;; + --) + shift + WAITFORIT_CLI=("$@") + break + ;; + --help) + usage + ;; + *) + echoerr "Unknown argument: $1" + usage + ;; + esac +done + +if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then + echoerr "Error: you need to provide a host and port to test." + usage +fi + +WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15} +WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} +WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} +WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} + +# Check to see if timeout is from busybox? +WAITFORIT_TIMEOUT_PATH=$(type -p timeout) +WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) + +WAITFORIT_BUSYTIMEFLAG="" +if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then + WAITFORIT_ISBUSY=1 + # Check if busybox timeout uses -t flag + # (recent Alpine versions don't support -t anymore) + if timeout &>/dev/stdout | grep -q -e '-t '; then + WAITFORIT_BUSYTIMEFLAG="-t" + fi +else + WAITFORIT_ISBUSY=0 +fi + +if [[ $WAITFORIT_CHILD -gt 0 ]]; then + wait_for + WAITFORIT_RESULT=$? + exit $WAITFORIT_RESULT +else + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + wait_for_wrapper + WAITFORIT_RESULT=$? + else + wait_for + WAITFORIT_RESULT=$? + fi +fi + +if [[ $WAITFORIT_CLI != "" ]]; then + if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then + echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess" + exit $WAITFORIT_RESULT + fi + exec "${WAITFORIT_CLI[@]}" +else + exit $WAITFORIT_RESULT +fi diff --git a/documentation/cerebrate-synchornization.pdf b/documentation/cerebrate-synchornization.pdf new file mode 100644 index 0000000..946306d Binary files /dev/null and b/documentation/cerebrate-synchornization.pdf differ diff --git a/documentation/local-tools-inter-connection.pdf b/documentation/local-tools-inter-connection.pdf new file mode 100644 index 0000000..5014c98 Binary files /dev/null and b/documentation/local-tools-inter-connection.pdf differ diff --git a/documentation/prerequisites.md b/documentation/prerequisites.md new file mode 100644 index 0000000..7b020ad --- /dev/null +++ b/documentation/prerequisites.md @@ -0,0 +1,17 @@ +# Prerequisites based on usecases + +This document list the requirements that have to be met in order to perform the desired usecase. + +## Connect a local tool to cerebrate +- **Networking**: The *cerebrate* application must be able to contact the local tool service. That means the address and the port of the local tool must be reachable by *cerebrate*. +- **User permissions**: Depends on the actions performed by Cerebrate on the local tool. + - Example: For a standard MISP configuration, a simple user with the `user` role is enough for Cerebrate to pass the health check. + +## Conect two cerebrate instances together +- **Networking**: The two *cerebrate* applications must be able to contact each others. That means the address and the port of both tools must be reachable by the other one. +- **User permissions**: No specific role or set of permission is required. Any user role can be used. + +## Connect two local tools through cerebrate +- **Networking**: The two *cerebrate* applications must be able to contact each others. That means the address and the port of both tools must be reachable by the other one. This also applies to both the local tools. +- **User permissions**: Depends on the actions performed by Cerebrate on the local tool. + - Example: For a standard MISP configuration, in order to have two instance connected, the API key used by *cebrate* to orchestrate the inter-connection must belong to a user having the `site-admin` permission flag. This is essential as only the `site-admin` permission allows to create synchronisation links between MISP instances. \ No newline at end of file diff --git a/logs/.gitkeep b/logs/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/logs/.gitkeep @@ -0,0 +1 @@ + diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 7107122..403e1e5 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,13 +1,12 @@ - + - - + + + + + + @@ -15,17 +14,17 @@ tests/TestCase/ - + + ./tests/TestCase/Controller + + + ./tests/TestCase/Api + - - - - - - - - + + + @@ -37,4 +36,4 @@ - + \ No newline at end of file diff --git a/plugins/Tags/config/Migrations/20210831121348_TagSystem.php b/plugins/Tags/config/Migrations/20210831121348_TagSystem.php index bb2e412..98492a3 100644 --- a/plugins/Tags/config/Migrations/20210831121348_TagSystem.php +++ b/plugins/Tags/config/Migrations/20210831121348_TagSystem.php @@ -10,22 +10,22 @@ class TagSystem extends AbstractMigration $tags = $this->table('tags_tags'); $tags->addColumn('namespace', 'string', [ 'default' => null, - 'limit' => 255, + 'limit' => 191, 'null' => true, ]) ->addColumn('predicate', 'string', [ 'default' => null, - 'limit' => 255, + 'limit' => 191, 'null' => true, ]) ->addColumn('value', 'string', [ 'default' => null, - 'limit' => 255, + 'limit' => 191, 'null' => true, ]) ->addColumn('name', 'string', [ 'default' => null, - 'limit' => 255, + 'limit' => 191, 'null' => false, ]) ->addColumn('colour', 'string', [ @@ -66,7 +66,7 @@ class TagSystem extends AbstractMigration ]) ->addColumn('fk_model', 'string', [ 'default' => null, - 'limit' => 255, + 'limit' => 191, 'null' => false, 'comment' => 'The model name of the entity being tagged' ]) @@ -86,4 +86,4 @@ class TagSystem extends AbstractMigration $tagged->addIndex(['tag_id', 'fk_id', 'fk_model'], ['unique' => true]) ->update(); } -} \ No newline at end of file +} diff --git a/plugins/Tags/src/Model/Behavior/TagBehavior.php b/plugins/Tags/src/Model/Behavior/TagBehavior.php index b27ded9..5772d82 100644 --- a/plugins/Tags/src/Model/Behavior/TagBehavior.php +++ b/plugins/Tags/src/Model/Behavior/TagBehavior.php @@ -46,10 +46,10 @@ class TagBehavior extends Behavior $config = $this->getConfig(); $tagsAssoc = $config['tagsAssoc']; $taggedAssoc = $config['taggedAssoc']; - + $table = $this->_table; $tableAlias = $this->_table->getAlias(); - + $assocConditions = ['Tagged.fk_model' => $tableAlias]; if (!$table->hasAssociation('Tagged')) { @@ -114,7 +114,6 @@ class TagBehavior extends Behavior $property = $this->getConfig('tagsAssoc.propertyName'); $options['accessibleFields'][$property] = true; $options['associated']['Tags']['accessibleFields']['id'] = true; - if (isset($data['tags'])) { if (!empty($data['tags'])) { $data[$property] = $this->normalizeTags($data['tags']); @@ -131,7 +130,6 @@ class TagBehavior extends Behavior if (!$tag->isNew()) { continue; } - $existingTag = $this->getExistingTag($tag->name); if (!$existingTag) { continue; @@ -176,15 +174,14 @@ class TagBehavior extends Behavior $result[] = array_merge($common, ['id' => $existingTag->id]); continue; } - $result[] = array_merge( $common, [ 'name' => $tagIdentifier, + 'colour' => '#924da6' ] ); } - return $result; } @@ -312,7 +309,7 @@ class TagBehavior extends Behavior $key = 'Tags.' . $finderField; $taggedAlias = 'Tagged'; $foreignKey = $this->getConfig('tagsAssoc.foreignKey'); - + if (!empty($filterValue['AND'])) { $subQuery = $this->buildQuerySnippet($filterValue['AND'], $finderField, $OperatorAND); $modelAlias = $this->_table->getAlias(); @@ -352,4 +349,4 @@ class TagBehavior extends Behavior return $query; } -} \ No newline at end of file +} diff --git a/src/Application.php b/src/Application.php index a63a6ce..0bf89c9 100644 --- a/src/Application.php +++ b/src/Application.php @@ -118,6 +118,7 @@ class Application extends BaseApplication implements AuthenticationServiceProvid 'collectionFactory' => null, 'logErrors' => true, ])); + \SocialConnect\JWX\JWT::$screw = Configure::check('keycloak.screw') ? Configure::read('keycloak.screw') : 0; } $middlewareQueue->add(new AuthenticationMiddleware($this)) ->add(new BodyParserMiddleware()); diff --git a/src/Command/ImporterCommand.php b/src/Command/ImporterCommand.php index ecbf6ac..2e0c3f8 100644 --- a/src/Command/ImporterCommand.php +++ b/src/Command/ImporterCommand.php @@ -2,7 +2,7 @@ /** * Generic importer to feed data to cerebrate from JSON or CSV. - * + * * - JSON configuration file must have the `format` key which can either take the value `json` or `csv` * - If `csv` is provided, the file must contains the header. * - If `json` is provided, a `mapping` key on how to reach each fields using the cakephp4's Hash syntax must be provided. @@ -10,7 +10,7 @@ * - The key is the field name * - The value * - Can either be the string representing the path from which to get the value - * - Or a JSON containg the `path`, the optional `override` parameter specifying if the existing data should be overriden + * - Or a JSON containg the `path`, the optional `override` parameter specifying if the existing data should be overriden * and an optional `massage` function able to alter the data. * - Example * { @@ -22,7 +22,7 @@ * }, * * - The optional primary key argument provides a way to make import replayable. It can typically be used when an ID or UUID is not provided in the source file but can be replaced by something else (e.g. team-name or other type of unique data). - * + * */ namespace App\Command; @@ -165,7 +165,7 @@ class ImporterCommand extends Command 'valueField' => 'id' ])->where(['meta_template_id' => $metaTemplate->id])->toArray(); } else { - $this->io->error("Unkown template for UUID {$config['metaTemplateUUID']}"); + $this->io->error("Unknown template for UUID {$config['metaTemplateUUID']}"); die(1); } } @@ -196,7 +196,7 @@ class ImporterCommand extends Command $metaEntity->meta_template_field_id = $metaTemplateFieldsMapping[$fieldName]; } else { $hasErrors = true; - $this->io->error("Field $fieldName is unkown for template {$metaTemplate->name}"); + $this->io->error("Field $fieldName is unknown for template {$metaTemplate->name}"); break; } } @@ -529,4 +529,4 @@ class ImporterCommand extends Command { return is_null($value) ? '' : $value; } -} \ No newline at end of file +} diff --git a/src/Controller/ApiController.php b/src/Controller/ApiController.php new file mode 100644 index 0000000..65cd11c --- /dev/null +++ b/src/Controller/ApiController.php @@ -0,0 +1,19 @@ +set('url', $url); + } +} diff --git a/src/Controller/AppController.php b/src/Controller/AppController.php index 53d1b24..6392205 100644 --- a/src/Controller/AppController.php +++ b/src/Controller/AppController.php @@ -54,7 +54,6 @@ class AppController extends Controller public function initialize(): void { parent::initialize(); - $this->loadComponent('RequestHandler'); $this->loadComponent('Flash'); $this->loadComponent('RestResponse'); @@ -102,7 +101,7 @@ class AppController extends Controller $this->ACL->setPublicInterfaces(); if (!empty($this->request->getAttribute('identity'))) { $user = $this->Users->get($this->request->getAttribute('identity')->getIdentifier(), [ - 'contain' => ['Roles', 'Individuals' => 'Organisations', 'UserSettings'] + 'contain' => ['Roles', 'Individuals' => 'Organisations', 'UserSettings', 'Organisations'] ]); if (!empty($user['disabled'])) { $this->Authentication->logout(); @@ -111,9 +110,13 @@ class AppController extends Controller } unset($user['password']); $this->ACL->setUser($user); + $this->request->getSession()->write('authUser', $user); $this->isAdmin = $user['role']['perm_admin']; - $this->set('menu', $this->ACL->getMenu()); - $this->set('loggedUser', $this->ACL->getUser()); + if (!$this->ParamHandler->isRest()) { + $this->set('menu', $this->ACL->getMenu()); + $this->set('loggedUser', $this->ACL->getUser()); + $this->set('roleAccess', $this->ACL->getRoleAccess(false, false)); + } } else if ($this->ParamHandler->isRest()) { throw new MethodNotAllowedException(__('Invalid user credentials.')); } @@ -128,17 +131,20 @@ class AppController extends Controller } $this->ACL->checkAccess(); - $this->set('ajax', $this->request->is('ajax')); - $this->request->getParam('prefix'); - $this->set('baseurl', Configure::read('App.fullBaseUrl')); - if (!empty($user) && !empty($user->user_settings_by_name['ui.bsTheme']['value'])) { - $this->set('bsTheme', $user->user_settings_by_name['ui.bsTheme']['value']); - } else { - $this->set('bsTheme', Configure::check('ui.bsTheme') ? Configure::read('ui.bsTheme') : 'default'); - } + if (!$this->ParamHandler->isRest()) { + $this->set('breadcrumb', $this->Navigation->getBreadcrumb()); + $this->set('ajax', $this->request->is('ajax')); + $this->request->getParam('prefix'); + $this->set('baseurl', Configure::read('App.fullBaseUrl')); + if (!empty($user) && !empty($user->user_settings_by_name['ui.bsTheme']['value'])) { + $this->set('bsTheme', $user->user_settings_by_name['ui.bsTheme']['value']); + } else { + $this->set('bsTheme', Configure::check('ui.bsTheme') ? Configure::read('ui.bsTheme') : 'default'); + } - if ($this->modelClass == 'Tags.Tags') { - $this->set('metaGroup', !empty($this->isAdmin) ? 'Administration' : 'Cerebrate'); + if ($this->modelClass == 'Tags.Tags') { + $this->set('metaGroup', !empty($this->isAdmin) ? 'Administration' : 'Cerebrate'); + } } } @@ -151,13 +157,30 @@ class AppController extends Controller { if (!empty($_SERVER['HTTP_AUTHORIZATION']) && strlen($_SERVER['HTTP_AUTHORIZATION'])) { $this->loadModel('AuthKeys'); + $logModel = $this->Users->auditLogs(); $authKey = $this->AuthKeys->checkKey($_SERVER['HTTP_AUTHORIZATION']); if (!empty($authKey)) { $this->loadModel('Users'); $user = $this->Users->get($authKey['user_id']); + $logModel->insert([ + 'request_action' => 'login', + 'model' => 'Users', + 'model_id' => $user['id'], + 'model_title' => $user['username'], + 'changed' => [] + ]); if (!empty($user)) { $this->Authentication->setIdentity($user); } + } else { + $user = $logModel->userInfo(); + $logModel->insert([ + 'request_action' => 'login', + 'model' => 'Users', + 'model_id' => $user['id'], + 'model_title' => $user['name'], + 'changed' => [] + ]); } } } @@ -172,4 +195,9 @@ class AppController extends Controller { return $this->RestResponse->viewData($this->ACL->findMissingFunctionNames()); } + + public function getRoleAccess() + { + return $this->RestResponse->viewData($this->ACL->getRoleAccess(false, false)); + } } diff --git a/src/Controller/AuditLogsController.php b/src/Controller/AuditLogsController.php new file mode 100644 index 0000000..27bee73 --- /dev/null +++ b/src/Controller/AuditLogsController.php @@ -0,0 +1,36 @@ +CRUD->index([ + 'contain' => $this->containFields, + 'filters' => $this->filterFields, + 'quickFilters' => $this->quickFilterFields, + 'afterFind' => function($data) { + $data['request_ip'] = inet_ntop(stream_get_contents($data['request_ip'])); + $data['changed'] = stream_get_contents($data['changed']); + return $data; + } + ]); + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; + } + $this->set('metaGroup', 'Administration'); + } +} diff --git a/src/Controller/AuthKeysController.php b/src/Controller/AuthKeysController.php index 5f75b4a..9ed43c3 100644 --- a/src/Controller/AuthKeysController.php +++ b/src/Controller/AuthKeysController.php @@ -16,15 +16,25 @@ class AuthKeysController extends AppController { public $filterFields = ['Users.username', 'authkey', 'comment', 'Users.id']; public $quickFilterFields = ['authkey', ['comment' => true]]; - public $containFields = ['Users']; + public $containFields = ['Users' => ['fields' => ['id', 'username']]]; public function index() { + $currentUser = $this->ACL->getUser(); + $conditions = []; + if (empty($currentUser['role']['perm_admin'])) { + $conditions['Users.organisation_id'] = $currentUser['organisation_id']; + if (empty($currentUser['role']['perm_org_admin'])) { + $conditions['Users.id'] = $currentUser['id']; + } + } $this->CRUD->index([ 'filters' => $this->filterFields, 'quickFilters' => $this->quickFilterFields, 'contain' => $this->containFields, - 'exclude_fields' => ['authkey'] + 'exclude_fields' => ['authkey'], + 'conditions' => $conditions, + 'hidden' => [] ]); $responsePayload = $this->CRUD->getResponsePayload(); if (!empty($responsePayload)) { @@ -35,7 +45,15 @@ class AuthKeysController extends AppController public function delete($id) { - $this->CRUD->delete($id); + $currentUser = $this->ACL->getUser(); + $conditions = []; + if (empty($currentUser['role']['perm_admin'])) { + $conditions['Users.organisation_id'] = $currentUser['organisation_id']; + if (empty($currentUser['role']['perm_org_admin'])) { + $conditions['Users.id'] = $currentUser['id']; + } + } + $this->CRUD->delete($id, ['conditions' => $conditions, 'contain' => 'Users']); $responsePayload = $this->CRUD->getResponsePayload(); if (!empty($responsePayload)) { return $responsePayload; @@ -46,8 +64,30 @@ class AuthKeysController extends AppController public function add() { $this->set('metaGroup', $this->isAdmin ? 'Administration' : 'Cerebrate'); + $validUsers = []; + $userConditions = []; + $currentUser = $this->ACL->getUser(); + if (empty($currentUser['role']['perm_admin'])) { + if (empty($currentUser['role']['perm_org_admin'])) { + $userConditions['id'] = $currentUser['id']; + } else { + $role_ids = $this->Users->Roles->find()->where(['perm_admin' => 0])->all()->extract('id')->toList(); + $userConditions['role_id IN'] = $role_ids; + } + } + $users = $this->Users->find('list'); + if (!empty($userConditions)) { + $users->where($userConditions); + } + $users = $users->order(['username' => 'asc'])->all()->toArray(); $this->CRUD->add([ - 'displayOnSuccess' => 'authkey_display' + 'displayOnSuccess' => 'authkey_display', + 'beforeSave' => function($data) use ($users) { + if (!in_array($data['user_id'], array_keys($users))) { + return false; + } + return $data; + } ]); $responsePayload = $this->CRUD->getResponsePayload([ 'displayOnSuccess' => 'authkey_display' @@ -57,9 +97,7 @@ class AuthKeysController extends AppController } $this->loadModel('Users'); $dropdownData = [ - 'user' => $this->Users->find('list', [ - 'sort' => ['username' => 'asc'] - ]) + 'user' => $users ]; $this->set(compact('dropdownData')); } diff --git a/src/Controller/Component/ACLComponent.php b/src/Controller/Component/ACLComponent.php index 5feae97..fb51f49 100644 --- a/src/Controller/Component/ACLComponent.php +++ b/src/Controller/Component/ACLComponent.php @@ -37,6 +37,7 @@ class ACLComponent extends Component '*' => [ 'checkPermission' => ['*'], 'generateUUID' => ['*'], + 'getRoleAccess' => ['*'], 'queryACL' => ['perm_admin'] ], 'Alignments' => [ @@ -45,6 +46,9 @@ class ACLComponent extends Component 'index' => ['*'], 'view' => ['*'] ], + 'AuditLogs' => [ + 'index' => ['perm_admin'] + ], 'AuthKeys' => [ 'add' => ['*'], 'delete' => ['*'], @@ -64,6 +68,7 @@ class ACLComponent extends Component 'view' => ['perm_admin'] ], 'EncryptionKeys' => [ + 'view' => ['*'], 'add' => ['*'], 'edit' => ['*'], 'delete' => ['*'], @@ -82,22 +87,30 @@ class ACLComponent extends Component 'add' => ['perm_admin'], 'delete' => ['perm_admin'], 'edit' => ['perm_admin'], + 'filtering' => ['*'], 'index' => ['*'], - 'view' => ['*'] + 'tag' => ['perm_tagger'], + 'untag' => ['perm_tagger'], + 'view' => ['*'], + 'viewTags' => ['*'] ], 'Instance' => [ 'home' => ['*'], 'migrate' => ['perm_admin'], 'migrationIndex' => ['perm_admin'], 'rollback' => ['perm_admin'], + 'saveSetting' => ['perm_admin'], + 'searchAll' => ['*'], + 'settings' => ['perm_admin'], 'status' => ['*'] ], 'LocalTools' => [ 'action' => ['perm_admin'], 'add' => ['perm_admin'], + 'batchAction' => ['perm_admin'], 'broodTools' => ['perm_admin'], 'connectionRequest' => ['perm_admin'], - 'connectLocal' => ['perm_admin'], + // 'connectLocal' => ['perm_admin'], 'delete' => ['perm_admin'], 'edit' => ['perm_admin'], 'exposedTools' => ['OR' => ['perm_admin', 'perm_sync']], @@ -123,7 +136,10 @@ class ACLComponent extends Component 'edit' => ['perm_admin'], 'filtering' => ['*'], 'index' => ['*'], - 'view' => ['*'] + 'tag' => ['perm_tagger'], + 'untag' => ['perm_tagger'], + 'view' => ['*'], + 'viewTags' => ['*'] ], 'Outbox' => [ 'createEntry' => ['perm_admin'], @@ -145,25 +161,42 @@ class ACLComponent extends Component 'view' => ['*'] ], 'SharingGroups' => [ - 'add' => ['perm_admin'], - 'addOrg' => ['perm_admin'], - 'delete' => ['perm_admin'], - 'edit' => ['perm_admin'], + 'add' => ['perm_org_admin'], + 'addOrg' => ['perm_org_admin'], + 'delete' => ['perm_org_admin'], + 'edit' => ['perm_org_admin'], 'index' => ['*'], 'listOrgs' => ['*'], - 'removeOrg' => ['perm_admin'], + 'removeOrg' => ['perm_org_admin'], 'view' => ['*'] ], 'Users' => [ - 'add' => ['perm_admin'], - 'delete' => ['perm_admin'], + 'add' => ['perm_org_admin'], + 'delete' => ['perm_org_admin'], 'edit' => ['*'], - 'index' => ['perm_admin'], + 'index' => ['perm_org_admin'], 'login' => ['*'], 'logout' => ['*'], 'register' => ['*'], - 'toggle' => ['perm_admin'], + 'settings' => ['*'], + 'toggle' => ['perm_org_admin'], 'view' => ['*'] + ], + 'UserSettings' => [ + 'index' => ['*'], + 'view' => ['*'], + 'add' => ['*'], + 'edit' => ['*'], + 'delete' => ['*'], + 'getSettingByName' => ['*'], + 'setSetting' => ['*'], + 'saveSetting' => ['*'], + 'getBookmarks' => ['*'], + 'saveBookmark' => ['*'], + 'deleteBookmark' => ['*'] + ], + 'Api' => [ + 'index' => ['*'] ] ); @@ -267,9 +300,19 @@ class ACLComponent extends Component return true; } //$this->__checkLoggedActions($user, $controller, $action); + if (isset($this->aclList['*'][$action])) { + if ($this->evaluateAccessLeaf('*', $action)) { + return true; + } + } if (!isset($this->aclList[$controller])) { return $this->__error(404, __('Invalid controller.'), $soft); } + return $this->evaluateAccessLeaf($controller, $action); + } + + private function evaluateAccessLeaf(string $controller, string $action): bool + { if (isset($this->aclList[$controller][$action]) && !empty($this->aclList[$controller][$action])) { if (in_array('*', $this->aclList[$controller][$action])) { return true; @@ -290,6 +333,12 @@ class ACLComponent extends Component if ($allConditionsMet) { return true; } + } else { + foreach ($this->aclList[$controller][$action] as $permission) { + if ($this->user['role'][$permission]) { + return true; + } + } } } return false; @@ -391,13 +440,18 @@ class ACLComponent extends Component return $missing; } + public function getRoleAccess($role = false, $url_mode = true) + { + return $this->__checkRoleAccess($role, $url_mode); + } + public function printRoleAccess($content = false) { $results = []; - $this->Role = TableRegistry::get('Role'); + $this->Role = TableRegistry::get('Roles'); $conditions = []; if (is_numeric($content)) { - $conditions = array('Role.id' => $content); + $conditions = array('id' => $content); } $roles = $this->Role->find('all', array( 'recursive' => -1, @@ -413,44 +467,54 @@ class ACLComponent extends Component return $results; } - private function __checkRoleAccess($role) + private function __formatControllerAction(array $results, string $controller, string $action, $url_mode = true): array { - $result = []; - foreach ($this->__aclList as $controller => $actions) { - $controllerNames = Inflector::variable($controller) == Inflector::underscore($controller) ? array(Inflector::variable($controller)) : array(Inflector::variable($controller), Inflector::underscore($controller)); - foreach ($controllerNames as $controllerName) { - foreach ($actions as $action => $permissions) { - if ($role['perm_site_admin']) { - $result[] = DS . $controllerName . DS . $action; - } elseif (in_array('*', $permissions)) { - $result[] = DS . $controllerName . DS . $action . DS . '*'; - } elseif (isset($permissions['OR'])) { - $access = false; - foreach ($permissions['OR'] as $permission) { - if ($role[$permission]) { - $access = true; - } + if ($url_mode) { + $results[] = DS . $controller . DS . $action . DS . '*'; + } else { + $results[$controller][] = $action; + } + return $results; + } + + private function __checkRoleAccess($role = false, $url_mode = true) + { + $results = []; + if ($role === false) { + $role = $this->getUser()['role']; + } + foreach ($this->aclList as $controller => $actions) { + foreach ($actions as $action => $permissions) { + if ($role['perm_admin']) { + $results = $this->__formatControllerAction($results, $controller, $action, $url_mode); + } elseif (in_array('*', $permissions)) { + $results = $this->__formatControllerAction($results, $controller, $action, $url_mode); + } elseif (isset($permissions['OR'])) { + $access = false; + foreach ($permissions['OR'] as $permission) { + if ($role[$permission]) { + $access = true; } - if ($access) { - $result[] = DS . $controllerName . DS . $action . DS . '*'; - } - } elseif (isset($permissions['AND'])) { - $access = true; - foreach ($permissions['AND'] as $permission) { - if ($role[$permission]) { - $access = false; - } - } - if ($access) { - $result[] = DS . $controllerName . DS . $action . DS . '*'; - } - } elseif (isset($permissions[0]) && $role[$permissions[0]]) { - $result[] = DS . $controllerName . DS . $action . DS . '*'; } + if ($access) { + $results = $this->__formatControllerAction($results, $controller, $action, $url_mode); + } + } elseif (isset($permissions['AND'])) { + $access = true; + foreach ($permissions['AND'] as $permission) { + if ($role[$permission]) { + $access = false; + } + } + if ($access) { + $results = $this->__formatControllerAction($results, $controller, $action, $url_mode); + } + } elseif (isset($permissions[0]) && $role[$permissions[0]]) { + $results = $this->__formatControllerAction($results, $controller, $action, $url_mode); } } } - return $result; + return $results; } public function getMenu() diff --git a/src/Controller/Component/CRUDComponent.php b/src/Controller/Component/CRUDComponent.php index 12cedf6..fa58b83 100644 --- a/src/Controller/Component/CRUDComponent.php +++ b/src/Controller/Component/CRUDComponent.php @@ -56,6 +56,9 @@ class CRUDComponent extends Component } $query = $this->setFilters($params, $query, $options); $query = $this->setQuickFilters($params, $query, $options); + if (!empty($options['conditions'])) { + $query->where($options['conditions']); + } if (!empty($options['contain'])) { $query->contain($options['contain']); } @@ -67,11 +70,25 @@ class CRUDComponent extends Component } if ($this->Controller->ParamHandler->isRest()) { $data = $query->all(); + if (isset($options['hidden'])) { + $data->each(function($value, $key) use ($options) { + $hidden = is_array($options['hidden']) ? $options['hidden'] : [$options['hidden']]; + $value->setHidden($hidden); + return $value; + }); + } if (isset($options['afterFind'])) { + $function = $options['afterFind']; if (is_callable($options['afterFind'])) { - $data = $options['afterFind']($data); + $function = $options['afterFind']; + $data->each(function($value, $key) use ($function) { + return $function($value); + }); } else { - $data = $this->Table->{$options['afterFind']}($data); + $t = $this->Table; + $data->each(function($value, $key) use ($t, $function) { + return $t->$function($value); + }); } } $this->Controller->restResponsePayload = $this->RestResponse->viewData($data, 'json'); @@ -82,10 +99,17 @@ class CRUDComponent extends Component $this->Controller->loadComponent('Paginator'); $data = $this->Controller->Paginator->paginate($query); if (isset($options['afterFind'])) { + $function = $options['afterFind']; if (is_callable($options['afterFind'])) { - $data = $options['afterFind']($data); + $function = $options['afterFind']; + $data->each(function($value, $key) use ($function) { + return $function($value); + }); } else { - $data = $this->Table->{$options['afterFind']}($data); + $t = $this->Table; + $data->each(function($value, $key) use ($t, $function) { + return $t->$function($value); + }); } } $this->setFilteringContext($options['contextFilters'] ?? [], $params); @@ -220,6 +244,9 @@ class CRUDComponent extends Component $data = $this->Table->patchEntity($data, $input, $patchEntityParams); if (isset($params['beforeSave'])) { $data = $params['beforeSave']($data); + if ($data === false) { + throw new NotFoundException(__('Could not save {0} due to the input failing to meet expectations. Your input is bad and you should feel bad.', $this->ObjectAlias)); + } } $savedData = $this->Table->save($data); if ($savedData !== false) { @@ -417,18 +444,25 @@ class CRUDComponent extends Component $params['contain'][] = 'Tags'; $this->setAllTags(); } - $getParam = isset($params['get']) ? $params['get'] : $params; + $queryParam = isset($params['get']) ? $params['get'] : $params; if ($this->metaFieldsSupported()) { - if (empty($getParam['contain'])) { - $getParam['contain'] = []; + if (empty( $queryParam['contain'])) { + $queryParam['contain'] = []; } - if (is_array($getParam['contain'])) { - $getParam['contain'][] = 'MetaFields'; + if (is_array( $queryParam['contain'])) { + $queryParam['contain'][] = 'MetaFields'; } else { - $getParam['contain'] = [$getParam['contain'], 'MetaFields']; + $queryParam['contain'] = [ $queryParam['contain'], 'MetaFields']; } } - $data = $this->Table->get($id, $getParam); + $query = $this->Table->find()->where(['id' => $id]); + if (!empty($params['conditions'])) { + $query->where($params['conditions']); + } + $data = $query->first(); + if (empty($data)) { + throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias)); + } if ($this->metaFieldsSupported()) { $metaTemplates = $this->getMetaTemplates(); $data = $this->attachMetaTemplatesIfNeeded($data, $metaTemplates->toArray()); @@ -653,11 +687,27 @@ class CRUDComponent extends Component return $data; } - public function delete($id = false): void + public function delete($id=false, $params=[]): void { if ($this->request->is('get')) { - if (!empty($id)) { - $data = $this->Table->get($id); + if(!empty($id)) { + $query = $this->Table->find()->where([$this->Table->getAlias() . '.id' => $id]); + if (!empty($params['conditions'])) { + $query->where($params['conditions']); + } + if (!empty($params['contain'])) { + $query->contain($params['contain']); + } + $data = $query->first(); + if (empty($data)) { + throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias)); + } + if (isset($params['beforeSave'])) { + $data = $params['beforeSave']($data); + if ($data === false) { + throw new NotFoundException(__('Could not save {0} due to the input failing to meet expectations. Your input is bad and you should feel bad.', $this->ObjectAlias)); + } + } $this->Controller->set('id', $data['id']); $this->Controller->set('data', $data); $this->Controller->set('bulkEnabled', false); @@ -669,8 +719,26 @@ class CRUDComponent extends Component $isBulk = count($ids) > 1; $bulkSuccesses = 0; foreach ($ids as $id) { - $data = $this->Table->get($id); - $success = $this->Table->delete($data); + $query = $this->Table->find()->where([$this->Table->getAlias() . '.id' => $id]); + if (!empty($params['conditions'])) { + $query->where($params['conditions']); + } + if (!empty($params['contain'])) { + $query->contain($params['contain']); + } + $data = $query->first(); + if (isset($params['beforeSave'])) { + $data = $params['beforeSave']($data); + if ($data === false) { + throw new NotFoundException(__('Could not save {0} due to the input failing to meet expectations. Your input is bad and you should feel bad.', $this->ObjectAlias)); + } + } + if (!empty($data)) { + $success = $this->Table->delete($data); + $success = true; + } else { + $success = false; + } if ($success) { $bulkSuccesses++; } diff --git a/src/Controller/Component/Navigation/sidemenu.php b/src/Controller/Component/Navigation/sidemenu.php index 5217a3f..a86bafe 100644 --- a/src/Controller/Component/Navigation/sidemenu.php +++ b/src/Controller/Component/Navigation/sidemenu.php @@ -50,7 +50,12 @@ class Sidemenu { 'label' => __('Broods'), 'icon' => $this->iconTable['Broods'], 'url' => '/broods/index', - ] + ], + 'API' => [ + 'label' => __('API'), + 'icon' => $this->iconTable['API'], + 'url' => '/api/index', + ], ], __('Administration') => [ 'Roles' => [ @@ -118,6 +123,11 @@ class Sidemenu { 'url' => '/instance/migrationIndex', 'icon' => 'database', ], + 'AuditLogs' => [ + 'label' => __('Audit Logs'), + 'url' => '/auditLogs/index', + 'icon' => 'history', + ], ] ], ], @@ -149,4 +159,4 @@ class Sidemenu { ] ]; } -} \ No newline at end of file +} diff --git a/src/Controller/Component/NavigationComponent.php b/src/Controller/Component/NavigationComponent.php index 0f3569f..5d3d1e5 100644 --- a/src/Controller/Component/NavigationComponent.php +++ b/src/Controller/Component/NavigationComponent.php @@ -35,6 +35,7 @@ class NavigationComponent extends Component 'LocalTools' => 'tools', 'Instance' => 'server', 'Tags' => 'tags', + 'API' => 'code', ]; public function initialize(array $config): void diff --git a/src/Controller/Component/ParamHandlerComponent.php b/src/Controller/Component/ParamHandlerComponent.php index 143a6eb..ae59f73 100644 --- a/src/Controller/Component/ParamHandlerComponent.php +++ b/src/Controller/Component/ParamHandlerComponent.php @@ -4,6 +4,7 @@ namespace App\Controller\Component; use Cake\Controller\Component; use Cake\Core\Configure; +use Cake\Http\Exception\MethodNotAllowedException; class ParamHandlerComponent extends Component { @@ -47,7 +48,7 @@ class ParamHandlerComponent extends Component return $this->isRest; } if ($this->request->is('json')) { - if (!empty($this->request->input()) && empty($this->request->input('json_decode'))) { + if (!empty((string)$this->request->getBody()) && empty($this->request->getParsedBody())) { throw new MethodNotAllowedException('Invalid JSON input. Make sure that the JSON input is a correctly formatted JSON string. This request has been blocked to avoid an unfiltered request.'); } $this->isRest = true; diff --git a/src/Controller/Component/RestResponseComponent.php b/src/Controller/Component/RestResponseComponent.php index 4a7346f..50fb40c 100644 --- a/src/Controller/Component/RestResponseComponent.php +++ b/src/Controller/Component/RestResponseComponent.php @@ -718,11 +718,11 @@ class RestResponseComponent extends Component 'operators' => array('equal'), 'help' => __('A valid x509 certificate ') ), - 'change' => array( + 'changed' => array( 'input' => 'text', 'type' => 'string', 'operators' => array('equal'), - 'help' => __('The text contained in the change field') + 'help' => __('The text contained in the changed field') ), 'change_pw' => array( 'input' => 'radio', diff --git a/src/Controller/EncryptionKeysController.php b/src/Controller/EncryptionKeysController.php index 45621d5..d0376db 100644 --- a/src/Controller/EncryptionKeysController.php +++ b/src/Controller/EncryptionKeysController.php @@ -14,7 +14,7 @@ use Cake\Error\Debugger; class EncryptionKeysController extends AppController { - public $filterFields = ['owner_model', 'organisation_id', 'individual_id', 'encryption_key']; + public $filterFields = ['owner_model', 'owner_id', 'encryption_key']; public $quickFilterFields = ['encryption_key']; public $containFields = ['Individuals', 'Organisations']; public $statisticsFields = ['type']; @@ -41,7 +41,15 @@ class EncryptionKeysController extends AppController public function delete($id) { - $this->CRUD->delete($id); + $orgConditions = []; + $individualConditions = []; + $dropdownData = []; + $currentUser = $this->ACL->getUser(); + $params = []; + if (empty($currentUser['role']['perm_admin'])) { + $params = $this->buildBeforeSave($params, $currentUser, $orgConditions, $individualConditions, $dropdownData); + } + $this->CRUD->delete($id, $params); $responsePayload = $this->CRUD->getResponsePayload(); if (!empty($responsePayload)) { return $responsePayload; @@ -49,35 +57,92 @@ class EncryptionKeysController extends AppController $this->set('metaGroup', 'ContactDB'); } - public function add() + private function buildBeforeSave(array $params, $currentUser, array &$orgConditions, array &$individualConditions, array &$dropdownData): array { - $this->CRUD->add(['redirect' => $this->referer()]); - $responsePayload = $this->CRUD->getResponsePayload(); - if (!empty($responsePayload)) { - return $responsePayload; + if (empty($currentUser['role']['perm_admin'])) { + $orgConditions = [ + 'id' => $currentUser['organisation_id'] + ]; + if (empty($currentUser['role']['perm_org_admin'])) { + $individualConditions = [ + 'id' => $currentUser['individual_id'] + ]; + } else { + $this->loadModel('Alignments'); + $individualConditions = ['id IN' => $this->Alignments->find('list', [ + 'keyField' => 'id', + 'valueField' => 'individual_id', + 'conditions' => ['organisation_id' => $currentUser['organisation_id']] + ])->toArray()]; + } + $params['beforeSave'] = function($entity) use($currentUser) { + if ($entity['owner_model'] === 'organisation') { + if ($entity['owner_id'] !== $currentUser['organisation_id']) { + throw new MethodNotAllowedException(__('Selected organisation cannot be linked by the current user.')); + } + } else { + if ($currentUser['role']['perm_org_admin']) { + $this->loadModel('Alignments'); + $validIndividuals = $this->Alignments->find('list', [ + 'keyField' => 'individual_id', + 'valueField' => 'id', + 'conditions' => ['organisation_id' => $currentUser['organisation_id']] + ])->toArray(); + if (!isset($validIndividuals[$entity['owner_id']])) { + throw new MethodNotAllowedException(__('Selected individual cannot be linked by the current user.')); + } + } else { + if ($entity['owner_id'] !== $currentUser['id']) { + throw new MethodNotAllowedException(__('Selected individual cannot be linked by the current user.')); + } + } + } + return $entity; + }; } $this->loadModel('Organisations'); $this->loadModel('Individuals'); $dropdownData = [ - 'organisation' => $this->Organisations->find('list', [ - 'sort' => ['name' => 'asc'] - ]), - 'individual' => $this->Individuals->find('list', [ - 'sort' => ['email' => 'asc'] - ]) + 'organisation' => $this->Organisations->find('list')->order(['name' => 'asc'])->where($orgConditions)->all()->toArray(), + 'individual' => $this->Individuals->find('list')->order(['email' => 'asc'])->where($individualConditions)->all()->toArray() ]; + return $params; + } + + public function add() + { + $orgConditions = []; + $individualConditions = []; + $dropdownData = []; + $currentUser = $this->ACL->getUser(); + $params = [ + 'redirect' => $this->referer() + ]; + $params = $this->buildBeforeSave($params, $currentUser, $orgConditions, $individualConditions, $dropdownData); + $this->CRUD->add($params); + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; + } $this->set(compact('dropdownData')); $this->set('metaGroup', 'ContactDB'); } public function edit($id = false) { + $orgConditions = []; + $individualConditions = []; + $dropdownData = []; + $currentUser = $this->ACL->getUser(); $params = [ 'fields' => [ 'type', 'encryption_key', 'revoked' ], 'redirect' => $this->referer() ]; + if (empty($currentUser['role']['perm_admin'])) { + $params = $this->buildBeforeSave($params, $currentUser, $orgConditions, $individualConditions, $dropdownData); + } $this->CRUD->edit($id, $params); $responsePayload = $this->CRUD->getResponsePayload(); if (!empty($responsePayload)) { @@ -87,4 +152,16 @@ class EncryptionKeysController extends AppController $this->set('metaGroup', 'ContactDB'); $this->render('add'); } + + public function view($id = false) + { + $this->CRUD->view($id, [ + 'contain' => ['Individuals', 'Organisations'] + ]); + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; + } + $this->set('metaGroup', 'ContactDB'); + } } diff --git a/src/Controller/InstanceController.php b/src/Controller/InstanceController.php index e9fc392..15fbfd4 100644 --- a/src/Controller/InstanceController.php +++ b/src/Controller/InstanceController.php @@ -69,6 +69,12 @@ class InstanceController extends AppController usort($status, function($a, $b) { return strcmp($b['id'], $a['id']); }); + if ($this->ParamHandler->isRest()) { + return $this->RestResponse->viewData([ + 'status' => $status, + 'updateAvailables' => $migrationStatus['updateAvailables'], + ], 'json'); + } $this->set('status', $status); $this->set('updateAvailables', $migrationStatus['updateAvailables']); } @@ -139,6 +145,14 @@ class InstanceController extends AppController { $this->Settings = $this->getTableLocator()->get('Settings'); $all = $this->Settings->getSettings(true); + if ($this->ParamHandler->isRest()) { + return $this->RestResponse->viewData([ + 'settingsProvider' => $all['settingsProvider'], + 'settings' => $all['settings'], + 'settingsFlattened' => $all['settingsFlattened'], + 'notices' => $all['notices'], + ], 'json'); + } $this->set('settingsProvider', $all['settingsProvider']); $this->set('settings', $all['settings']); $this->set('settingsFlattened', $all['settingsFlattened']); diff --git a/src/Controller/LocalToolsController.php b/src/Controller/LocalToolsController.php index a0d31b9..12d9d62 100644 --- a/src/Controller/LocalToolsController.php +++ b/src/Controller/LocalToolsController.php @@ -340,6 +340,7 @@ class LocalToolsController extends AppController } } +/* public function connectLocal($local_tool_id) { $params = [ @@ -355,10 +356,8 @@ class LocalToolsController extends AppController $params['target_tool_id'] = $postParams['target_tool_id']; $result = $this->LocalTools->encodeLocalConnection($params); // Send message to remote inbox - debug($result); } else { $target_tools = $this->LocalTools->findConnectable($local_tool); - debug($target_tools); if (empty($target_tools)) { throw new NotFoundException(__('No tools found to connect.')); } @@ -369,4 +368,5 @@ class LocalToolsController extends AppController ]); } } +*/ } diff --git a/src/Controller/Open/IndividualsController.php b/src/Controller/Open/IndividualsController.php index fa75aaa..28cd51d 100644 --- a/src/Controller/Open/IndividualsController.php +++ b/src/Controller/Open/IndividualsController.php @@ -6,6 +6,7 @@ use App\Controller\AppController; use Cake\Utility\Hash; use Cake\Utility\Text; use \Cake\Database\Expression\QueryExpression; +use Cake\Http\Exception\BadRequestException; use Cake\Http\Exception\NotFoundException; use Cake\Http\Exception\MethodNotAllowedException; use Cake\Http\Exception\ForbiddenException; diff --git a/src/Controller/SharingGroupsController.php b/src/Controller/SharingGroupsController.php index 594c9df..cf4c9b0 100644 --- a/src/Controller/SharingGroupsController.php +++ b/src/Controller/SharingGroupsController.php @@ -16,10 +16,16 @@ class SharingGroupsController extends AppController public function index() { + $currentUser = $this->ACL->getUser(); + $conditions = []; + if (empty($currentUser['role']['perm_admin'])) { + $conditions['SharingGroups.organisation_id'] = $currentUser['organisation_id']; + } $this->CRUD->index([ 'contain' => $this->containFields, 'filters' => $this->filterFields, - 'quickFilters' => $this->quickFilterFields + 'quickFilters' => $this->quickFilterFields, + 'conditions' => $conditions ]); $responsePayload = $this->CRUD->getResponsePayload(); if (!empty($responsePayload)) { @@ -57,7 +63,12 @@ class SharingGroupsController extends AppController public function edit($id = false) { - $this->CRUD->edit($id); + $params = []; + $currentUser = $this->ACL->getUser(); + if (empty($currentUser['role']['perm_admin'])) { + $params['conditions'] = ['organisation_id' => $currentUser['organisation_id']]; + } + $this->CRUD->edit($id, $params); $responsePayload = $this->CRUD->getResponsePayload(); if (!empty($responsePayload)) { return $responsePayload; @@ -201,11 +212,11 @@ class SharingGroupsController extends AppController $organisations = []; if (!empty($user['role']['perm_admin'])) { $organisations = $this->SharingGroups->Organisations->find('list')->order(['name' => 'ASC'])->toArray(); - } else if (!empty($user['individual']['organisations'])) { + } else { $organisations = $this->SharingGroups->Organisations->find('list', [ 'sort' => ['name' => 'asc'], 'conditions' => [ - 'id IN' => array_values(\Cake\Utility\Hash::extract($user, 'individual.organisations.{n}.id')) + 'id' => $user['organisation_id'] ] ]); } diff --git a/src/Controller/UserSettingsController.php b/src/Controller/UserSettingsController.php index 3d73707..d28f6ca 100644 --- a/src/Controller/UserSettingsController.php +++ b/src/Controller/UserSettingsController.php @@ -9,6 +9,8 @@ use \Cake\Database\Expression\QueryExpression; use Cake\Http\Exception\NotFoundException; use Cake\Http\Exception\MethodNotAllowedException; use Cake\Http\Exception\ForbiddenException; +use Cake\Http\Exception\UnauthorizedException; + class UserSettingsController extends AppController { @@ -19,8 +21,12 @@ class UserSettingsController extends AppController public function index() { $conditions = []; + $currentUser = $this->ACL->getUser(); + if (empty($currentUser['role']['perm_admin'])) { + $conditions['user_id'] = $currentUser->id; + } $this->CRUD->index([ - 'conditions' => [], + 'conditions' => $conditions, 'contain' => $this->containFields, 'filters' => $this->filterFields, 'quickFilters' => $this->quickFilterFields, @@ -39,6 +45,9 @@ class UserSettingsController extends AppController public function view($id) { + if (!$this->isLoggedUserAllowedToEdit($id)) { + throw new NotFoundException(__('Invalid {0}.', 'user setting')); + } $this->CRUD->view($id, [ 'contain' => ['Users'] ]); @@ -50,10 +59,13 @@ class UserSettingsController extends AppController public function add($user_id = false) { + $currentUser = $this->ACL->getUser(); $this->CRUD->add([ 'redirect' => ['action' => 'index', $user_id], - 'beforeSave' => function($data) use ($user_id) { - $data['user_id'] = $user_id; + 'beforeSave' => function ($data) use ($currentUser) { + if (empty($currentUser['role']['perm_admin'])) { + $data['user_id'] = $currentUser->id; + } return $data; } ]); @@ -61,10 +73,13 @@ class UserSettingsController extends AppController if (!empty($responsePayload)) { return $responsePayload; } + $allUsers = $this->UserSettings->Users->find('list', ['keyField' => 'id', 'valueField' => 'username'])->order(['username' => 'ASC']); + if (empty($currentUser['role']['perm_admin'])) { + $allUsers->where(['id' => $currentUser->id]); + $user_id = $currentUser->id; + } $dropdownData = [ - 'user' => $this->UserSettings->Users->find('list', [ - 'sort' => ['username' => 'asc'] - ]), + 'user' => $allUsers->all()->toArray(), ]; $this->set(compact('dropdownData')); $this->set('user_id', $user_id); @@ -75,6 +90,11 @@ class UserSettingsController extends AppController $entity = $this->UserSettings->find()->where([ 'id' => $id ])->first(); + + if (!$this->isLoggedUserAllowedToEdit($entity)) { + throw new NotFoundException(__('Invalid {0}.', 'user setting')); + } + $entity = $this->CRUD->edit($id, [ 'redirect' => ['action' => 'index', $entity->user_id] ]); @@ -94,6 +114,9 @@ class UserSettingsController extends AppController public function delete($id) { + if (!$this->isLoggedUserAllowedToEdit($id)) { + throw new NotFoundException(__('Invalid {0}.', 'user setting')); + } $this->CRUD->delete($id); $responsePayload = $this->CRUD->getResponsePayload(); if (!empty($responsePayload)) { @@ -160,7 +183,7 @@ class UserSettingsController extends AppController } } - public function getBookmarks($forSidebar=false) + public function getBookmarks($forSidebar = false) { $bookmarks = $this->UserSettings->getSettingByName($this->ACL->getUser(), $this->UserSettings->BOOKMARK_SETTING_NAME); $bookmarks = json_decode($bookmarks['value'], true); @@ -200,4 +223,29 @@ class UserSettingsController extends AppController $this->set('user_id', $this->ACL->getUser()->id); } -} \ No newline at end of file + /** + * isLoggedUserAllowedToEdit + * + * @param int|\App\Model\Entity\UserSetting $setting + * @return boolean + */ + private function isLoggedUserAllowedToEdit($setting): bool + { + $currentUser = $this->ACL->getUser(); + $isAllowed = false; + if (!empty($currentUser['role']['perm_admin'])) { + $isAllowed = true; + } else { + if (is_numeric($setting)) { + $setting = $this->UserSettings->find()->where([ + 'id' => $setting + ])->first(); + if (empty($setting)) { + return false; + } + } + $isAllowed = $setting->user_id == $currentUser->id; + } + return $isAllowed; + } +} diff --git a/src/Controller/UsersController.php b/src/Controller/UsersController.php index d2235a6..9ffb2fe 100644 --- a/src/Controller/UsersController.php +++ b/src/Controller/UsersController.php @@ -7,20 +7,27 @@ use Cake\Utility\Text; use Cake\ORM\TableRegistry; use \Cake\Database\Expression\QueryExpression; use Cake\Http\Exception\UnauthorizedException; +use Cake\Http\Exception\MethodNotAllowedException; use Cake\Core\Configure; class UsersController extends AppController { - public $filterFields = ['Individuals.uuid', 'username', 'Individuals.email', 'Individuals.first_name', 'Individuals.last_name']; + public $filterFields = ['Individuals.uuid', 'username', 'Individuals.email', 'Individuals.first_name', 'Individuals.last_name', 'Organisations.name']; public $quickFilterFields = ['Individuals.uuid', ['username' => true], ['Individuals.first_name' => true], ['Individuals.last_name' => true], 'Individuals.email']; - public $containFields = ['Individuals', 'Roles', 'UserSettings']; + public $containFields = ['Individuals', 'Roles', 'UserSettings', 'Organisations']; public function index() { + $currentUser = $this->ACL->getUser(); + $conditions = []; + if (empty($currentUser['role']['perm_admin'])) { + $conditions['organisation_id'] = $currentUser['organisation_id']; + } $this->CRUD->index([ 'contain' => $this->containFields, 'filters' => $this->filterFields, 'quickFilters' => $this->quickFilterFields, + 'conditions' => $conditions ]); $responsePayload = $this->CRUD->getResponsePayload(); if (!empty($responsePayload)) { @@ -31,8 +38,12 @@ class UsersController extends AppController public function add() { + $currentUser = $this->ACL->getUser(); $this->CRUD->add([ - 'beforeSave' => function($data) { + 'beforeSave' => function($data) use ($currentUser) { + if (!$currentUser['role']['perm_admin']) { + $data['organisation_id'] = $currentUser['organisation_id']; + } $this->Users->enrollUserRouter($data); return $data; } @@ -41,12 +52,28 @@ class UsersController extends AppController if (!empty($responsePayload)) { return $responsePayload; } + /* + $alignments = $this->Users->Individuals->Alignments->find('list', [ + //'keyField' => 'id', + 'valueField' => 'organisation_id', + 'groupField' => 'individual_id' + ])->toArray(); + $alignments = array_map(function($value) { return array_values($value); }, $alignments); + */ + $org_conditions = []; + if (empty($currentUser['role']['perm_admin'])) { + $org_conditions = ['id' => $currentUser['organisation_id']]; + } $dropdownData = [ 'role' => $this->Users->Roles->find('list', [ 'sort' => ['name' => 'asc'] ]), 'individual' => $this->Users->Individuals->find('list', [ 'sort' => ['email' => 'asc'] + ]), + 'organisation' => $this->Users->Organisations->find('list', [ + 'sort' => ['name' => 'asc'], + 'conditions' => $org_conditions ]) ]; $this->set(compact('dropdownData')); @@ -59,7 +86,7 @@ class UsersController extends AppController $id = $this->ACL->getUser()['id']; } $this->CRUD->view($id, [ - 'contain' => ['Individuals' => ['Alignments' => 'Organisations'], 'Roles'] + 'contain' => ['Individuals' => ['Alignments' => 'Organisations'], 'Roles', 'Organisations'] ]); $responsePayload = $this->CRUD->getResponsePayload(); if (!empty($responsePayload)) { @@ -70,9 +97,18 @@ class UsersController extends AppController public function edit($id = false) { - if (empty($id) || empty($this->ACL->getUser()['role']['perm_admin'])) { - $id = $this->ACL->getUser()['id']; + $currentUser = $this->ACL->getUser(); + if (empty($id)) { + $id = $currentUser['id']; + } else { + $id = intval($id); + if ((empty($currentUser['role']['perm_org_admin']) && empty($currentUser['role']['perm_admin']))) { + if ($id !== $currentUser['id']) { + throw new MethodNotAllowedException(__('You are not authorised to edit that user.')); + } + } } + $params = [ 'get' => [ 'fields' => [ @@ -83,11 +119,15 @@ class UsersController extends AppController 'password' ], 'fields' => [ - 'id', 'individual_id', 'username', 'disabled', 'password', 'confirm_password' + 'password', 'confirm_password' ] ]; if (!empty($this->ACL->getUser()['role']['perm_admin'])) { + $params['fields'][] = 'individual_id'; + $params['fields'][] = 'username'; $params['fields'][] = 'role_id'; + $params['fields'][] = 'organisation_id'; + $params['fields'][] = 'disabled'; } $this->CRUD->edit($id, $params); $responsePayload = $this->CRUD->getResponsePayload(); @@ -100,6 +140,9 @@ class UsersController extends AppController ]), 'individual' => $this->Users->Individuals->find('list', [ 'sort' => ['email' => 'asc'] + ]), + 'organisation' => $this->Users->Organisations->find('list', [ + 'sort' => ['name' => 'asc'] ]) ]; $this->set(compact('dropdownData')); @@ -130,11 +173,27 @@ class UsersController extends AppController { $result = $this->Authentication->getResult(); // If the user is logged in send them away. + $logModel = $this->Users->auditLogs(); if ($result->isValid()) { + $user = $logModel->userInfo(); + $logModel->insert([ + 'request_action' => 'login', + 'model' => 'Users', + 'model_id' => $user['id'], + 'model_title' => $user['name'], + 'changed' => [] + ]); $target = $this->Authentication->getLoginRedirect() ?? '/instance/home'; return $this->redirect($target); } if ($this->request->is('post') && !$result->isValid()) { + $logModel->insert([ + 'request_action' => 'login_fail', + 'model' => 'Users', + 'model_id' => 0, + 'model_title' => 'unknown_user', + 'changed' => [] + ]); $this->Flash->error(__('Invalid username or password')); } $this->viewBuilder()->setLayout('login'); @@ -144,6 +203,15 @@ class UsersController extends AppController { $result = $this->Authentication->getResult(); if ($result->isValid()) { + $logModel = $this->Users->auditLogs(); + $user = $logModel->userInfo(); + $logModel->insert([ + 'request_action' => 'logout', + 'model' => 'Users', + 'model_id' => $user['id'], + 'model_title' => $user['name'], + 'changed' => [] + ]); $this->Authentication->logout(); $this->Flash->success(__('Goodbye.')); return $this->redirect(\Cake\Routing\Router::url('/users/login')); @@ -162,7 +230,7 @@ class UsersController extends AppController public function register() { - if (empty(Configure::read('Cerebrate')['security.registration.self-registration'])) { + if (empty(Configure::read('security.registration.self-registration'))) { throw new UnauthorizedException(__('User self-registration is not open.')); } if ($this->request->is('post')) { diff --git a/src/Lib/default/local_tool_connectors/CommonConnectorTools.php b/src/Lib/default/local_tool_connectors/CommonConnectorTools.php index 179a238..d01e9d9 100644 --- a/src/Lib/default/local_tool_connectors/CommonConnectorTools.php +++ b/src/Lib/default/local_tool_connectors/CommonConnectorTools.php @@ -2,6 +2,8 @@ namespace CommonConnectorTools; use Cake\ORM\Locator\LocatorAwareTrait; +use Cake\Log\Log; +use Cake\Log\Engine\FileLog; class CommonConnectorTools { @@ -20,6 +22,38 @@ class CommonConnectorTools const STATE_CANCELLED = 'Request cancelled'; const STATE_DECLINED = 'Request declined by remote'; + public function __construct() + { + if (empty(Log::getConfig("LocalToolDebug{$this->connectorName}"))) { + Log::setConfig("LocalToolDebug{$this->connectorName}", [ + 'className' => FileLog::class, + 'path' => LOGS, + 'file' => "{$this->connectorName}-debug", + 'scopes' => [$this->connectorName], + 'levels' => ['notice', 'info', 'debug'], + ]); + } + if (empty(Log::getConfig("LocalToolError{$this->connectorName}"))) { + Log::setConfig("LocalToolError{$this->connectorName}", [ + 'className' => FileLog::class, + 'path' => LOGS, + 'file' => "{$this->connectorName}-error", + 'scopes' => [$this->connectorName], + 'levels' => ['warning', 'error', 'critical', 'alert', 'emergency'], + ]); + } + } + + protected function logDebug($message) + { + Log::debug($message, [$this->connectorName]); + } + + protected function logError($message, $scope=[]) + { + Log::error($message, [$this->connectorName]); + } + public function addExposedFunction(string $functionName): void { $this->exposedFunctions[] = $functionName; diff --git a/src/Lib/default/local_tool_connectors/MispConnector.php b/src/Lib/default/local_tool_connectors/MispConnector.php index c955cba..1ffc98f 100644 --- a/src/Lib/default/local_tool_connectors/MispConnector.php +++ b/src/Lib/default/local_tool_connectors/MispConnector.php @@ -122,6 +122,11 @@ class MispConnector extends CommonConnectorTools 'type' => 'boolean' ], ]; + public $settingsPlaceholder = [ + 'url' => 'https://your.misp.intance', + 'authkey' => '', + 'skip_ssl' => '0', + ]; public function addSettingValidatorRules($validator) { @@ -183,6 +188,7 @@ class MispConnector extends CommonConnectorTools $settings = json_decode($connection->settings, true); $http = $this->genHTTPClient($connection, $options); $url = sprintf('%s%s', $settings['url'], $relativeURL); + $this->logDebug(sprintf('%s %s %s', __('Posting data') . PHP_EOL, "POST {$url}" . PHP_EOL, json_encode($data))); return $http->post($url, $data, $options); } @@ -234,14 +240,18 @@ class MispConnector extends CommonConnectorTools if (!empty($params['softError'])) { return $response; } - throw new NotFoundException(__('Could not retrieve the requested resource.')); + $errorMsg = __('Could not post to the requested resource for `{0}`. Remote returned:', $url) . PHP_EOL . $response->getStringBody(); + $this->logError($errorMsg); + throw new NotFoundException($errorMsg); } } private function postData(string $url, array $params): Response { if (empty($params['connection'])) { - throw new NotFoundException(__('No connection object received.')); + $errorMsg = __('No connection object received.'); + $this->logError($errorMsg); + throw new NotFoundException($errorMsg); } $url = $this->urlAppendParams($url, $params); if (!is_string($params['body'])) { @@ -251,7 +261,9 @@ class MispConnector extends CommonConnectorTools if ($response->isOk()) { return $response; } else { - throw new NotFoundException(__('Could not post to the requested resource. Remote returned:') . PHP_EOL . $response->getStringBody()); + $errorMsg = __('Could not post to the requested resource for `{0}`. Remote returned:', $url) . PHP_EOL . $response->getStringBody(); + $this->logError($errorMsg); + throw new NotFoundException($errorMsg); } } @@ -309,54 +321,55 @@ class MispConnector extends CommonConnectorTools 'additionalUrlParams' => $urlParams ] ] + ] + ], + 'fields' => [ + [ + 'name' => 'Setting', + 'sort' => 'setting', + 'data_path' => 'setting', ], - 'fields' => [ - [ - 'name' => 'Setting', - 'sort' => 'setting', - 'data_path' => 'setting', + [ + 'name' => 'Criticality', + 'sort' => 'level', + 'data_path' => 'level', + 'arrayData' => [ + 0 => 'Critical', + 1 => 'Recommended', + 2 => 'Optional' ], - [ - 'name' => 'Criticality', - 'sort' => 'level', - 'data_path' => 'level', - 'arrayData' => [ - 0 => 'Critical', - 1 => 'Recommended', - 2 => 'Optional' - ], - 'element' => 'array_lookup_field' - ], - [ - 'name' => __('Value'), - 'sort' => 'value', - 'data_path' => 'value', - 'options' => 'options' - ], - [ - 'name' => __('Type'), - 'sort' => 'type', - 'data_path' => 'type', - ], - [ - 'name' => __('Error message'), - 'sort' => 'errorMessage', - 'data_path' => 'errorMessage', - ] + 'element' => 'array_lookup_field' ], - 'title' => false, - 'description' => false, - 'pull' => 'right', - 'actions' => [ - [ - 'open_modal' => '/localTools/action/' . h($params['connection']['id']) . '/modifySettingAction?setting={{0}}', - 'modal_params_data_path' => ['setting'], - 'icon' => 'download', - 'reload_url' => '/localTools/action/' . h($params['connection']['id']) . '/ServerSettingsAction' - ] + [ + 'name' => __('Value'), + 'sort' => 'value', + 'data_path' => 'value', + 'options' => 'options' + ], + [ + 'name' => __('Type'), + 'sort' => 'type', + 'data_path' => 'type', + ], + [ + 'name' => __('Error message'), + 'sort' => 'errorMessage', + 'data_path' => 'errorMessage', + ] + ], + 'title' => false, + 'description' => false, + 'pull' => 'right', + 'actions' => [ + [ + 'open_modal' => '/localTools/action/' . h($params['connection']['id']) . '/modifySettingAction?setting={{0}}', + 'modal_params_data_path' => ['setting'], + 'icon' => 'download', + 'reload_url' => '/localTools/action/' . h($params['connection']['id']) . '/ServerSettingsAction' ] ] - ]; + ] + ]; if (!empty($params['quickFilter'])) { $needle = strtolower($params['quickFilter']); foreach ($finalSettings['data']['data'] as $k => $v) { diff --git a/src/Lib/default/local_tool_connectors/SkeletonConnectorExample.php b/src/Lib/default/local_tool_connectors/SkeletonConnectorExample.php index 8daa13a..eefbd7a 100644 --- a/src/Lib/default/local_tool_connectors/SkeletonConnectorExample.php +++ b/src/Lib/default/local_tool_connectors/SkeletonConnectorExample.php @@ -46,6 +46,22 @@ class SkeletonConnector extends CommonConnectorTools 'redirect' => 'serverSettingsAction' ] ]; + public $settings = [ + 'url' => [ + 'type' => 'text' + ], + 'authkey' => [ + 'type' => 'text' + ], + 'skip_ssl' => [ + 'type' => 'boolean' + ], + ]; + public $settingsPlaceholder = [ + 'url' => 'https://your.url', + 'authkey' => '', + 'skip_ssl' => '0', + ]; public function health(Object $connection): array { diff --git a/src/Model/Behavior/AuditLogBehavior.php b/src/Model/Behavior/AuditLogBehavior.php new file mode 100644 index 0000000..55df5e8 --- /dev/null +++ b/src/Model/Behavior/AuditLogBehavior.php @@ -0,0 +1,213 @@ + true, + 'lastpushedid' => true, + 'timestamp' => true, + 'revision' => true, + 'modified' => true, + 'date_modified' => true, // User + 'current_login' => true, // User + 'last_login' => true, // User + 'newsread' => true, // User + 'proposal_email_lock' => true, // Event + ]; + + public function initialize(array $config): void + { + $this->config = $config; + } + + public function beforeSave(EventInterface $event, EntityInterface $entity, ArrayObject $options) + { + $fields = $entity->extract($entity->getVisible(), true); + $skipFields = $this->skipFields; + $fieldsToFetch = array_filter($fields, function($key) use ($skipFields) { + return strpos($key, '_') !== 0 && !isset($skipFields[$key]); + }, ARRAY_FILTER_USE_KEY); + // Do not fetch old version when just few fields will be fetched + $fieldToFetch = []; + if (!empty($options['fieldList'])) { + foreach ($options['fieldList'] as $field) { + if (!isset($this->skipFields[$field])) { + $fieldToFetch[] = $field; + } + } + if (empty($fieldToFetch)) { + $this->old = null; + return true; + } + } + if ($entity->id) { + $this->old = $this->_table->find()->where(['id' => $entity->id])->contain($fieldToFetch)->first(); + } else { + $this->old = null; + } + return true; + } + + public function afterSave(EventInterface $event, EntityInterface $entity, ArrayObject $options) + { + if ($entity->id) { + $id = $entity->id; + } else { + $id = null; + } + + if ($entity->isNew()) { + $action = $entity->getConstant('ACTION_ADD'); + } else { + $action = $entity->getConstant('ACTION_EDIT'); + if (isset($entity['deleted'])) { + if ($entity['deleted']) { + $action = $entity->getConstant('ACTION_SOFT_DELETE'); + } else if (!$entity['deleted'] && $this->old['deleted']) { + $action = $entity->getConstant('ACTION_UNDELETE'); + } + } + } + $changedFields = $this->changedFields($entity, isset($options['fieldList']) ? $options['fieldList'] : null); + if (empty($changedFields)) { + return; + } + + $modelTitleField = $this->_table->getDisplayField(); + if (is_callable($modelTitleField)) { + $modelTitle = $modelTitleField($entity, isset($this->old) ? $this->old : []); + } else if (isset($entity[$modelTitleField])) { + $modelTitle = $entity[$modelTitleField]; + } else if ($this->old[$modelTitleField]) { + $modelTitle = $this->old[$modelTitleField]; + } + $this->auditLogs()->insert([ + 'request_action' => $action, + 'model' => $entity->getSource(), + 'model_id' => $id, + 'model_title' => $modelTitle, + 'changed' => $changedFields + ]); + } + + public function beforeDelete(EventInterface $event, EntityInterface $entity, ArrayObject $options) + { + $this->old = $this->_table->find()->where(['id' => $entity->id])->first(); + return true; + } + + public function afterDelete(EventInterface $event, EntityInterface $entity, ArrayObject $options) + { + $modelTitleField = $this->_table->getDisplayField(); + if (is_callable($modelTitleField)) { + $modelTitle = $modelTitleField($entity, []); + } else if (isset($entity[$modelTitleField])) { + $modelTitle = $entity[$modelTitleField]; + } + + $this->auditLogs()->insert([ + 'request_action' => $entity->getConstant('ACTION_DELETE'), + 'model' => $entity->getSource(), + 'model_id' => $this->old->id, + 'model_title' => $modelTitle, + 'changed' => $this->changedFields($entity) + ]); + } + + /** + * @param Model $model + * @param array|null $fieldsToSave + * @return array + */ + private function changedFields(EntityInterface $entity, $fieldsToSave = null) + { + $dbFields = $this->_table->getSchema()->typeMap(); + $changedFields = []; + foreach ($entity->extract($entity->getVisible()) as $key => $value) { + if (isset($this->skipFields[$key])) { + continue; + } + if (!isset($dbFields[$key])) { + continue; + } + if ($fieldsToSave && !in_array($key, $fieldsToSave, true)) { + continue; + } + if (isset($entity[$key]) && isset($this->old[$key])) { + $old = $this->old[$key]; + } else { + $old = null; + } + // Normalize + if (is_bool($old)) { + $old = $old ? 1 : 0; + } + if (is_bool($value)) { + $value = $value ? 1 : 0; + } + $dbType = $dbFields[$key]; + if ($dbType === 'integer' || $dbType === 'tinyinteger' || $dbType === 'biginteger' || $dbType === 'boolean') { + $value = (int)$value; + if ($old !== null) { + $old = (int)$old; + } + } + if ($value == $old) { + continue; + } + if ($key === 'password' || $key === 'authkey') { + $value = '*****'; + if ($old !== null) { + $old = $value; + } + } + + if ($old === null) { + $changedFields[$key] = $value; + } else { + $changedFields[$key] = [$old, $value]; + } + } + return $changedFields; + } + + /** + * @return AuditLogs + */ + public function auditLogs() + { + if ($this->AuditLogs === null) { + $this->AuditLogs = TableRegistry::getTableLocator()->get('AuditLogs'); + } + return $this->AuditLogs; + } + + public function log() + { + + } +} diff --git a/src/Model/Behavior/AuthKeycloakBehavior.php b/src/Model/Behavior/AuthKeycloakBehavior.php index d42a8c9..12bb3e2 100644 --- a/src/Model/Behavior/AuthKeycloakBehavior.php +++ b/src/Model/Behavior/AuthKeycloakBehavior.php @@ -98,7 +98,7 @@ class AuthKeycloakBehavior extends Behavior { $individual = $this->_table->Individuals->find()->where( ['id' => $data['individual_id']] - )->contain(['Organisations'])->first(); + )->first(); $roleConditions = [ 'id' => $data['role_id'] ]; @@ -106,10 +106,9 @@ class AuthKeycloakBehavior extends Behavior $roleConditions['name'] = Configure::read('keycloak.default_role_name'); } $role = $this->_table->Roles->find()->where($roleConditions)->first(); - $orgs = []; - foreach ($individual['organisations'] as $org) { - $orgs[] = $org['uuid']; - } + $org = $this->_table->Organisations->find()->where([ + ['id' => $data['organisation_id']] + ])->first(); $token = $this->getAdminAccessToken(); $keyCloakUser = [ 'firstName' => $individual['first_name'], @@ -118,7 +117,7 @@ class AuthKeycloakBehavior extends Behavior 'email' => $individual['email'], 'attributes' => [ 'role_name' => empty($role['name']) ? Configure::read('keycloak.default_role_name') : $role['name'], - 'org_uuid' => empty($orgs[0]) ? '' : $orgs[0] + 'org_uuid' => $org['uuid'] ] ]; $keycloakConfig = Configure::read('keycloak'); diff --git a/src/Model/Entity/AppModel.php b/src/Model/Entity/AppModel.php index 84c86b3..50bfe86 100644 --- a/src/Model/Entity/AppModel.php +++ b/src/Model/Entity/AppModel.php @@ -6,6 +6,28 @@ use Cake\ORM\Entity; class AppModel extends Entity { + const BROTLI_HEADER = "\xce\xb2\xcf\x81"; + const BROTLI_MIN_LENGTH = 200; + + const ACTION_ADD = 'add', + ACTION_EDIT = 'edit', + ACTION_SOFT_DELETE = 'soft_delete', + ACTION_DELETE = 'delete', + ACTION_UNDELETE = 'undelete', + ACTION_TAG = 'tag', + ACTION_TAG_LOCAL = 'tag_local', + ACTION_REMOVE_TAG = 'remove_tag', + ACTION_REMOVE_TAG_LOCAL = 'remove_local_tag', + ACTION_LOGIN = 'login', + ACTION_LOGIN_FAIL = 'login_fail', + ACTION_LOGOUT = 'logout'; + + + public function getConstant($name) + { + return constant('self::' . $name); + } + public function getAccessibleFieldForNew(): array { return $this->_accessibleOnNew ?? []; diff --git a/src/Model/Entity/AuditLog.php b/src/Model/Entity/AuditLog.php new file mode 100644 index 0000000..74e2f41 --- /dev/null +++ b/src/Model/Entity/AuditLog.php @@ -0,0 +1,68 @@ +compressionEnabled = Configure::read('Cerebrate.log_compress') && function_exists('brotli_compress'); + parent::__construct($properties, $options); + } + + protected function _getTitle(): String + { + return $this->generateUserFriendlyTitle($this); + } + + /** + * @param string $change + * @return array|string + * @throws JsonException + */ + private function decodeChange($change) + { + if (substr($change, 0, 4) === self::BROTLI_HEADER) { + if (function_exists('brotli_uncompress')) { + $change = brotli_uncompress(substr($change, 4)); + if ($change === false) { + return 'Compressed'; + } + } else { + return 'Compressed'; + } + } + return json_decode($change, true); + } + + /** + * @param array $auditLog + * @return string + */ + public function generateUserFriendlyTitle($auditLog) + { + if (in_array($auditLog['request_action'], [self::ACTION_TAG, self::ACTION_TAG_LOCAL, self::ACTION_REMOVE_TAG, self::ACTION_REMOVE_TAG_LOCAL], true)) { + $attached = ($auditLog['request_action'] === self::ACTION_TAG || $auditLog['request_action'] === self::ACTION_TAG_LOCAL); + $local = ($auditLog['request_action'] === self::ACTION_TAG_LOCAL || $auditLog['request_action'] === self::ACTION_REMOVE_TAG_LOCAL) ? __('local') : __('global'); + if ($attached) { + return __('Attached %s tag "%s" to %s #%s', $local, $auditLog['model_title'], strtolower($auditLog['model']), $auditLog['model_id']); + } else { + return __('Detached %s tag "%s" from %s #%s', $local, $auditLog['model_title'], strtolower($auditLog['model']), $auditLog['model_id']); + } + } + + + $title = "{$auditLog['model']} #{$auditLog['model_id']}"; + + if (isset($auditLog['model_title']) && $auditLog['model_title']) { + $title .= ": {$auditLog['model_title']}"; + } + return $title; + } +} diff --git a/src/Model/Entity/Organisation.php b/src/Model/Entity/Organisation.php index bc78378..6766963 100644 --- a/src/Model/Entity/Organisation.php +++ b/src/Model/Entity/Organisation.php @@ -10,10 +10,5 @@ class Organisation extends AppModel protected $_accessible = [ '*' => true, 'id' => false, - 'uuid' => false, - ]; - - protected $_accessibleOnNew = [ - 'uuid' => true, ]; } diff --git a/src/Model/Entity/Outbox.php b/src/Model/Entity/Outbox.php new file mode 100644 index 0000000..51304e6 --- /dev/null +++ b/src/Model/Entity/Outbox.php @@ -0,0 +1,11 @@ +belongsTo('Individuals'); + $this->addBehavior('AuditLog'); $this->belongsTo('Organisations'); $this->addBehavior('Timestamp'); } diff --git a/src/Model/Table/AppTable.php b/src/Model/Table/AppTable.php index b9ce7d8..df88d0c 100644 --- a/src/Model/Table/AppTable.php +++ b/src/Model/Table/AppTable.php @@ -107,8 +107,9 @@ class AppTable extends Table $statistics['amount'] = $table->find()->all()->count(); if ($table->behaviors()->has('Timestamp') && $includeTimeline) { $statistics['timeline'] = $this->buildTimeline($table, $days, $field); - $statistics['variation'] = $table->find()->where(["{$field} >" => new \DateTime("-{$days} days")])->all()->count(); - -$statistics['amount']; + $startCount = $table->find()->where(["{$field} >" => new \DateTime("-{$days} days")])->all()->count(); + $endCount = $statistics['amount']; + $statistics['variation'] = $startCount - $endCount; } else { $statistics['timeline'] = []; $statistics['variation'] = 0; diff --git a/src/Model/Table/AuditLogsTable.php b/src/Model/Table/AuditLogsTable.php new file mode 100644 index 0000000..7987af6 --- /dev/null +++ b/src/Model/Table/AuditLogsTable.php @@ -0,0 +1,250 @@ +addBehavior('UUID'); + $this->addBehavior('Timestamp', [ + 'Model.beoreSave' => [ + 'created_at' => 'new' + ] + ]); + $this->belongsTo('Users'); + $this->setDisplayField('info'); + $this->compressionEnabled = Configure::read('Cerebrate.log_new_audit_compress') && function_exists('brotli_compress'); + } + + public function beforeMarshal(EventInterface $event, ArrayObject $data, ArrayObject $options) + { + if (!isset($data['request_ip'])) { + $ipHeader = 'REMOTE_ADDR'; + if (isset($_SERVER[$ipHeader])) { + $data['request_ip'] = $_SERVER[$ipHeader]; + } else { + $data['request_ip'] = '127.0.0.1'; + } + } + foreach (['user_id', 'request_type', 'authkey_id'] as $field) { + if (!isset($data[$field])) { + if (!isset($userInfo)) { + $userInfo = $this->userInfo(); + } + if (!empty($userInfo[$field])) { + $data[$field] = $userInfo[$field]; + } else { + $data[$field] = 0; + } + } + } + + if (!isset($data['request_id'] ) && isset($_SERVER['HTTP_X_REQUEST_ID'])) { + $data['request_id'] = $_SERVER['HTTP_X_REQUEST_ID']; + } + + // Truncate request_id + if (isset($data['request_id']) && strlen($data['request_id']) > 255) { + $data['request_id'] = substr($data['request_id'], 0, 255); + } + + // Truncate model title + if (isset($data['model_title']) && mb_strlen($data['model_title']) > 255) { + $data['model_title'] = mb_substr($data['model_title'], 0, 252) . '...'; + } + + if (isset($data['changed'])) { + $changed = json_encode($data['changed'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + if ($this->compressionEnabled && strlen($changed) >= self::BROTLI_MIN_LENGTH) { + $changed = self::BROTLI_HEADER . brotli_compress($changed, 4, BROTLI_TEXT); + } + $data['changed'] = $changed; + } + } + + public function beforeSave(EventInterface $event, EntityInterface $entity, ArrayObject $options) + { + $entity->request_ip = inet_pton($entity->request_ip); + $this->logData($entity); + return true; + } + + /** + * @param array $data + * @return bool + */ + private function logData(EntityInterface $entity) + { + if (Configure::read('Plugin.ZeroMQ_enable') && Configure::read('Plugin.ZeroMQ_audit_notifications_enable')) { + $pubSubTool = $this->getPubSubTool(); + $pubSubTool->publish($data, 'audit', 'log'); + } + + //$this->publishKafkaNotification('audit', $data, 'log'); + + if (Configure::read('Plugin.ElasticSearch_logging_enable')) { + // send off our logs to distributed /dev/null + $logIndex = Configure::read("Plugin.ElasticSearch_log_index"); + $elasticSearchClient = $this->getElasticSearchTool(); + $elasticSearchClient->pushDocument($logIndex, "log", $data); + } + + // write to syslogd as well if enabled + if ($this->syslog === null) { + if (Configure::read('Security.syslog')) { + $options = []; + $syslogToStdErr = Configure::read('Security.syslog_to_stderr'); + if ($syslogToStdErr !== null) { + $options['to_stderr'] = $syslogToStdErr; + } + $syslogIdent = Configure::read('Security.syslog_ident'); + if ($syslogIdent) { + $options['ident'] = $syslogIdent; + } + $this->syslog = new SysLog($options); + } else { + $this->syslog = false; + } + } + if ($this->syslog) { + $entry = $data['request_action']; + $title = $entity->generateUserFriendlyTitle(); + if ($title) { + $entry .= " -- $title"; + } + $this->syslog->write('info', $entry); + } + return true; + } + + /** + * @return array + */ + public function userInfo() + { + if ($this->user !== null) { + return $this->user; + } + + $this->user = ['id' => 0, /*'org_id' => 0, */'authkey_id' => 0, 'request_type' => self::REQUEST_TYPE_DEFAULT, 'name' => '']; + + $isShell = (php_sapi_name() === 'cli'); + if ($isShell) { + // do not start session for shell commands and fetch user info from configuration + $this->user['request_type'] = self::REQUEST_TYPE_CLI; + $currentUserId = Configure::read('CurrentUserId'); + if (!empty($currentUserId)) { + $this->user['id'] = $currentUserId; + $userFromDb = $this->Users->find()->where(['id' => $currentUserId])->first(); + $this->user['name'] = $userFromDb['name']; + $this->user['org_id'] = $userFromDb['org_id']; + } + } else { + $authUser = Router::getRequest()->getSession()->read('authUser'); + if (!empty($authUser)) { + $this->user['id'] = $authUser['id']; + $this->user['user_id'] = $authUser['id']; + $this->user['name'] = $authUser['name']; + //$this->user['org_id'] = $authUser['org_id']; + if (isset($authUser['logged_by_authkey']) && $authUser['logged_by_authkey']) { + $this->user['request_type'] = self::REQUEST_TYPE_API; + } + if (isset($authUser['authkey_id'])) { + $this->user['authkey_id'] = $authUser['authkey_id']; + } + } + } + return $this->user; + } + + public function insert(array $data) + { + $logEntity = $this->newEntity($data); + if ($logEntity->getErrors()) { + throw new Exception($logEntity->getErrors()); + } else { + $this->save($logEntity); + } + } + + /** + * @param string|int $org + * @return array + */ + public function returnDates($org = 'all') + { + $conditions = []; + if ($org !== 'all') { + $org = $this->Organisation->fetchOrg($org); + if (empty($org)) { + throw new NotFoundException('Invalid organisation.'); + } + $conditions['org_id'] = $org['id']; + } + + $dataSource = ConnectionManager::getDataSource('default')->config['datasource']; + if ($dataSource === 'Database/Mysql' || $dataSource === 'Database/MysqlObserver') { + $validDates = $this->find('all', [ + 'recursive' => -1, + 'fields' => ['DISTINCT UNIX_TIMESTAMP(DATE(created)) AS Date', 'count(id) AS count'], + 'conditions' => $conditions, + 'group' => ['Date'], + 'order' => ['Date'], + ]); + } elseif ($dataSource === 'Database/Postgres') { + if (!empty($conditions['org_id'])) { + $condOrg = sprintf('WHERE org_id = %s', intval($conditions['org_id'])); + } else { + $condOrg = ''; + } + $sql = 'SELECT DISTINCT EXTRACT(EPOCH FROM CAST(created AS DATE)) AS "Date", COUNT(id) AS count + FROM audit_logs + ' . $condOrg . ' + GROUP BY "Date" ORDER BY "Date"'; + $validDates = $this->query($sql); + } + $data = []; + foreach ($validDates as $date) { + $data[(int)$date[0]['Date']] = (int)$date[0]['count']; + } + return $data; + } +} diff --git a/src/Model/Table/AuthKeysTable.php b/src/Model/Table/AuthKeysTable.php index 3663af9..f5336e6 100644 --- a/src/Model/Table/AuthKeysTable.php +++ b/src/Model/Table/AuthKeysTable.php @@ -19,10 +19,11 @@ class AuthKeysTable extends AppTable { parent::initialize($config); $this->addBehavior('UUID'); + $this->addBehavior('AuditLog'); $this->belongsTo( 'Users' ); - $this->setDisplayField('authkey'); + $this->setDisplayField('comment'); } public function beforeMarshal(EventInterface $event, ArrayObject $data, ArrayObject $options) diff --git a/src/Model/Table/BroodsTable.php b/src/Model/Table/BroodsTable.php index e1993b4..b0d3dca 100644 --- a/src/Model/Table/BroodsTable.php +++ b/src/Model/Table/BroodsTable.php @@ -19,6 +19,7 @@ class BroodsTable extends AppTable parent::initialize($config); $this->addBehavior('UUID'); $this->addBehavior('Timestamp'); + $this->addBehavior('AuditLog'); $this->BelongsTo( 'Organisations' ); @@ -278,7 +279,7 @@ class BroodsTable extends AppTable } return $jsonReply; } - + /** * handleSendingFailed - Handle the case if the request could not be sent or if the remote rejected the connection request * @@ -302,7 +303,7 @@ class BroodsTable extends AppTable ]; return $creationResult; } - + /** * handleMessageNotCreated - Handle the case if the request was sent but the remote brood did not save the message in the inbox * diff --git a/src/Model/Table/EncryptionKeysTable.php b/src/Model/Table/EncryptionKeysTable.php index 23b4867..2008e0d 100644 --- a/src/Model/Table/EncryptionKeysTable.php +++ b/src/Model/Table/EncryptionKeysTable.php @@ -14,6 +14,7 @@ class EncryptionKeysTable extends AppTable { parent::initialize($config); $this->addBehavior('UUID'); + $this->addBehavior('AuditLog'); $this->addBehavior('Timestamp'); $this->belongsTo( 'Individuals', diff --git a/src/Model/Table/InboxTable.php b/src/Model/Table/InboxTable.php index 0c62a80..18faf47 100644 --- a/src/Model/Table/InboxTable.php +++ b/src/Model/Table/InboxTable.php @@ -19,7 +19,7 @@ class InboxTable extends AppTable parent::initialize($config); $this->addBehavior('UUID'); $this->addBehavior('Timestamp'); - + $this->addBehavior('AuditLog'); $this->belongsTo('Users'); $this->setDisplayField('title'); } @@ -68,7 +68,7 @@ class InboxTable extends AppTable if (empty($brood)) { $errors[] = __('Unkown brood `{0}`', $entryData['data']['cerebrateURL']); } - + // $found = false; // foreach ($user->individual->organisations as $organisations) { // if ($organisations->id == $brood->organisation_id) { diff --git a/src/Model/Table/IndividualsTable.php b/src/Model/Table/IndividualsTable.php index d1c2cdf..0fba61f 100644 --- a/src/Model/Table/IndividualsTable.php +++ b/src/Model/Table/IndividualsTable.php @@ -15,6 +15,7 @@ class IndividualsTable extends AppTable $this->addBehavior('Timestamp'); $this->addBehavior('Tags.Tag'); $this->addBehavior('MetaFields'); + $this->addBehavior('AuditLog'); $this->hasMany( 'Alignments', diff --git a/src/Model/Table/InstanceTable.php b/src/Model/Table/InstanceTable.php index 8d3a86c..0832f4d 100644 --- a/src/Model/Table/InstanceTable.php +++ b/src/Model/Table/InstanceTable.php @@ -18,6 +18,8 @@ class InstanceTable extends AppTable public function initialize(array $config): void { parent::initialize($config); + $this->addBehavior('AuditLog'); + $this->setDisplayField('name'); } public function validationDefault(Validator $validator): Validator @@ -38,6 +40,18 @@ class InstanceTable extends AppTable public function searchAll($value, $limit=5, $model=null) { $results = []; + + // search in metafields. FIXME: To be replaced by the meta-template system + $metaFieldTable = TableRegistry::get('MetaFields'); + $query = $metaFieldTable->find()->where([ + 'value LIKE' => '%' . $value . '%' + ]); + $results['MetaFields']['amount'] = $query->count(); + $result = $query->limit($limit)->all()->toList(); + if (!empty($result)) { + $results['MetaFields']['entries'] = $result; + } + $models = $this->seachAllTables; if (!is_null($model)) { if (in_array($model, $this->seachAllTables)) { diff --git a/src/Model/Table/LocalToolsTable.php b/src/Model/Table/LocalToolsTable.php index 8f2bfea..f764ace 100644 --- a/src/Model/Table/LocalToolsTable.php +++ b/src/Model/Table/LocalToolsTable.php @@ -30,6 +30,7 @@ class LocalToolsTable extends AppTable public function initialize(array $config): void { parent::initialize($config); + $this->addBehavior('AuditLog'); $this->addBehavior('Timestamp'); } @@ -142,7 +143,8 @@ class LocalToolsTable extends AppTable 'connector' => $connector_type, 'connector_version' => $connector_class->version, 'connector_description' => $connector_class->description, - 'connector_settings' => $connector_class->settings ?? [] + 'connector_settings' => $connector_class->settings ?? [], + 'connector_settings_placeholder' => $connector_class->settingsPlaceholder ?? [], ]; if ($includeConnections) { $connector['connections'] = $this->healthCheck($connector_type, $connector_class); @@ -287,6 +289,7 @@ class LocalToolsTable extends AppTable return $jsonReply; } +/* public function findConnectable($local_tool): array { $connectors = $this->getInterconnectors($local_tool['connector']); @@ -296,8 +299,8 @@ class LocalToolsTable extends AppTable $validTargets[$connector['connects'][1]] = 1; } } - } +*/ public function fetchConnection($id): object { diff --git a/src/Model/Table/MetaFieldsTable.php b/src/Model/Table/MetaFieldsTable.php index 639b306..6fffc93 100644 --- a/src/Model/Table/MetaFieldsTable.php +++ b/src/Model/Table/MetaFieldsTable.php @@ -18,6 +18,7 @@ class MetaFieldsTable extends AppTable 'MetaTemplateFields' => ['counter'] ]); + $this->addBehavior('AuditLog'); $this->belongsTo('MetaTemplates'); $this->belongsTo('MetaTemplateFields'); diff --git a/src/Model/Table/OrganisationsTable.php b/src/Model/Table/OrganisationsTable.php index 785128c..b3fa792 100644 --- a/src/Model/Table/OrganisationsTable.php +++ b/src/Model/Table/OrganisationsTable.php @@ -16,8 +16,10 @@ class OrganisationsTable extends AppTable public function initialize(array $config): void { parent::initialize($config); + $this->addBehavior('UUID'); $this->addBehavior('Timestamp'); $this->addBehavior('Tags.Tag'); + $this->addBehavior('AuditLog'); $this->hasMany( 'Alignments', [ diff --git a/src/Model/Table/OutboxProcessorsTable.php b/src/Model/Table/OutboxProcessorsTable.php index da26692..5812cb4 100644 --- a/src/Model/Table/OutboxProcessorsTable.php +++ b/src/Model/Table/OutboxProcessorsTable.php @@ -28,6 +28,7 @@ class OutboxProcessorsTable extends AppTable if (empty($this->outboxProcessors)) { $this->loadProcessors(); } + $this->addBehavior('AuditLog'); } public function getProcessor($scope, $action=null) @@ -87,7 +88,7 @@ class OutboxProcessorsTable extends AppTable } } } - + /** * getProcessorClass * @@ -112,7 +113,7 @@ class OutboxProcessorsTable extends AppTable return $e->getMessage(); } } - + /** * createOutboxEntry * diff --git a/src/Model/Table/OutboxTable.php b/src/Model/Table/OutboxTable.php index 02fd442..a78e0c6 100644 --- a/src/Model/Table/OutboxTable.php +++ b/src/Model/Table/OutboxTable.php @@ -19,8 +19,8 @@ class OutboxTable extends AppTable parent::initialize($config); $this->addBehavior('UUID'); $this->addBehavior('Timestamp'); - $this->belongsTo('Users'); + $this->addBehavior('AuditLog'); $this->setDisplayField('title'); } diff --git a/src/Model/Table/RemoteToolConnectionsTable.php b/src/Model/Table/RemoteToolConnectionsTable.php index 7e8cf21..1e1ee25 100644 --- a/src/Model/Table/RemoteToolConnectionsTable.php +++ b/src/Model/Table/RemoteToolConnectionsTable.php @@ -18,6 +18,7 @@ class RemoteToolConnectionsTable extends AppTable 'LocalTools' ); $this->setDisplayField('id'); + $this->addBehavior('AuditLog'); } public function validationDefault(Validator $validator): Validator diff --git a/src/Model/Table/RolesTable.php b/src/Model/Table/RolesTable.php index 3973b1b..74f290b 100644 --- a/src/Model/Table/RolesTable.php +++ b/src/Model/Table/RolesTable.php @@ -12,6 +12,7 @@ class RolesTable extends AppTable { parent::initialize($config); $this->addBehavior('UUID'); + $this->addBehavior('AuditLog'); $this->hasMany( 'Users', [ diff --git a/src/Model/Table/SettingProviders/CerebrateSettingsProvider.php b/src/Model/Table/SettingProviders/CerebrateSettingsProvider.php index 25365de..fa80078 100644 --- a/src/Model/Table/SettingProviders/CerebrateSettingsProvider.php +++ b/src/Model/Table/SettingProviders/CerebrateSettingsProvider.php @@ -211,6 +211,13 @@ class CerebrateSettingsProvider extends BaseSettingsProvider }, 'dependsOn' => 'keycloak.enabled' ], + 'keycloak.screw' => [ + 'name' => 'Screw', + 'type' => 'string', + 'severity' => 'info', + 'default' => 0, + 'description' => __('The misalignment allowed when validating JWT tokens between cerebrate and keycloak. Whilst crisp timings are essential for any timing push, perfect timing is only achievable by GSL participants. (given in seconds)') + ], 'keycloak.mapping.org_uuid' => [ 'name' => 'org_uuid mapping', 'type' => 'string', diff --git a/src/Model/Table/SettingsTable.php b/src/Model/Table/SettingsTable.php index c2d71c9..7e9bcff 100644 --- a/src/Model/Table/SettingsTable.php +++ b/src/Model/Table/SettingsTable.php @@ -26,6 +26,7 @@ class SettingsTable extends AppTable parent::initialize($config); $this->setTable(false); $this->SettingsProvider = new CerebrateSettingsProvider(); + $this->addBehavior('AuditLog'); } public function getSettings($full=false): array diff --git a/src/Model/Table/SharingGroupsTable.php b/src/Model/Table/SharingGroupsTable.php index ec3791e..ff39220 100644 --- a/src/Model/Table/SharingGroupsTable.php +++ b/src/Model/Table/SharingGroupsTable.php @@ -15,6 +15,7 @@ class SharingGroupsTable extends AppTable parent::initialize($config); $this->addBehavior('UUID'); $this->addBehavior('Timestamp'); + $this->addBehavior('AuditLog'); $this->belongsTo( 'Users' ); diff --git a/src/Model/Table/UserSettingsTable.php b/src/Model/Table/UserSettingsTable.php index 4c7e708..bdfe535 100644 --- a/src/Model/Table/UserSettingsTable.php +++ b/src/Model/Table/UserSettingsTable.php @@ -11,7 +11,7 @@ use App\Settings\SettingsProvider\UserSettingsProvider; class UserSettingsTable extends AppTable { - protected $BOOKMARK_SETTING_NAME = 'ui.bookmarks'; + public $BOOKMARK_SETTING_NAME = 'ui.bookmarks'; public function initialize(array $config): void { diff --git a/src/Model/Table/UsersTable.php b/src/Model/Table/UsersTable.php index d9804a4..61f06b8 100644 --- a/src/Model/Table/UsersTable.php +++ b/src/Model/Table/UsersTable.php @@ -12,6 +12,7 @@ use \Cake\Http\Session; use Cake\Http\Client; use Cake\Utility\Security; use Cake\Core\Configure; +use Cake\Utility\Text; class UsersTable extends AppTable { @@ -20,6 +21,7 @@ class UsersTable extends AppTable parent::initialize($config); $this->addBehavior('Timestamp'); $this->addBehavior('UUID'); + $this->addBehavior('AuditLog'); $this->initAuthBehaviors(); $this->belongsTo( 'Individuals', @@ -35,6 +37,13 @@ class UsersTable extends AppTable 'cascadeCallbacks' => false ] ); + $this->belongsTo( + 'Organisations', + [ + 'dependent' => false, + 'cascadeCallbacks' => false + ] + ); $this->hasMany( 'UserSettings', [ @@ -88,15 +97,35 @@ class UsersTable extends AppTable return $rules; } + public function test() + { + $this->Roles = TableRegistry::get('Roles'); + $role = $this->Roles->newEntity([ + 'name' => 'admin', + 'perm_admin' => 1, + 'perm_org_admin' => 1, + 'perm_sync' => 1 + ]); + $this->Roles->save($role); + } + public function checkForNewInstance(): bool { if (empty($this->find()->first())) { $this->Roles = TableRegistry::get('Roles'); $role = $this->Roles->newEntity([ 'name' => 'admin', - 'perm_admin' => 1 + 'perm_admin' => 1, + 'perm_org_admin' => 1, + 'perm_sync' => 1 ]); $this->Roles->save($role); + $this->Organisations = TableRegistry::get('Organisations'); + $organisation = $this->Organisations->newEntity([ + 'name' => 'default_organisation', + 'uuid' => Text::uuid() + ]); + $this->Organisations->save($organisation); $this->Individuals = TableRegistry::get('Individuals'); $individual = $this->Individuals->newEntity([ 'email' => 'admin@admin.test', @@ -108,6 +137,7 @@ class UsersTable extends AppTable 'username' => 'admin', 'password' => 'Password1234', 'individual_id' => $individual->id, + 'organisation_id' => $organisation->id, 'role_id' => $role->id ]); $this->save($user); diff --git a/src/View/AppView.php b/src/View/AppView.php index 7636a87..9018168 100644 --- a/src/View/AppView.php +++ b/src/View/AppView.php @@ -44,5 +44,6 @@ class AppView extends View $this->loadHelper('FormFieldMassage'); $this->loadHelper('Paginator', ['templates' => 'cerebrate-pagination-templates']); $this->loadHelper('Tags.Tag'); + $this->loadHelper('ACL'); } } diff --git a/src/View/Helper/ACLHelper.php b/src/View/Helper/ACLHelper.php new file mode 100644 index 0000000..e563e82 --- /dev/null +++ b/src/View/Helper/ACLHelper.php @@ -0,0 +1,25 @@ +roleAccess)) { + $this->roleAccess = $this->getView()->get('roleAccess'); + } + if ( + in_array($action, $this->roleAccess['*']) || + (isset($this->roleAccess[$controller]) && in_array($action, $this->roleAccess[$controller])) + ) { + return true; + } else { + return false; + } + } +} diff --git a/src/View/Helper/BootstrapHelper.php b/src/View/Helper/BootstrapHelper.php index f248c6f..5353411 100644 --- a/src/View/Helper/BootstrapHelper.php +++ b/src/View/Helper/BootstrapHelper.php @@ -651,13 +651,13 @@ class BoostrapTable extends BootstrapGeneric ], ]); foreach ($this->items as $i => $row) { - $body .= $this->genRow($row); + $body .= $this->genRow($row, $i); } $body .= $this->closeNode('tbody'); return $body; } - private function genRow($row) + private function genRow($row, $rowIndex) { $html = $this->openNode('tr', [ 'class' => [ @@ -672,21 +672,21 @@ class BoostrapTable extends BootstrapGeneric $key = $field; } $cellValue = Hash::get($row, $key); - $html .= $this->genCell($cellValue, $field, $row, $i); + $html .= $this->genCell($cellValue, $field, $row, $rowIndex); } } else { // indexed array - foreach ($row as $cellValue) { - $html .= $this->genCell($cellValue, $field, $row, $i); + foreach ($row as $i => $cellValue) { + $html .= $this->genCell($cellValue, 'index', $row, $rowIndex); } } $html .= $this->closeNode('tr'); return $html; } - private function genCell($value, $field = [], $row = [], $i = 0) + private function genCell($value, $field=[], $row=[], $rowIndex=0) { if (isset($field['formatter'])) { - $cellContent = $field['formatter']($value, $row, $i); + $cellContent = $field['formatter']($value, $row, $rowIndex); } else if (isset($field['element'])) { $cellContent = $this->btHelper->getView()->element($field['element'], [ 'data' => [$value], diff --git a/templates/Api/index.php b/templates/Api/index.php new file mode 100644 index 0000000..96be4b8 --- /dev/null +++ b/templates/Api/index.php @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/templates/AuditLogs/index.php b/templates/AuditLogs/index.php new file mode 100644 index 0000000..c4657cb --- /dev/null +++ b/templates/AuditLogs/index.php @@ -0,0 +1,65 @@ +element('genericElements/IndexTable/index_table', [ + 'data' => [ + 'data' => $data, + 'top_bar' => [ + 'children' => [ + [ + 'type' => 'search', + 'button' => __('Filter'), + 'placeholder' => __('Enter value to search'), + 'data' => '', + 'searchKey' => 'value' + ] + ] + ], + 'fields' => [ + [ + 'name' => '#', + 'sort' => 'id', + 'data_path' => 'id', + ], + [ + 'name' => __('IP'), + 'sort' => 'request_ip', + 'data_path' => 'request_ip', + ], + [ + 'name' => __('Username'), + 'sort' => 'user.username', + 'data_path' => 'user.username', + ], + [ + 'name' => __('Title'), + 'data_path' => 'title', + ], + [ + 'name' => __('Model'), + 'sort' => 'model', + 'data_path' => 'model', + ], + [ + 'name' => __('Model ID'), + 'sort' => 'model', + 'data_path' => 'model_id', + ], + [ + 'name' => __('Action'), + 'sort' => 'request_action', + 'data_path' => 'request_action', + ], + [ + 'name' => __('Changed'), + 'sort' => 'changed', + 'data_path' => 'changed', + 'element' => 'json' + ], + ], + 'title' => __('Logs'), + 'description' => null, + 'pull' => 'right', + 'actions' => [] + ] +]); +echo ''; +?> diff --git a/templates/EncryptionKeys/view.php b/templates/EncryptionKeys/view.php new file mode 100644 index 0000000..e92da92 --- /dev/null +++ b/templates/EncryptionKeys/view.php @@ -0,0 +1,32 @@ +element( + '/genericElements/SingleViews/single_view', + [ + 'data' => $entity, + 'fields' => [ + [ + 'key' => __('ID'), + 'path' => 'id' + ], + [ + 'key' => __('Type'), + 'path' => 'type' + ], + [ + 'key' => __('Owner'), + 'path' => 'owner_id', + 'owner_model_path' => 'owner_model', + 'type' => 'owner' + ], + [ + 'key' => __('Revoked'), + 'path' => 'revoked' + ], + + [ + 'key' => __('Key'), + 'path' => 'encryption_key' + ] + ] + ] +); diff --git a/templates/Individuals/add.php b/templates/Individuals/add.php index 0726623..436d6fa 100644 --- a/templates/Individuals/add.php +++ b/templates/Individuals/add.php @@ -23,7 +23,8 @@ ), array( 'field' => 'tag_list', - 'type' => 'tags' + 'type' => 'tags', + 'requirements' => $this->request->getParam('action') === 'edit' ), ), 'submit' => array( diff --git a/templates/Instance/search_all.php b/templates/Instance/search_all.php index 76c46f6..bd6bb17 100644 --- a/templates/Instance/search_all.php +++ b/templates/Instance/search_all.php @@ -19,14 +19,25 @@ ', h($tableName), $tableResult['amount']); foreach ($tableResult['entries'] as $entry) { - $section .= sprintf('%s', - Cake\Routing\Router::URL([ - 'controller' => Cake\Utility\Inflector::pluralize($entry->getSource()), - 'action' => 'view', - h($entry['id']) - ]), - h($entry[$fieldPath]) - ); + if ($entry->getSource() == 'MetaFields') { + $section .= sprintf('%s', + Cake\Routing\Router::URL([ + 'controller' => Cake\Utility\Inflector::pluralize($entry->scope), + 'action' => 'view', + h($entry->parent_id) + ]), + sprintf('%s (%s::%s)', h($entry->value), h($entry->scope), h($entry->field)) + ); + } else { + $section .= sprintf('%s', + Cake\Routing\Router::URL([ + 'controller' => Cake\Utility\Inflector::pluralize($entry->getSource()), + 'action' => 'view', + h($entry['id']) + ]), + h($entry[$fieldPath]) + ); + } } $remaining = $tableResult['amount'] - count($tableResult['entries']); if ($remaining > 0) { diff --git a/templates/LocalTools/add.php b/templates/LocalTools/add.php index 184f0e6..5197f21 100644 --- a/templates/LocalTools/add.php +++ b/templates/LocalTools/add.php @@ -22,7 +22,8 @@ 'codemirror' => [ 'height' => '10rem', 'hints' => $connectors[0]['connector_settings'] - ] + ], + 'placeholder' => json_encode($connectors[0]['connector_settings_placeholder'], JSON_FORCE_OBJECT | JSON_PRETTY_PRINT) ], [ 'field' => 'description', diff --git a/templates/LocalTools/connector_index.php b/templates/LocalTools/connector_index.php index ed5d221..c896b17 100644 --- a/templates/LocalTools/connector_index.php +++ b/templates/LocalTools/connector_index.php @@ -89,12 +89,14 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'url_params_data_paths' => ['id'], 'icon' => 'eye' ], + /* [ 'open_modal' => '/localTools/connectLocal/[onclick_params_data_path]', 'modal_params_data_path' => 'id', 'reload_url' => sprintf('/localTools/connectorIndex/%s', h($connectorName)), 'icon' => 'plug' ], + */ [ 'open_modal' => '/localTools/edit/[onclick_params_data_path]', 'modal_params_data_path' => 'id', diff --git a/templates/Organisations/add.php b/templates/Organisations/add.php index 77d4ca8..72b9a9e 100644 --- a/templates/Organisations/add.php +++ b/templates/Organisations/add.php @@ -7,17 +7,13 @@ array( 'field' => 'name' ), - array( - 'field' => 'description', - 'type' => 'textarea' - ), array( 'field' => 'uuid', 'label' => 'UUID', 'type' => 'uuid' ), array( - 'field' => 'URL' + 'field' => 'url' ), array( 'field' => 'nationality' diff --git a/templates/Organisations/index.php b/templates/Organisations/index.php index bb781a3..34e4a10 100644 --- a/templates/Organisations/index.php +++ b/templates/Organisations/index.php @@ -97,12 +97,14 @@ echo $this->element('genericElements/IndexTable/index_table', [ [ 'open_modal' => '/organisations/edit/[onclick_params_data_path]', 'modal_params_data_path' => 'id', - 'icon' => 'edit' + 'icon' => 'edit', + 'requirement' => $loggedUser['role']['perm_admin'] ], [ 'open_modal' => '/organisations/delete/[onclick_params_data_path]', 'modal_params_data_path' => 'id', - 'icon' => 'trash' + 'icon' => 'trash', + 'requirement' => $loggedUser['role']['perm_admin'] ], ] ] diff --git a/templates/Roles/index.php b/templates/Roles/index.php index 4271700..d846934 100644 --- a/templates/Roles/index.php +++ b/templates/Roles/index.php @@ -78,12 +78,14 @@ echo $this->element('genericElements/IndexTable/index_table', [ [ 'open_modal' => '/roles/edit/[onclick_params_data_path]', 'modal_params_data_path' => 'id', - 'icon' => 'edit' + 'icon' => 'edit', + 'requirement' => !empty($loggedUser['role']['perm_admin']) ], [ 'open_modal' => '/roles/delete/[onclick_params_data_path]', 'modal_params_data_path' => 'id', - 'icon' => 'trash' + 'icon' => 'trash', + 'requirement' => !empty($loggedUser['role']['perm_admin']) ], ] ] diff --git a/templates/SharingGroups/index.php b/templates/SharingGroups/index.php index 5a353ed..0162c23 100644 --- a/templates/SharingGroups/index.php +++ b/templates/SharingGroups/index.php @@ -10,7 +10,8 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'data' => [ 'type' => 'simple', 'text' => __('Add sharing group'), - 'popover_url' => '/SharingGroups/add' + 'popover_url' => '/SharingGroups/add', + 'requirement' => $this->ACL->checkAccess('SharingGroups', 'add') ] ] ], @@ -35,6 +36,11 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'class' => 'short', 'data_path' => 'name', ], + [ + 'name' => __('Owner'), + 'class' => 'short', + 'data_path' => 'organisation.name' + ], [ 'name' => __('UUID'), 'sort' => 'uuid', @@ -43,9 +49,9 @@ echo $this->element('genericElements/IndexTable/index_table', [ ], [ 'name' => __('Members'), - 'data_path' => 'alignments', + 'data_path' => 'sharing_group_orgs', 'element' => 'count_summary', - 'url' => '/sharingGroups/view/{{id}}', + 'url' => '/sharingGroups/view/{{url_data}}', 'url_data_path' => 'id' ] ], diff --git a/templates/Users/add.php b/templates/Users/add.php index 99bf366..a3c90a8 100644 --- a/templates/Users/add.php +++ b/templates/Users/add.php @@ -14,12 +14,19 @@ 'field' => 'username', 'autocomplete' => 'off' ], + [ + 'field' => 'organisation_id', + 'type' => 'dropdown', + 'label' => __('Associated organisation'), + 'options' => $dropdownData['organisation'] + ], [ 'field' => 'password', 'label' => __('Password'), 'type' => 'password', 'required' => $this->request->getParam('action') === 'add' ? 'required' : false, - 'autocomplete' => 'new-password' + 'autocomplete' => 'new-password', + 'value' => '' ], [ 'field' => 'confirm_password', diff --git a/templates/Users/index.php b/templates/Users/index.php index 1bad3e5..9caa7de 100644 --- a/templates/Users/index.php +++ b/templates/Users/index.php @@ -51,6 +51,13 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'sort' => 'username', 'data_path' => 'username', ], + [ + 'name' => __('Organisation'), + 'sort' => 'organisation.name', + 'data_path' => 'organisation.name', + 'url' => '/organisations/view/{{0}}', + 'url_vars' => ['organisation.id'] + ], [ 'name' => __('Email'), 'sort' => 'individual.email', diff --git a/templates/Users/login.php b/templates/Users/login.php index d5cab0e..1f62282 100644 --- a/templates/Users/login.php +++ b/templates/Users/login.php @@ -24,7 +24,7 @@ use Cake\Core\Configure; echo $this->Form->control('password', ['type' => 'password', 'label' => 'Password', 'class' => 'form-control mb-3', 'placeholder' => __('Password')]); echo $this->Form->control(__('Login'), ['type' => 'submit', 'class' => 'btn btn-primary']); echo $this->Form->end(); - if (!empty(Configure::read('Cerebrate')['security.registration.self-registration'])) { + if (!empty(Configure::read('security.registration.self-registration'))) { echo '
'; echo sprintf('%s %s', __('Doesn\'t have an account?'), __('Sign up')); echo '
'; @@ -55,4 +55,4 @@ use Cake\Core\Configure; echo $this->Form->end(); } ?> - \ No newline at end of file + diff --git a/templates/Users/view.php b/templates/Users/view.php index 760ead4..fbddf52 100644 --- a/templates/Users/view.php +++ b/templates/Users/view.php @@ -21,6 +21,14 @@ echo $this->element( 'path' => 'individual.email' ], [ + 'type' => 'generic', + 'key' => __('Organisation'), + 'path' => 'organisation.name', + 'url' => '/organisations/view/{{0}}', + 'url_vars' => 'organisation.id' + ], + [ + 'type' => 'generic', 'key' => __('Role'), 'path' => 'role.name', 'url' => '/roles/view/{{0}}', @@ -48,8 +56,8 @@ echo $this->element( 'title' => __('Authentication keys') ], [ - 'url' => '/EncryptionKeys/index?Users.id={{0}}', - 'url_params' => ['id'], + 'url' => '/EncryptionKeys/index?owner_id={{0}}', + 'url_params' => ['individual_id'], 'title' => __('Encryption keys') ], [ diff --git a/templates/element/genericElements/Form/Fields/dropdownField.php b/templates/element/genericElements/Form/Fields/dropdownField.php index 017e25f..a00f0e9 100644 --- a/templates/element/genericElements/Form/Fields/dropdownField.php +++ b/templates/element/genericElements/Form/Fields/dropdownField.php @@ -2,7 +2,7 @@ $controlParams = [ 'options' => $fieldData['options'], 'empty' => $fieldData['empty'] ?? false, - 'value' => $fieldData['value'] ?? [], + 'value' => $fieldData['value'] ?? null, 'multiple' => $fieldData['multiple'] ?? false, 'disabled' => $fieldData['disabled'] ?? false, 'class' => ($fieldData['class'] ?? '') . ' formDropdown form-select' diff --git a/templates/element/genericElements/Form/Fields/passwordField.php b/templates/element/genericElements/Form/Fields/passwordField.php new file mode 100644 index 0000000..6831ce6 --- /dev/null +++ b/templates/element/genericElements/Form/Fields/passwordField.php @@ -0,0 +1,6 @@ +FormFieldMassage->prepareFormElement($this->Form, $params, $fieldData); +?> diff --git a/templates/element/genericElements/IndexTable/Fields/json.php b/templates/element/genericElements/IndexTable/Fields/json.php index 6c14248..2aca821 100644 --- a/templates/element/genericElements/IndexTable/Fields/json.php +++ b/templates/element/genericElements/IndexTable/Fields/json.php @@ -1,9 +1,12 @@ Hash->extract($row, $field['data_path'])); + $data = $this->Hash->extract($row, $field['data_path']); // I feed dirty for this... if (is_array($data) && count($data) === 1 && isset($data[0])) { $data = $data[0]; } + if (!is_array($data)) { + $data = json_decode($data, true); + } echo sprintf( '
', h($k) diff --git a/templates/element/genericElements/SingleViews/Fields/ownerField.php b/templates/element/genericElements/SingleViews/Fields/ownerField.php new file mode 100644 index 0000000..4f213a7 --- /dev/null +++ b/templates/element/genericElements/SingleViews/Fields/ownerField.php @@ -0,0 +1,6 @@ +element('/genericElements/IndexTable/Fields/owner', [ + 'field' => $field, + 'row' => $data +]); +?> diff --git a/templates/element/layouts/header/header-profile.php b/templates/element/layouts/header/header-profile.php index a35e041..146aa3e 100644 --- a/templates/element/layouts/header/header-profile.php +++ b/templates/element/layouts/header/header-profile.php @@ -8,10 +8,13 @@ use Cake\Routing\Router; \ No newline at end of file + diff --git a/templates/layout/default.php b/templates/layout/default.php index 849592e..59d2efe 100644 --- a/templates/layout/default.php +++ b/templates/layout/default.php @@ -52,6 +52,7 @@ $sidebarOpen = $loggedUser->user_settings_by_name_with_fallback['ui.sidebar.expa Html->script('CodeMirror/addon/lint/json-lint') ?> Html->script('CodeMirror/addon/edit/matchbrackets') ?> Html->script('CodeMirror/addon/edit/closebrackets') ?> + Html->script('CodeMirror/addon/display/placeholder') ?> Html->css('CodeMirror/codemirror') ?> Html->css('CodeMirror/codemirror-additional') ?> Html->css('CodeMirror/addon/hint/show-hint') ?> diff --git a/tests/Fixture/AuthKeysFixture.php b/tests/Fixture/AuthKeysFixture.php new file mode 100644 index 0000000..24034f3 --- /dev/null +++ b/tests/Fixture/AuthKeysFixture.php @@ -0,0 +1,85 @@ +records = [ + [ + 'id' => self::ADMIN_API_ID, + 'uuid' => $faker->uuid(), + 'authkey' => $hasher->hash(self::ADMIN_API_KEY), + 'authkey_start' => substr(self::ADMIN_API_KEY, 0, 4), + 'authkey_end' => substr(self::ADMIN_API_KEY, -4), + 'expiration' => 0, + 'user_id' => UsersFixture::USER_ADMIN_ID, + 'comment' => '', + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ], + [ + 'id' => self::SYNC_API_ID, + 'uuid' => $faker->uuid(), + 'authkey' => $hasher->hash(self::SYNC_API_KEY), + 'authkey_start' => substr(self::SYNC_API_KEY, 0, 4), + 'authkey_end' => substr(self::SYNC_API_KEY, -4), + 'expiration' => 0, + 'user_id' => UsersFixture::USER_SYNC_ID, + 'comment' => '', + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ], + [ + 'id' => self::ORG_ADMIN_API_ID, + 'uuid' => $faker->uuid(), + 'authkey' => $hasher->hash(self::ORG_ADMIN_API_KEY), + 'authkey_start' => substr(self::ORG_ADMIN_API_KEY, 0, 4), + 'authkey_end' => substr(self::ORG_ADMIN_API_KEY, -4), + 'expiration' => 0, + 'user_id' => UsersFixture::USER_ORG_ADMIN_ID, + 'comment' => '', + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ], + [ + 'id' => self::REGULAR_USER_API_ID, + 'uuid' => $faker->uuid(), + 'authkey' => $hasher->hash(self::REGULAR_USER_API_KEY), + 'authkey_start' => substr(self::REGULAR_USER_API_KEY, 0, 4), + 'authkey_end' => substr(self::REGULAR_USER_API_KEY, -4), + 'expiration' => 0, + 'user_id' => UsersFixture::USER_REGULAR_USER_ID, + 'comment' => '', + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ] + ]; + parent::init(); + } +} diff --git a/tests/Fixture/BroodsFixture.php b/tests/Fixture/BroodsFixture.php new file mode 100644 index 0000000..c329e6a --- /dev/null +++ b/tests/Fixture/BroodsFixture.php @@ -0,0 +1,72 @@ +records = [ + [ + 'id' => self::BROOD_A_ID, + 'uuid' => $faker->uuid(), + 'name' => 'Brood A', + 'url' => $faker->url, + 'description' => $faker->text, + 'organisation_id' => OrganisationsFixture::ORGANISATION_A_ID, + 'trusted' => true, + 'pull' => true, + 'skip_proxy' => true, + 'authkey' => self::BROOD_A_API_KEY, + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ], + [ + 'id' => self::BROOD_B_ID, + 'uuid' => $faker->uuid(), + 'name' => 'Brood A', + 'url' => $faker->url, + 'description' => $faker->text, + 'organisation_id' => OrganisationsFixture::ORGANISATION_B_ID, + 'trusted' => true, + 'pull' => true, + 'skip_proxy' => true, + 'authkey' => self::BROOD_B_API_KEY, + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ], + [ + 'id' => self::BROOD_WIREMOCK_ID, + 'uuid' => $faker->uuid(), + 'name' => 'wiremock', + 'url' => 'http://localhost:8080', + 'description' => $faker->text, + 'organisation_id' => OrganisationsFixture::ORGANISATION_B_ID, + 'trusted' => true, + 'pull' => true, + 'skip_proxy' => true, + 'authkey' => self::BROOD_WIREMOCK_API_KEY, + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ] + ]; + parent::init(); + } +} diff --git a/tests/Fixture/EncryptionKeysFixture.php b/tests/Fixture/EncryptionKeysFixture.php new file mode 100644 index 0000000..a9d6c27 --- /dev/null +++ b/tests/Fixture/EncryptionKeysFixture.php @@ -0,0 +1,133 @@ +records = [ + [ + 'id' => self::ENCRYPTION_KEY_ORG_A_ID, + 'uuid' => $faker->uuid(), + 'type' => self::TYPE_PGP, + 'encryption_key' => $this->getPublicKey(self::KEY_TYPE_EDCH), + 'revoked' => false, + 'expires' => null, + 'owner_id' => OrganisationsFixture::ORGANISATION_A_ID, + 'owner_model' => 'Organisation', + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ], + [ + 'id' => self::ENCRYPTION_KEY_ORG_B_ID, + 'uuid' => $faker->uuid(), + 'type' => self::TYPE_PGP, + 'encryption_key' => $this->getPublicKey(self::KEY_TYPE_EDCH), + 'revoked' => false, + 'expires' => null, + 'owner_id' => OrganisationsFixture::ORGANISATION_B_ID, + 'owner_model' => 'Organisation', + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ], + ]; + parent::init(); + } + + public static function getPublicKey(string $type): string + { + switch ($type) { + case self::KEY_TYPE_EDCH: + return <<records = [ + [ + 'id' => self::INBOX_USER_REGISTRATION_ID, + 'uuid' => $faker->uuid(), + 'scope' => 'User', + 'action' => 'Registration', + 'title' => 'User account creation requested for foo@bar.com', + 'origin' => '::1', + 'comment' => null, + 'description' => 'Handle user account for this cerebrate instance', + 'user_id' => UsersFixture::USER_ADMIN_ID, + 'data' => [ + 'email' => 'foo@bar.com', + 'password' => '$2y$10$dr5C0MWgBx1723yyws0HPudTqHz4k8wJ1PQ1ApVkNuH64LuZAr\/ve', + ], + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ], + [ + 'id' => self::INBOX_INCOMING_CONNECTION_REQUEST_ID, + 'uuid' => $faker->uuid(), + 'scope' => 'LocalTool', + 'action' => 'IncomingConnectionRequest', + 'title' => 'Request for MISP Inter-connection', + 'origin' => 'http://127.0.0.1', + 'comment' => null, + 'description' => 'Handle Phase I of inter-connection when another cerebrate instance performs the request.', + 'user_id' => UsersFixture::USER_ORG_ADMIN_ID, + 'data' => [ + 'connectorName' => 'MispConnector', + 'cerebrateURL' => 'http://127.0.0.1', + 'local_tool_id' => 1, + 'remote_tool_id' => 1, + ], + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ], + ]; + parent::init(); + } +} diff --git a/tests/Fixture/IndividualsFixture.php b/tests/Fixture/IndividualsFixture.php new file mode 100644 index 0000000..13aecdf --- /dev/null +++ b/tests/Fixture/IndividualsFixture.php @@ -0,0 +1,77 @@ +records = [ + [ + 'id' => self::INDIVIDUAL_ADMIN_ID, + 'uuid' => $faker->uuid(), + 'email' => $faker->email(), + 'first_name' => $faker->firstName, + 'last_name' => $faker->lastName, + 'position' => 'admin', + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ], + [ + 'id' => self::INDIVIDUAL_SYNC_ID, + 'uuid' => $faker->uuid(), + 'email' => $faker->email(), + 'first_name' => $faker->firstName, + 'last_name' => $faker->lastName, + 'position' => 'sync', + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ], + [ + 'id' => self::INDIVIDUAL_ORG_ADMIN_ID, + 'uuid' => $faker->uuid(), + 'email' => $faker->email(), + 'first_name' => $faker->firstName, + 'last_name' => $faker->lastName, + 'position' => 'org_admin', + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ], + [ + 'id' => self::INDIVIDUAL_REGULAR_USER_ID, + 'uuid' => $faker->uuid(), + 'email' => $faker->email(), + 'first_name' => $faker->firstName, + 'last_name' => $faker->lastName, + 'position' => 'user', + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ], + [ + 'id' => self::INDIVIDUAL_A_ID, + 'uuid' => $faker->uuid(), + 'email' => $faker->email(), + 'first_name' => $faker->firstName, + 'last_name' => $faker->lastName, + 'position' => 'user', + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ] + ]; + parent::init(); + } +} diff --git a/tests/Fixture/OrganisationsFixture.php b/tests/Fixture/OrganisationsFixture.php new file mode 100644 index 0000000..8531d0c --- /dev/null +++ b/tests/Fixture/OrganisationsFixture.php @@ -0,0 +1,48 @@ +records = [ + [ + 'id' => self::ORGANISATION_A_ID, + 'uuid' => $faker->uuid(), + 'name' => 'Organisation A', + 'url' => $faker->url, + 'nationality' => $faker->countryCode, + 'sector' => 'IT', + 'type' => '', + 'contacts' => '', + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ], + [ + 'id' => self::ORGANISATION_B_ID, + 'uuid' => $faker->uuid(), + 'name' => 'Organisation B', + 'url' => $faker->url, + 'nationality' => $faker->countryCode, + 'sector' => 'IT', + 'type' => '', + 'contacts' => '', + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ] + ]; + parent::init(); + } +} diff --git a/tests/Fixture/RolesFixture.php b/tests/Fixture/RolesFixture.php new file mode 100644 index 0000000..1230e8f --- /dev/null +++ b/tests/Fixture/RolesFixture.php @@ -0,0 +1,62 @@ +records = [ + [ + 'id' => self::ROLE_ADMIN_ID, + 'uuid' => $faker->uuid(), + 'name' => 'admin', + 'is_default' => false, + 'perm_admin' => true, + 'perm_sync' => false, + 'perm_org_admin' => false + ], + [ + 'id' => self::ROLE_SYNC_ID, + 'uuid' => $faker->uuid(), + 'name' => 'sync', + 'is_default' => false, + 'perm_admin' => false, + 'perm_sync' => true, + 'perm_org_admin' => false + ], + [ + 'id' => self::ROLE_ORG_ADMIN_ID, + 'uuid' => $faker->uuid(), + 'name' => 'org_admin', + 'is_default' => false, + 'perm_admin' => false, + 'perm_sync' => false, + 'perm_org_admin' => true + ], + [ + 'id' => self::ROLE_REGULAR_USER_ID, + 'uuid' => $faker->uuid(), + 'name' => 'user', + 'is_default' => true, + 'perm_admin' => false, + 'perm_sync' => false, + 'perm_org_admin' => false + ] + ]; + parent::init(); + } +} diff --git a/tests/Fixture/SharingGroupsFixture.php b/tests/Fixture/SharingGroupsFixture.php new file mode 100644 index 0000000..79665b6 --- /dev/null +++ b/tests/Fixture/SharingGroupsFixture.php @@ -0,0 +1,50 @@ +records = [ + [ + 'id' => self::SHARING_GROUP_A_ID, + 'uuid' => $faker->uuid(), + 'name' => 'Sharing Group A', + 'releasability' => 'Sharing Group A releasability', + 'description' => 'Sharing Group A description', + 'organisation_id' => OrganisationsFixture::ORGANISATION_A_ID, + 'user_id' => UsersFixture::USER_ADMIN_ID, + 'active' => true, + 'local' => true, + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ], + [ + 'id' => self::SHARING_GROUP_B_ID, + 'uuid' => $faker->uuid(), + 'name' => 'Sharing Group B', + 'releasability' => 'Sharing Group B releasability', + 'description' => 'Sharing Group B description', + 'organisation_id' => OrganisationsFixture::ORGANISATION_B_ID, + 'user_id' => UsersFixture::USER_ADMIN_ID, + 'active' => true, + 'local' => true, + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ], + ]; + parent::init(); + } +} diff --git a/tests/Fixture/TagsTaggedsFixture.php b/tests/Fixture/TagsTaggedsFixture.php new file mode 100644 index 0000000..e2bf70f --- /dev/null +++ b/tests/Fixture/TagsTaggedsFixture.php @@ -0,0 +1,40 @@ +records = [ + [ + 'tag_id' => TagsTagsFixture::TAG_ORG_A_ID, + 'fk_id' => OrganisationsFixture::ORGANISATION_A_ID, + 'fk_model' => 'Organisations', + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ], + [ + 'tag_id' => TagsTagsFixture::TAG_ORG_B_ID, + 'fk_id' => OrganisationsFixture::ORGANISATION_B_ID, + 'fk_model' => 'Organisations', + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ], + ]; + parent::init(); + } +} diff --git a/tests/Fixture/TagsTagsFixture.php b/tests/Fixture/TagsTagsFixture.php new file mode 100644 index 0000000..002bc9e --- /dev/null +++ b/tests/Fixture/TagsTagsFixture.php @@ -0,0 +1,87 @@ +records = [ + [ + 'id' => self::TAG_RED_ID, + 'name' => 'red', + 'namespace' => null, + 'predicate' => null, + 'value' => null, + 'colour' => 'FF0000', + 'counter' => 0, + 'text_colour' => 'red', + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ], + [ + 'id' => self::TAG_GREEN_ID, + 'name' => 'green', + 'namespace' => null, + 'predicate' => null, + 'value' => null, + 'colour' => '00FF00', + 'counter' => 0, + 'text_colour' => 'green', + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ], + [ + 'id' => self::TAG_BLUE_ID, + 'name' => 'blue', + 'namespace' => null, + 'predicate' => null, + 'value' => null, + 'colour' => '0000FF', + 'counter' => 0, + 'text_colour' => 'blue', + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ], + [ + 'id' => self::TAG_ORG_A_ID, + 'name' => 'org-a', + 'namespace' => null, + 'predicate' => null, + 'value' => null, + 'colour' => '000000', + 'counter' => 0, + 'text_colour' => 'black', + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ], + [ + 'id' => self::TAG_ORG_B_ID, + 'name' => 'org-b', + 'namespace' => null, + 'predicate' => null, + 'value' => null, + 'colour' => '000000', + 'counter' => 0, + 'text_colour' => 'black', + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ] + ]; + parent::init(); + } +} diff --git a/tests/Fixture/UsersFixture.php b/tests/Fixture/UsersFixture.php new file mode 100644 index 0000000..ba35e7a --- /dev/null +++ b/tests/Fixture/UsersFixture.php @@ -0,0 +1,92 @@ +records = [ + [ + 'id' => self::USER_ADMIN_ID, + 'uuid' => $faker->uuid(), + 'username' => self::USER_ADMIN_USERNAME, + 'password' => $hasher->hash(self::USER_ADMIN_PASSWORD), + 'role_id' => RolesFixture::ROLE_ADMIN_ID, + 'individual_id' => IndividualsFixture::INDIVIDUAL_ADMIN_ID, + 'disabled' => 0, + 'organisation_id' => OrganisationsFixture::ORGANISATION_A_ID, + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ], + [ + 'id' => self::USER_SYNC_ID, + 'uuid' => $faker->uuid(), + 'username' => self::USER_SYNC_USERNAME, + 'password' => $hasher->hash(self::USER_SYNC_PASSWORD), + 'role_id' => RolesFixture::ROLE_SYNC_ID, + 'individual_id' => IndividualsFixture::INDIVIDUAL_SYNC_ID, + 'disabled' => 0, + 'organisation_id' => OrganisationsFixture::ORGANISATION_A_ID, + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ], + [ + 'id' => self::USER_ORG_ADMIN_ID, + 'uuid' => $faker->uuid(), + 'username' => self::USER_ORG_ADMIN_USERNAME, + 'password' => $hasher->hash(self::USER_ORG_ADMIN_PASSWORD), + 'role_id' => RolesFixture::ROLE_ORG_ADMIN_ID, + 'individual_id' => IndividualsFixture::INDIVIDUAL_ORG_ADMIN_ID, + 'disabled' => 0, + 'organisation_id' => OrganisationsFixture::ORGANISATION_A_ID, + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ], + [ + 'id' => self::USER_REGULAR_USER_ID, + 'uuid' => $faker->uuid(), + 'username' => self::USER_REGULAR_USER_USERNAME, + 'password' => $hasher->hash(self::USER_REGULAR_USER_PASSWORD), + 'role_id' => RolesFixture::ROLE_REGULAR_USER_ID, + 'individual_id' => IndividualsFixture::INDIVIDUAL_REGULAR_USER_ID, + 'disabled' => 0, + 'organisation_id' => OrganisationsFixture::ORGANISATION_A_ID, + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ] + ]; + parent::init(); + } +} diff --git a/tests/Helper/ApiTestTrait.php b/tests/Helper/ApiTestTrait.php new file mode 100644 index 0000000..a55268c --- /dev/null +++ b/tests/Helper/ApiTestTrait.php @@ -0,0 +1,294 @@ +initializeOpenApiValidator(); + } + + public function setAuthToken(string $authToken): void + { + $this->_authToken = $authToken; + + // somehow this is not set automatically in test environment + $_SERVER['HTTP_AUTHORIZATION'] = $authToken; + + $this->configRequest([ + 'headers' => [ + 'Accept' => 'application/json', + 'Authorization' => $this->_authToken, + 'Content-Type' => 'application/json' + ] + ]); + } + + /** + * Skip OpenAPI validations. + * + * @return void + */ + public function skipOpenApiValidations(): void + { + $this->_skipOpenApiValidations = true; + } + + public function assertResponseContainsArray(array $expected): void + { + $responseArray = json_decode((string)$this->_response->getBody(), true); + throw new NotImplementedException('TODO: see codeception seeResponseContainsJson()'); + } + + /** + * Load OpenAPI specification validator + * + * @return void + */ + public function initializeOpenApiValidator(): void + { + if (!$this->_skipOpenApiValidations) { + $this->_validator = Configure::read('App.OpenAPIValidator'); + if ($this->_validator === null) { + throw new \Exception('OpenAPI validator is not configured'); + } + } + } + + /** + * Validates the API request against the OpenAPI spec + * + * @return void + */ + public function assertRequestMatchesOpenApiSpec(): void + { + $this->_validator->getRequestValidator()->validate($this->_psrRequest); + } + + /** + * Validates the API response against the OpenAPI spec + * + * @param string $path The path to the API endpoint + * @param string $method The HTTP method used to call the endpoint + * @return void + */ + public function assertResponseMatchesOpenApiSpec(string $endpoint, string $method = 'get'): void + { + $address = new OperationAddress($endpoint, $method); + $this->_validator->getResponseValidator()->validate($address, $this->_response); + } + + /** + * Validates a record exists in the database + * + * @param string $table The table name + * @param array $conditions The conditions to check + * @return void + * @throws \Exception + * @throws \Cake\Datasource\Exception\RecordNotFoundException + * + * @see https://book.cakephp.org/4/en/orm-query-builder.html + */ + public function assertDbRecordExists(string $table, array $conditions): void + { + $record = $this->getTableLocator()->get($table)->find()->where($conditions)->first(); + if (!$record) { + throw new \PHPUnit\Framework\AssertionFailedError("Record not found in table '$table' with conditions: " . json_encode($conditions)); + } + $this->assertNotEmpty($record); + } + + /** + * Validates a record do not exists in the database + * + * @param string $table The table name + * @param array $conditions The conditions to check + * @return void + * @throws \Exception + * @throws \Cake\Datasource\Exception\RecordNotFoundException + * + * @see https://book.cakephp.org/4/en/orm-query-builder.html + */ + public function assertDbRecordNotExists(string $table, array $conditions): void + { + $record = $this->getTableLocator()->get($table)->find()->where($conditions)->first(); + if ($record) { + throw new \PHPUnit\Framework\AssertionFailedError("Record found in table '$table' with conditions: " . json_encode($conditions)); + } + $this->assertEmpty($record); + } + + /** + * Parses the response body and returns the decoded JSON + * + * @return array + * @throws \Exception + */ + public function getJsonResponseAsArray(): array + { + if ($this->_response->getHeaders()['Content-Type'][0] !== 'application/json') { + throw new \Exception('The response is not a JSON response'); + } + + return json_decode((string)$this->_response->getBody(), true); + } + + /** + * Gets a database records as an array + * + * @param string $table The table name + * @param array $conditions The conditions to check + * @return array + * @throws \Cake\Datasource\Exception\RecordNotFoundException + */ + public function getRecordFromDb(string $table, array $conditions): array + { + return $this->getTableLocator()->get($table)->find()->where($conditions)->first()->toArray(); + } + + /** + * This method intercepts IntegrationTestTrait::_buildRequest() + * in the quest to get a PSR-7 request object and saves it for + * later inspection, also validates it against the OpenAPI spec. + * @see \Cake\TestSuite\IntegrationTestTrait::_buildRequest() + * + * @param string $url The URL + * @param string $method The HTTP method + * @param array|string $data The request data. + * @return array The request context + */ + protected function _buildRequest(string $url, $method, $data = []): array + { + $spec = $this->_buildRequestOriginal($url, $method, $data); + + $this->_psrRequest = $this->_createPsr7RequestFromSpec($spec); + + // Validate request against OpenAPI spec + if (!$this->_skipOpenApiValidations) { + try { + $this->assertRequestMatchesOpenApiSpec(); + } catch (\Exception $exception) { + $this->fail($exception->getMessage()); + } + } else { + $this->addWarning( + sprintf( + 'OpenAPI spec validations skipped for request [%s]%s.', + $this->_psrRequest->getMethod(), + $this->_psrRequest->getPath() + ) + ); + } + + return $spec; + } + + /** + * This method intercepts IntegrationTestTrait::_buildRequest() + * and validates the response against the OpenAPI spec. + * + * @see \Cake\TestSuite\IntegrationTestTrait::_sendRequest() + * + * @param array|string $url The URL + * @param string $method The HTTP method + * @param array|string $data The request data. + * @return void + * @throws \PHPUnit\Exception|\Throwable + */ + protected function _sendRequest($url, $method, $data = []): void + { + // Adding Content-Type: application/json $this->configRequest() prevents this from happening somehow + if (in_array($method, ['POST', 'PATCH', 'PUT']) && $this->_request['headers']['Content-Type'] === 'application/json') { + $data = json_encode($data); + } + + $this->_sendRequestOriginal($url, $method, $data); + + // Validate response against OpenAPI spec + if (!$this->_skipOpenApiValidations) { + $this->assertResponseMatchesOpenApiSpec( + $this->_psrRequest->getPath(), + strtolower($this->_psrRequest->getMethod()) + ); + } else { + $this->addWarning( + sprintf( + 'OpenAPI spec validations skipped for response of [%s]%s.', + $this->_psrRequest->getMethod(), + $this->_psrRequest->getPath() + ) + ); + } + } + + /** + * Create a PSR-7 request from the request spec. + * @see \Cake\TestSuite\MiddlewareDispatcher::_createRequest() + * + * @param array $spec The request spec. + * @return \Cake\Http\ServerRequest + */ + private function _createPsr7RequestFromSpec(array $spec): ServerRequest + { + if (isset($spec['input'])) { + $spec['post'] = []; + $spec['environment']['CAKEPHP_INPUT'] = $spec['input']; + } + $environment = array_merge( + array_merge($_SERVER, ['REQUEST_URI' => $spec['url']]), + $spec['environment'] + ); + if (strpos($environment['PHP_SELF'], 'phpunit') !== false) { + $environment['PHP_SELF'] = '/'; + } + return ServerRequestFactory::fromGlobals( + $environment, + $spec['query'], + $spec['post'], + $spec['cookies'], + $spec['files'] + ); + } +} diff --git a/tests/Helper/WireMockTestTrait.php b/tests/Helper/WireMockTestTrait.php new file mode 100644 index 0000000..9b42f7e --- /dev/null +++ b/tests/Helper/WireMockTestTrait.php @@ -0,0 +1,49 @@ + */ + private $config = [ + 'hostname' => 'localhost', + 'port' => 8080 + ]; + + public function initializeWireMock(): void + { + $this->wiremock = WireMock::create( + $_ENV['WIREMOCK_HOST'] ?? $this->config['hostname'], + $_ENV['WIREMOCK_PORT'] ?? $this->config['port'] + ); + + if (!$this->wiremock->isAlive()) { + throw new Exception('Failed to connect to WireMock server.'); + } + + $this->clearWireMockStubs(); + } + + public function clearWireMockStubs(): void + { + $this->wiremock->resetToDefault(); + } + + public function getWireMock(): WireMock + { + return $this->wiremock; + } + + public function getWireMockBaseUrl(): string + { + return sprintf('http://%s:%s', $this->config['hostname'], $this->config['port']); + } +} diff --git a/tests/Helper/wiremock/start.sh b/tests/Helper/wiremock/start.sh new file mode 100644 index 0000000..60b8197 --- /dev/null +++ b/tests/Helper/wiremock/start.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +# Adapted from @rowanhill wiremock start.sh script +# https://github.com/rowanhill/wiremock-php/blob/master/wiremock/start.sh + +cd ./tmp/ + +instance=1 +port=8080 +if [ $# -gt 0 ]; then + instance=$1 + port=$2 +fi +pidFile=wiremock.$instance.pid +logFile=wiremock.$instance.log + +# Ensure WireMock isn't already running +if [ -e $pidFile ]; then + echo WireMock is already started: see process `cat $pidFile` 1>&2 + exit 0 +fi + +# Download the wiremock jar if we need it +if ! [ -e wiremock-standalone.jar ]; then + echo WireMock standalone JAR missing. Downloading. + curl https://repo1.maven.org/maven2/com/github/tomakehurst/wiremock-jre8-standalone/2.32.0/wiremock-jre8-standalone-2.32.0.jar -o wiremock-standalone.jar + status=$? + if [ ${status} -ne 0 ]; then + echo curl could not download WireMock JAR 1>&2 + exit ${status} + fi +fi + +# Start WireMock in standalone mode (in a background process) and save its output to a log +java -jar wiremock-standalone.jar --port $port --root-dir $instance --disable-banner &> $logFile 2>&1 & +pgrep -f wiremock-standalone.jar > $pidFile + +echo WireMock $instance started on port $port \ No newline at end of file diff --git a/tests/Helper/wiremock/stop.sh b/tests/Helper/wiremock/stop.sh new file mode 100644 index 0000000..f9e3f9e --- /dev/null +++ b/tests/Helper/wiremock/stop.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# Adapted from @rowanhill wiremock stop.sh script +# https://github.com/rowanhill/wiremock-php/blob/master/wiremock/stop.sh + +cd ./tmp/ + +instance=1 +if [ $# -gt 0 ]; then + instance=$1 +fi +pidFile=wiremock.$instance.pid + + +if [ -e $pidFile ]; then + kill -9 `cat $pidFile` + rm $pidFile +else + echo WireMock is not started 2>&1 + exit 1 +fi + +echo WireMock $instance stopped \ No newline at end of file diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..434f84a --- /dev/null +++ b/tests/README.md @@ -0,0 +1,123 @@ +# Testing +1. Add a `cerebrate_test` database to the database: +```mysql +CREATE DATABASE cerebrate_test; +GRANT ALL PRIVILEGES ON cerebrate_test.* to cerebrate@localhost; +FLUSH PRIVILEGES; +QUIT; +``` + +2. Add a the test database to your `config/app_local.php` config file and set `debug` mode to `true`. +```php +'debug' => true, +'Datasources' => [ + 'default' => [ + ... + ], + /* + * The test connection is used during the test suite. + */ + 'test' => [ + 'host' => 'localhost', + 'username' => 'cerebrate', + 'password' => 'cerebrate', + 'database' => 'cerebrate_test', + ], +], +``` + +## Runing the tests +``` +$ composer install +$ composer test +> sh ./tests/Helper/wiremock/start.sh +WireMock 1 started on port 8080 +> phpunit +[ * ] Running DB migrations, it may take some time ... + +The WireMock server is started ..... +port: 8080 +enable-browser-proxying: false +disable-banner: true +no-request-journal: false +verbose: false + +PHPUnit 8.5.22 by Sebastian Bergmann and contributors. + + +..... 5 / 5 (100%) + +Time: 11.61 seconds, Memory: 26.00 MB + +OK (5 tests, 15 assertions) +``` + +Running a specific suite: +``` +$ vendor/bin/phpunit --testsuite=api --testdox +``` +Available suites: +* `app`: runs all test suites +* `api`: runs only api tests +* `controller`: runs only controller tests +* _to be continued ..._ + +By default the database is re-generated before running the test suite, to skip this step and speed up the test run set the following env variable in `phpunit.xml`: +```xml + + ... + + +``` +## Extras +### WireMock +Some integration tests perform calls to external APIs, we use WireMock to mock the response of these API calls. + +To download and run WireMock run the following script in a separate terminal: + ``` + sh ./tests/Helper/wiremock/start.sh + ``` + +You can also run WireMock with docker, check the official docs: http://wiremock.org/docs/docker/ + +> NOTE: When running the tests with `composer test` WireMock is automatically started and stoped after the tests finish. + +The default `hostname` and `port` for WireMock are set in `phpunit.xml` as environment variables: +```xml + + ... + + + +``` +### Coverage +HTML: +``` +$ vendor/bin/phpunit --coverage-html tmp/coverage +``` + +XML: +``` +$ vendor/bin/phpunit --verbose --coverage-clover=coverage.xml +``` + +### OpenAPI validation +API tests can assert the API response matches the OpenAPI specification, after the request add this line: + +```php +$this->assertResponseMatchesOpenApiSpec(self::ENDPOINT); +``` + +The default OpenAPI spec path is set in `phpunit.xml` as a environment variablea: +```xml + + ... + + +``` + +### Debugging tests +``` +$ export XDEBUG_CONFIG="idekey=IDEKEY" +$ phpunit +``` diff --git a/tests/TestCase/Api/AuthKeys/AddAuthKeyApiTest.php b/tests/TestCase/Api/AuthKeys/AddAuthKeyApiTest.php new file mode 100644 index 0000000..ca305e8 --- /dev/null +++ b/tests/TestCase/Api/AuthKeys/AddAuthKeyApiTest.php @@ -0,0 +1,72 @@ +setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + + $faker = \Faker\Factory::create(); + $uuid = $faker->uuid; + + $this->post( + self::ENDPOINT, + [ + 'uuid' => $uuid, + 'authkey' => $faker->sha1, + 'expiration' => 0, + 'user_id' => UsersFixture::USER_ADMIN_ID, + 'comment' => $faker->text + ] + ); + + $this->assertResponseOk(); + $this->assertResponseContains(sprintf('"uuid": "%s"', $uuid)); + $this->assertDbRecordExists('AuthKeys', ['uuid' => $uuid]); + } + + public function testAddAdminAuthKeyNotAllowedAsRegularUser(): void + { + $this->setAuthToken(AuthKeysFixture::REGULAR_USER_API_KEY); + + $faker = \Faker\Factory::create(); + $uuid = $faker->uuid; + + + $this->post( + self::ENDPOINT, + [ + 'uuid' => $uuid, + 'authkey' => $faker->sha1, + 'expiration' => 0, + 'user_id' => UsersFixture::USER_ADMIN_ID, + 'comment' => $faker->text + ] + ); + + $this->assertResponseCode(404); + $this->addWarning('Should return 405 Method Not Allowed instead of 404 Not Found'); + $this->assertDbRecordNotExists('AuthKeys', ['uuid' => $uuid]); + } +} diff --git a/tests/TestCase/Api/AuthKeys/DeleteAuthKeyApiTest.php b/tests/TestCase/Api/AuthKeys/DeleteAuthKeyApiTest.php new file mode 100644 index 0000000..a621f37 --- /dev/null +++ b/tests/TestCase/Api/AuthKeys/DeleteAuthKeyApiTest.php @@ -0,0 +1,45 @@ +setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + $url = sprintf('%s/%d', self::ENDPOINT, AuthKeysFixture::ADMIN_API_ID); + $this->delete($url); + + $this->assertResponseOk(); + $this->assertDbRecordNotExists('AuthKeys', ['id' => AuthKeysFixture::ADMIN_API_ID]); + } + + public function testDeleteOrgAdminAuthKeyNotAllowedAsRegularUser(): void + { + $this->setAuthToken(AuthKeysFixture::REGULAR_USER_API_KEY); + $url = sprintf('%s/%d', self::ENDPOINT, AuthKeysFixture::ORG_ADMIN_API_ID); + + $this->delete($url); + + $this->assertResponseCode(405); + $this->assertDbRecordExists('AuthKeys', ['id' => AuthKeysFixture::ORG_ADMIN_API_ID]); + } +} diff --git a/tests/TestCase/Api/AuthKeys/IndexAuthKeysApiTest.php b/tests/TestCase/Api/AuthKeys/IndexAuthKeysApiTest.php new file mode 100644 index 0000000..0712480 --- /dev/null +++ b/tests/TestCase/Api/AuthKeys/IndexAuthKeysApiTest.php @@ -0,0 +1,42 @@ +setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + $this->get(self::ENDPOINT); + + $this->assertResponseOk(); + $this->assertResponseContains(sprintf('"id": %d', AuthKeysFixture::ADMIN_API_ID)); + } + + public function testIndexDoesNotShowAdminAuthKeysAsRegularUser(): void + { + $this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + $this->get(self::ENDPOINT); + + $this->assertResponseOk(); + $this->assertResponseNotContains(sprintf('"id": %d', AuthKeysFixture::REGULAR_USER_API_KEY)); + } +} diff --git a/tests/TestCase/Api/Broods/AddBroodApiTest.php b/tests/TestCase/Api/Broods/AddBroodApiTest.php new file mode 100644 index 0000000..f064f61 --- /dev/null +++ b/tests/TestCase/Api/Broods/AddBroodApiTest.php @@ -0,0 +1,79 @@ +setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + + $faker = \Faker\Factory::create(); + $uuid = $faker->uuid; + + $this->post( + self::ENDPOINT, + [ + 'uuid' => $uuid, + 'name' => 'Brood A', + 'url' => $faker->url, + 'description' => $faker->text, + 'organisation_id' => OrganisationsFixture::ORGANISATION_A_ID, + 'trusted' => true, + 'pull' => true, + 'skip_proxy' => true, + 'authkey' => $faker->sha1, + ] + ); + + $this->assertResponseOk(); + $this->assertResponseContains(sprintf('"uuid": "%s"', $uuid)); + $this->assertDbRecordExists('Broods', ['uuid' => $uuid]); + } + + public function testAddBroodNotAllowedAsRegularUser(): void + { + $this->setAuthToken(AuthKeysFixture::REGULAR_USER_API_KEY); + + $faker = \Faker\Factory::create(); + $uuid = $faker->uuid; + + $this->post( + self::ENDPOINT, + [ + 'uuid' => $uuid, + 'name' => 'Brood A', + 'url' => $faker->url, + 'description' => $faker->text, + 'organisation_id' => OrganisationsFixture::ORGANISATION_A_ID, + 'trusted' => true, + 'pull' => true, + 'skip_proxy' => true, + 'authkey' => $faker->sha1, + ] + ); + + $this->assertResponseCode(405); + $this->assertDbRecordNotExists('Broods', ['uuid' => $uuid]); + } +} diff --git a/tests/TestCase/Api/Broods/DeleteBroodApiTest.php b/tests/TestCase/Api/Broods/DeleteBroodApiTest.php new file mode 100644 index 0000000..420bf01 --- /dev/null +++ b/tests/TestCase/Api/Broods/DeleteBroodApiTest.php @@ -0,0 +1,46 @@ +setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + $url = sprintf('%s/%d', self::ENDPOINT, BroodsFixture::BROOD_A_ID); + $this->delete($url); + + $this->assertResponseOk(); + $this->assertDbRecordNotExists('Broods', ['id' => BroodsFixture::BROOD_A_ID]); + } + + public function testDeleteBroodNotAllowedAsRegularUser(): void + { + $this->setAuthToken(AuthKeysFixture::REGULAR_USER_API_KEY); + $url = sprintf('%s/%d', self::ENDPOINT, BroodsFixture::BROOD_A_ID); + $this->delete($url); + + $this->assertResponseCode(405); + $this->assertDbRecordExists('Broods', ['id' => BroodsFixture::BROOD_A_ID]); + } +} diff --git a/tests/TestCase/Api/Broods/EditBroodApiTest.php b/tests/TestCase/Api/Broods/EditBroodApiTest.php new file mode 100644 index 0000000..ad5d70b --- /dev/null +++ b/tests/TestCase/Api/Broods/EditBroodApiTest.php @@ -0,0 +1,71 @@ +setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + + $url = sprintf('%s/%d', self::ENDPOINT, BroodsFixture::BROOD_A_ID); + $this->put( + $url, + [ + 'name' => 'Test Brood 4321', + ] + ); + + $this->assertResponseOk(); + $this->assertDbRecordExists( + 'Broods', + [ + 'id' => BroodsFixture::BROOD_A_ID, + 'name' => 'Test Brood 4321', + ] + ); + } + + public function testEditBroodNotAllowedAsRegularUser(): void + { + $this->setAuthToken(AuthKeysFixture::REGULAR_USER_API_KEY); + + $url = sprintf('%s/%d', self::ENDPOINT, BroodsFixture::BROOD_B_ID); + $this->put( + $url, + [ + 'name' => 'Test Brood 1234' + ] + ); + + $this->assertResponseCode(405); + $this->assertDbRecordNotExists( + 'Broods', + [ + 'id' => BroodsFixture::BROOD_B_ID, + 'name' => 'Test Brood 1234' + ] + ); + } +} diff --git a/tests/TestCase/Api/Broods/IndexBroodsApiTest.php b/tests/TestCase/Api/Broods/IndexBroodsApiTest.php new file mode 100644 index 0000000..d70bc53 --- /dev/null +++ b/tests/TestCase/Api/Broods/IndexBroodsApiTest.php @@ -0,0 +1,35 @@ +setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + $this->get(self::ENDPOINT); + + $this->assertResponseOk(); + $this->assertResponseContains(sprintf('"id": %d', BroodsFixture::BROOD_A_ID)); + } +} diff --git a/tests/TestCase/Api/Broods/TestBroodConnectionApiTest.php b/tests/TestCase/Api/Broods/TestBroodConnectionApiTest.php new file mode 100644 index 0000000..ee1117f --- /dev/null +++ b/tests/TestCase/Api/Broods/TestBroodConnectionApiTest.php @@ -0,0 +1,68 @@ +setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + $this->initializeWireMock(); + $this->mockCerebrateStatusResponse(); + + $url = sprintf('%s/%d', self::ENDPOINT, BroodsFixture::BROOD_WIREMOCK_ID); + $this->get($url); + + $this->getWireMock()->verify( + WireMock::getRequestedFor(WireMock::urlEqualTo('/instance/status.json')) + ->withHeader('Content-Type', WireMock::equalTo('application/json')) + ->withHeader('Authorization', WireMock::equalTo(BroodsFixture::BROOD_WIREMOCK_API_KEY)) + ); + + $this->assertResponseOk(); + $this->assertResponseContains('"user": "wiremock"'); + } + + private function mockCerebrateStatusResponse(): \WireMock\Stubbing\StubMapping + { + return $this->getWireMock()->stubFor( + WireMock::get(WireMock::urlEqualTo('/instance/status.json')) + ->willReturn(WireMock::aResponse() + ->withHeader('Content-Type', 'application/json') + ->withBody((string)json_encode([ + "version" => "0.1", + "application" => "Cerebrate", + "user" => [ + "id" => 1, + "username" => "wiremock", + "role" => [ + "id" => 1 + ] + ] + ]))) + ); + } +} diff --git a/tests/TestCase/Api/Broods/ViewBroodApiTest.php b/tests/TestCase/Api/Broods/ViewBroodApiTest.php new file mode 100644 index 0000000..bd9e5a7 --- /dev/null +++ b/tests/TestCase/Api/Broods/ViewBroodApiTest.php @@ -0,0 +1,36 @@ +setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + $url = sprintf('%s/%d', self::ENDPOINT, BroodsFixture::BROOD_A_ID); + $this->get($url); + + $this->assertResponseOk(); + $this->assertResponseContains(sprintf('"id": %d', BroodsFixture::BROOD_A_ID)); + } +} diff --git a/tests/TestCase/Api/EncryptionKeys/AddEncryptionKeyApiTest.php b/tests/TestCase/Api/EncryptionKeys/AddEncryptionKeyApiTest.php new file mode 100644 index 0000000..00cc377 --- /dev/null +++ b/tests/TestCase/Api/EncryptionKeys/AddEncryptionKeyApiTest.php @@ -0,0 +1,76 @@ +setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + + $faker = \Faker\Factory::create(); + $uuid = $faker->uuid; + + $this->post( + self::ENDPOINT, + [ + 'uuid' => $uuid, + 'type' => EncryptionKeysFixture::TYPE_PGP, + 'encryption_key' => EncryptionKeysFixture::getPublicKey(EncryptionKeysFixture::KEY_TYPE_EDCH), + 'revoked' => false, + 'expires' => null, + 'owner_id' => UsersFixture::USER_ADMIN_ID, + 'owner_model' => 'User' + ] + ); + + $this->assertResponseOk(); + $this->assertResponseContains(sprintf('"uuid": "%s"', $uuid)); + $this->assertDbRecordExists('EncryptionKeys', ['uuid' => $uuid]); + } + + public function testAddAdminUserEncryptionKeyNotAllowedAsRegularUser(): void + { + $this->setAuthToken(AuthKeysFixture::REGULAR_USER_API_KEY); + + $faker = \Faker\Factory::create(); + $uuid = $faker->uuid; + + $this->post( + self::ENDPOINT, + [ + 'uuid' => $uuid, + 'type' => EncryptionKeysFixture::TYPE_PGP, + 'encryption_key' => EncryptionKeysFixture::getPublicKey(EncryptionKeysFixture::KEY_TYPE_EDCH), + 'revoked' => false, + 'expires' => null, + 'owner_id' => UsersFixture::USER_ADMIN_ID, + 'owner_model' => 'User' + ] + ); + + $this->assertResponseCode(405); + $this->assertDbRecordNotExists('EncryptionKeys', ['uuid' => $uuid]); + } +} diff --git a/tests/TestCase/Api/EncryptionKeys/DeleteEncryptionKeyApiTest.php b/tests/TestCase/Api/EncryptionKeys/DeleteEncryptionKeyApiTest.php new file mode 100644 index 0000000..6ae8143 --- /dev/null +++ b/tests/TestCase/Api/EncryptionKeys/DeleteEncryptionKeyApiTest.php @@ -0,0 +1,46 @@ +setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + $url = sprintf('%s/%d', self::ENDPOINT, EncryptionKeysFixture::ENCRYPTION_KEY_ORG_A_ID); + $this->delete($url); + + $this->assertResponseOk(); + $this->assertDbRecordNotExists('EncryptionKeys', ['id' => EncryptionKeysFixture::ENCRYPTION_KEY_ORG_A_ID]); + } + + public function testDeleteEncryptionKeyNotAllowedAsRegularUser(): void + { + $this->setAuthToken(AuthKeysFixture::REGULAR_USER_API_KEY); + $url = sprintf('%s/%d', self::ENDPOINT, EncryptionKeysFixture::ENCRYPTION_KEY_ORG_B_ID); + $this->delete($url); + + $this->assertResponseCode(405); + $this->assertDbRecordExists('EncryptionKeys', ['id' => EncryptionKeysFixture::ENCRYPTION_KEY_ORG_B_ID]); + } +} diff --git a/tests/TestCase/Api/EncryptionKeys/EditEncryptionKeyApiTest.php b/tests/TestCase/Api/EncryptionKeys/EditEncryptionKeyApiTest.php new file mode 100644 index 0000000..2636fc1 --- /dev/null +++ b/tests/TestCase/Api/EncryptionKeys/EditEncryptionKeyApiTest.php @@ -0,0 +1,70 @@ +setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + + $url = sprintf('%s/%d', self::ENDPOINT, EncryptionKeysFixture::ENCRYPTION_KEY_ORG_A_ID); + $this->put( + $url, + [ + 'revoked' => true, + ] + ); + + $this->assertResponseOk(); + $this->assertDbRecordExists( + 'EncryptionKeys', + [ + 'id' => EncryptionKeysFixture::ENCRYPTION_KEY_ORG_A_ID, + 'revoked' => true, + ] + ); + } + + public function testRevokeAdminEncryptionKeyNotAllowedAsRegularUser(): void + { + $this->setAuthToken(AuthKeysFixture::REGULAR_USER_API_KEY); + + $url = sprintf('%s/%d', self::ENDPOINT, EncryptionKeysFixture::ENCRYPTION_KEY_ORG_B_ID); + $this->put( + $url, + [ + 'revoked' => true + ] + ); + + $this->assertResponseCode(405); + $this->assertDbRecordNotExists( + 'EncryptionKeys', + [ + 'id' => EncryptionKeysFixture::ENCRYPTION_KEY_ORG_B_ID, + 'revoked' => true + ] + ); + } +} diff --git a/tests/TestCase/Api/EncryptionKeys/IndexEncryptionKeysApiTest.php b/tests/TestCase/Api/EncryptionKeys/IndexEncryptionKeysApiTest.php new file mode 100644 index 0000000..844336d --- /dev/null +++ b/tests/TestCase/Api/EncryptionKeys/IndexEncryptionKeysApiTest.php @@ -0,0 +1,36 @@ +setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + $this->get(self::ENDPOINT); + + $this->assertResponseOk(); + $this->assertResponseContains(sprintf('"id": %d', EncryptionKeysFixture::ENCRYPTION_KEY_ORG_A_ID)); + } +} diff --git a/tests/TestCase/Api/EncryptionKeys/ViewEncryptionKeyApiTest.php b/tests/TestCase/Api/EncryptionKeys/ViewEncryptionKeyApiTest.php new file mode 100644 index 0000000..de324fb --- /dev/null +++ b/tests/TestCase/Api/EncryptionKeys/ViewEncryptionKeyApiTest.php @@ -0,0 +1,36 @@ +setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + $url = sprintf('%s/%d', self::ENDPOINT, EncryptionKeysFixture::ENCRYPTION_KEY_ORG_A_ID); + $this->get($url); + + $this->assertResponseOk(); + $this->assertResponseContains(sprintf('"id": %d', EncryptionKeysFixture::ENCRYPTION_KEY_ORG_A_ID)); + } +} diff --git a/tests/TestCase/Api/Inbox/CreateInboxEntryApiTest.php b/tests/TestCase/Api/Inbox/CreateInboxEntryApiTest.php new file mode 100644 index 0000000..8434d62 --- /dev/null +++ b/tests/TestCase/Api/Inbox/CreateInboxEntryApiTest.php @@ -0,0 +1,70 @@ +setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + + // to avoid $this->request->clientIp() to return null + $_SERVER['REMOTE_ADDR'] = '::1'; + + $url = sprintf("%s/%s/%s", self::ENDPOINT, 'User', 'Registration'); + $this->post( + $url, + [ + 'email' => 'john@example.com', + 'password' => 'Password12345!' + ] + ); + + $this->assertResponseOk(); + $this->assertResponseContains('"email": "john@example.com"'); + $this->assertDbRecordExists( + 'Inbox', + [ + 'id' => 3, // hacky, but `data` is json string cannot verify the value because of the hashed password + 'scope' => 'User', + 'action' => 'Registration', + ] + ); + } + + public function testAddUserRegistrationInboxNotAllowedAsRegularUser(): void + { + $this->setAuthToken(AuthKeysFixture::REGULAR_USER_API_KEY); + + $url = sprintf("%s/%s/%s", self::ENDPOINT, 'User', 'Registration'); + $this->post( + $url, + [ + 'email' => 'john@example.com', + 'password' => 'Password12345!' + ] + ); + + $this->assertResponseCode(405); + $this->assertDbRecordNotExists('Inbox', ['id' => 3]); + } +} diff --git a/tests/TestCase/Api/Inbox/IndexInboxApiTest.php b/tests/TestCase/Api/Inbox/IndexInboxApiTest.php new file mode 100644 index 0000000..ae9c039 --- /dev/null +++ b/tests/TestCase/Api/Inbox/IndexInboxApiTest.php @@ -0,0 +1,36 @@ +setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + $this->get(self::ENDPOINT); + + $this->assertResponseOk(); + $this->assertResponseContains(sprintf('"id": %d', InboxFixture::INBOX_USER_REGISTRATION_ID)); + $this->assertResponseContains(sprintf('"id": %d', InboxFixture::INBOX_INCOMING_CONNECTION_REQUEST_ID)); + } +} diff --git a/tests/TestCase/Api/Individuals/AddIndividualApiTest.php b/tests/TestCase/Api/Individuals/AddIndividualApiTest.php new file mode 100644 index 0000000..fcff319 --- /dev/null +++ b/tests/TestCase/Api/Individuals/AddIndividualApiTest.php @@ -0,0 +1,59 @@ +setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + $this->post( + self::ENDPOINT, + [ + 'email' => 'john@example.com', + 'first_name' => 'John', + 'last_name' => 'Doe', + 'position' => 'Security Analyst' + ] + ); + + $this->assertResponseOk(); + $this->assertResponseContains('"email": "john@example.com"'); + $this->assertDbRecordExists('Individuals', ['email' => 'john@example.com']); + } + + // public function testAddUserNotAllowedAsRegularUser(): void + // { + // $this->setAuthToken(AuthKeysFixture::REGULAR_USER_API_KEY); + // $this->post( + // self::ENDPOINT, + // [ + // 'email' => 'john@example.com', + // 'first_name' => 'John', + // 'last_name' => 'Doe', + // 'position' => 'Security Analyst' + // ] + // ); + + // $this->assertResponseCode(405); + // $this->assertDbRecordNotExists('Individuals', ['email' => 'john@example.com']); + // } +} diff --git a/tests/TestCase/Api/Individuals/DeleteIndividualApiTest.php b/tests/TestCase/Api/Individuals/DeleteIndividualApiTest.php new file mode 100644 index 0000000..e5657aa --- /dev/null +++ b/tests/TestCase/Api/Individuals/DeleteIndividualApiTest.php @@ -0,0 +1,45 @@ +setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + $url = sprintf('%s/%d', self::ENDPOINT, IndividualsFixture::INDIVIDUAL_A_ID); + $this->delete($url); + + $this->assertResponseOk(); + $this->assertDbRecordNotExists('Individuals', ['id' => IndividualsFixture::INDIVIDUAL_A_ID]); + } + + public function testDeleteIndividualNotAllowedAsRegularUser(): void + { + $this->setAuthToken(AuthKeysFixture::REGULAR_USER_API_KEY); + $url = sprintf('%s/%d', self::ENDPOINT, IndividualsFixture::INDIVIDUAL_ADMIN_ID); + $this->delete($url); + + $this->assertResponseCode(405); + $this->assertDbRecordExists('Individuals', ['id' => IndividualsFixture::INDIVIDUAL_ADMIN_ID]); + } +} diff --git a/tests/TestCase/Api/Individuals/EditIndividualApiTest.php b/tests/TestCase/Api/Individuals/EditIndividualApiTest.php new file mode 100644 index 0000000..c888bba --- /dev/null +++ b/tests/TestCase/Api/Individuals/EditIndividualApiTest.php @@ -0,0 +1,61 @@ +setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + $url = sprintf('%s/%d', self::ENDPOINT, IndividualsFixture::INDIVIDUAL_REGULAR_USER_ID); + $this->put( + $url, + [ + 'email' => 'foo@bar.com', + ] + ); + + $this->assertResponseOk(); + $this->assertDbRecordExists('Individuals', [ + 'id' => IndividualsFixture::INDIVIDUAL_REGULAR_USER_ID, + 'email' => 'foo@bar.com' + ]); + } + + public function testEditAnyIndividualNotAllowedAsRegularUser(): void + { + $this->setAuthToken(AuthKeysFixture::REGULAR_USER_API_KEY); + $url = sprintf('%s/%d', self::ENDPOINT, IndividualsFixture::INDIVIDUAL_ADMIN_ID); + $this->put( + $url, + [ + 'email' => 'foo@bar.com', + ] + ); + + $this->assertResponseCode(405); + $this->assertDbRecordNotExists('Individuals', [ + 'id' => IndividualsFixture::INDIVIDUAL_ADMIN_ID, + 'email' => 'foo@bar.com' + ]); + } +} diff --git a/tests/TestCase/Api/Individuals/IndexIndividualsApiTest.php b/tests/TestCase/Api/Individuals/IndexIndividualsApiTest.php new file mode 100644 index 0000000..e5c92ce --- /dev/null +++ b/tests/TestCase/Api/Individuals/IndexIndividualsApiTest.php @@ -0,0 +1,34 @@ +setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + $this->get(self::ENDPOINT); + + $this->assertResponseOk(); + $this->assertResponseContains(sprintf('"id": %d', IndividualsFixture::INDIVIDUAL_ADMIN_ID)); + } +} diff --git a/tests/TestCase/Api/Individuals/ViewIndividualApiTest.php b/tests/TestCase/Api/Individuals/ViewIndividualApiTest.php new file mode 100644 index 0000000..d4b94d9 --- /dev/null +++ b/tests/TestCase/Api/Individuals/ViewIndividualApiTest.php @@ -0,0 +1,35 @@ +setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + $url = sprintf('%s/%d', self::ENDPOINT, IndividualsFixture::INDIVIDUAL_ADMIN_ID); + $this->get($url); + + $this->assertResponseOk(); + $this->assertResponseContains(sprintf('"id": %d', IndividualsFixture::INDIVIDUAL_ADMIN_ID)); + } +} diff --git a/tests/TestCase/Api/Organisations/AddOrganisationApiTest.php b/tests/TestCase/Api/Organisations/AddOrganisationApiTest.php new file mode 100644 index 0000000..5a47554 --- /dev/null +++ b/tests/TestCase/Api/Organisations/AddOrganisationApiTest.php @@ -0,0 +1,73 @@ +setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + + $faker = \Faker\Factory::create(); + $uuid = $faker->uuid; + + $this->post( + self::ENDPOINT, + [ + 'name' => 'Test Organisation', + 'description' => $faker->text, + 'uuid' => $uuid, + 'url' => 'http://example.com', + 'nationality' => 'US', + 'sector' => 'sector', + 'type' => 'type', + ] + ); + + $this->assertResponseOk(); + $this->assertResponseContains(sprintf('"uuid": "%s"', $uuid)); + $this->assertDbRecordExists('Organisations', ['uuid' => $uuid]); + } + + public function testAddOrganisationNotAllowedAsRegularUser(): void + { + $this->setAuthToken(AuthKeysFixture::REGULAR_USER_API_KEY); + + $faker = \Faker\Factory::create(); + $uuid = $faker->uuid; + + $this->post( + self::ENDPOINT, + [ + 'name' => 'Test Organisation', + 'description' => $faker->text, + 'uuid' => $uuid, + 'url' => 'http://example.com', + 'nationality' => 'US', + 'sector' => 'sector', + 'type' => 'type', + ] + ); + + $this->assertResponseCode(405); + $this->assertDbRecordNotExists('Organisations', ['uuid' => $uuid]); + } +} diff --git a/tests/TestCase/Api/Organisations/DeleteOrganisationApiTest.php b/tests/TestCase/Api/Organisations/DeleteOrganisationApiTest.php new file mode 100644 index 0000000..efdaa5c --- /dev/null +++ b/tests/TestCase/Api/Organisations/DeleteOrganisationApiTest.php @@ -0,0 +1,46 @@ +setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + $url = sprintf('%s/%d', self::ENDPOINT, OrganisationsFixture::ORGANISATION_B_ID); + $this->delete($url); + + $this->assertResponseOk(); + $this->assertDbRecordNotExists('Organisations', ['id' => OrganisationsFixture::ORGANISATION_B_ID]); + } + + public function testDeleteOrganisationNotAllowedAsRegularUser(): void + { + $this->setAuthToken(AuthKeysFixture::REGULAR_USER_API_KEY); + $url = sprintf('%s/%d', self::ENDPOINT, OrganisationsFixture::ORGANISATION_B_ID); + $this->delete($url); + + $this->assertResponseCode(405); + $this->assertDbRecordExists('Organisations', ['id' => OrganisationsFixture::ORGANISATION_B_ID]); + } +} diff --git a/tests/TestCase/Api/Organisations/EditOrganisationApiTest.php b/tests/TestCase/Api/Organisations/EditOrganisationApiTest.php new file mode 100644 index 0000000..6d14f3c --- /dev/null +++ b/tests/TestCase/Api/Organisations/EditOrganisationApiTest.php @@ -0,0 +1,69 @@ +setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + + $url = sprintf('%s/%d', self::ENDPOINT, OrganisationsFixture::ORGANISATION_A_ID); + $this->put( + $url, + [ + 'name' => 'Test Organisation 4321', + ] + ); + + $this->assertResponseOk(); + $this->assertDbRecordExists( + 'Organisations', + [ + 'id' => OrganisationsFixture::ORGANISATION_A_ID, + 'name' => 'Test Organisation 4321', + ] + ); + } + + public function testEditOrganisationNotAllowedAsRegularUser(): void + { + $this->setAuthToken(AuthKeysFixture::REGULAR_USER_API_KEY); + + $url = sprintf('%s/%d', self::ENDPOINT, OrganisationsFixture::ORGANISATION_B_ID); + $this->put( + $url, + [ + 'name' => 'Test Organisation 1234' + ] + ); + + $this->assertResponseCode(405); + $this->assertDbRecordNotExists( + 'Organisations', + [ + 'id' => OrganisationsFixture::ORGANISATION_B_ID, + 'name' => 'Test Organisation 1234' + ] + ); + } +} diff --git a/tests/TestCase/Api/Organisations/IndexOrganisationsApiTest.php b/tests/TestCase/Api/Organisations/IndexOrganisationsApiTest.php new file mode 100644 index 0000000..a22e0f4 --- /dev/null +++ b/tests/TestCase/Api/Organisations/IndexOrganisationsApiTest.php @@ -0,0 +1,35 @@ +setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + $this->get(self::ENDPOINT); + + $this->assertResponseOk(); + $this->assertResponseContains(sprintf('"id": %d', OrganisationsFixture::ORGANISATION_A_ID)); + $this->assertResponseContains(sprintf('"id": %d', OrganisationsFixture::ORGANISATION_B_ID)); + } +} diff --git a/tests/TestCase/Api/Organisations/TagOrganisationApiTest.php b/tests/TestCase/Api/Organisations/TagOrganisationApiTest.php new file mode 100644 index 0000000..f8bd194 --- /dev/null +++ b/tests/TestCase/Api/Organisations/TagOrganisationApiTest.php @@ -0,0 +1,74 @@ +setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + + $url = sprintf('%s/%d', self::ENDPOINT, OrganisationsFixture::ORGANISATION_A_ID); + $this->post( + $url, + [ + 'tag_list' => "[\"red\"]" + ] + ); + + $this->assertResponseOk(); + $this->assertDbRecordExists( + 'TagsTagged', + [ + 'tag_id' => TagsTagsFixture::TAG_RED_ID, + 'fk_id' => OrganisationsFixture::ORGANISATION_A_ID, + 'fk_model' => 'Organisations' + ] + ); + } + + public function testTagOrganisationNotAllowedAsRegularUser(): void + { + $this->setAuthToken(AuthKeysFixture::REGULAR_USER_API_KEY); + + $url = sprintf('%s/%d', self::ENDPOINT, OrganisationsFixture::ORGANISATION_A_ID); + $this->post( + $url, + [ + 'tag_list' => "[\"green\"]" + ] + ); + + $this->assertResponseCode(405); + $this->assertDbRecordNotExists( + 'TagsTagged', + [ + 'tag_id' => TagsTagsFixture::TAG_GREEN_ID, + 'fk_id' => OrganisationsFixture::ORGANISATION_A_ID, + 'fk_model' => 'Organisations' + ] + ); + } +} diff --git a/tests/TestCase/Api/Organisations/UntagOrganisationApiTest.php b/tests/TestCase/Api/Organisations/UntagOrganisationApiTest.php new file mode 100644 index 0000000..59f1bea --- /dev/null +++ b/tests/TestCase/Api/Organisations/UntagOrganisationApiTest.php @@ -0,0 +1,74 @@ +setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + + $url = sprintf('%s/%d', self::ENDPOINT, OrganisationsFixture::ORGANISATION_A_ID); + $this->post( + $url, + [ + 'tag_list' => "[\"org-a\"]" + ] + ); + + $this->assertResponseOk(); + $this->assertDbRecordNotExists( + 'TagsTagged', + [ + 'tag_id' => TagsTagsFixture::TAG_ORG_A_ID, + 'fk_id' => OrganisationsFixture::ORGANISATION_A_ID, + 'fk_model' => 'Organisations' + ] + ); + } + + public function testUntagOrganisationNotAllowedAsRegularUser(): void + { + $this->setAuthToken(AuthKeysFixture::REGULAR_USER_API_KEY); + + $url = sprintf('%s/%d', self::ENDPOINT, OrganisationsFixture::ORGANISATION_A_ID); + $this->post( + $url, + [ + 'tag_list' => "[\"org-a\"]" + ] + ); + + $this->assertResponseCode(405); + $this->assertDbRecordExists( + 'TagsTagged', + [ + 'tag_id' => TagsTagsFixture::TAG_ORG_A_ID, + 'fk_id' => OrganisationsFixture::ORGANISATION_A_ID, + 'fk_model' => 'Organisations' + ] + ); + } +} diff --git a/tests/TestCase/Api/Organisations/ViewOrganisationApiTest.php b/tests/TestCase/Api/Organisations/ViewOrganisationApiTest.php new file mode 100644 index 0000000..a9a728b --- /dev/null +++ b/tests/TestCase/Api/Organisations/ViewOrganisationApiTest.php @@ -0,0 +1,37 @@ +setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + $url = sprintf('%s/%d', self::ENDPOINT, OrganisationsFixture::ORGANISATION_A_ID); + $this->get($url); + + $this->assertResponseOk(); + $this->assertResponseContains('"name": "Organisation A"'); + } +} diff --git a/tests/TestCase/Api/SharingGroups/AddSharingGroupApiTest.php b/tests/TestCase/Api/SharingGroups/AddSharingGroupApiTest.php new file mode 100644 index 0000000..cbfebbb --- /dev/null +++ b/tests/TestCase/Api/SharingGroups/AddSharingGroupApiTest.php @@ -0,0 +1,78 @@ +setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + + $faker = \Faker\Factory::create(); + $uuid = $faker->uuid; + + $this->post( + self::ENDPOINT, + [ + 'uuid' => $uuid, + 'name' => 'Test Sharing Group', + 'releasability' => 'Test Sharing Group releasability', + 'description' => 'Test Sharing Group description', + 'organisation_id' => OrganisationsFixture::ORGANISATION_A_ID, + 'user_id' => UsersFixture::USER_ADMIN_ID, + 'active' => true, + 'local' => true + ] + ); + + $this->assertResponseOk(); + $this->assertResponseContains(sprintf('"uuid": "%s"', $uuid)); + $this->assertDbRecordExists('SharingGroups', ['uuid' => $uuid]); + } + + public function testAddSharingGroupNotAllowedAsRegularUser(): void + { + $this->setAuthToken(AuthKeysFixture::REGULAR_USER_API_KEY); + + $faker = \Faker\Factory::create(); + $uuid = $faker->uuid; + + $this->post( + self::ENDPOINT, + [ + 'uuid' => $uuid, + 'name' => 'Test Sharing Group', + 'releasability' => 'Sharing Group A', + 'description' => 'Sharing Group A description', + 'organisation_id' => OrganisationsFixture::ORGANISATION_A_ID, + 'user_id' => UsersFixture::USER_ADMIN_ID, + 'active' => true, + 'local' => true + ] + ); + + $this->assertResponseCode(405); + $this->assertDbRecordNotExists('SharingGroups', ['uuid' => $uuid]); + } +} diff --git a/tests/TestCase/Api/SharingGroups/DeleteSharingGroupApiTest.php b/tests/TestCase/Api/SharingGroups/DeleteSharingGroupApiTest.php new file mode 100644 index 0000000..e2d1dc5 --- /dev/null +++ b/tests/TestCase/Api/SharingGroups/DeleteSharingGroupApiTest.php @@ -0,0 +1,46 @@ +setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + $url = sprintf('%s/%d', self::ENDPOINT, SharingGroupsFixture::SHARING_GROUP_A_ID); + $this->delete($url); + + $this->assertResponseOk(); + $this->assertDbRecordNotExists('SharingGroups', ['id' => SharingGroupsFixture::SHARING_GROUP_A_ID]); + } + + public function testDeleteSharingGroupNotAllowedAsRegularUser(): void + { + $this->setAuthToken(AuthKeysFixture::REGULAR_USER_API_KEY); + $url = sprintf('%s/%d', self::ENDPOINT, SharingGroupsFixture::SHARING_GROUP_A_ID); + $this->delete($url); + + $this->assertResponseCode(405); + $this->assertDbRecordExists('SharingGroups', ['id' => SharingGroupsFixture::SHARING_GROUP_A_ID]); + } +} diff --git a/tests/TestCase/Api/SharingGroups/EditSharingGroupApiTest.php b/tests/TestCase/Api/SharingGroups/EditSharingGroupApiTest.php new file mode 100644 index 0000000..07dff5b --- /dev/null +++ b/tests/TestCase/Api/SharingGroups/EditSharingGroupApiTest.php @@ -0,0 +1,70 @@ +setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + + $url = sprintf('%s/%d', self::ENDPOINT, SharingGroupsFixture::SHARING_GROUP_A_ID); + $this->put( + $url, + [ + 'name' => 'Test Sharing Group 4321', + ] + ); + + $this->assertResponseOk(); + $this->assertDbRecordExists( + 'SharingGroups', + [ + 'id' => SharingGroupsFixture::SHARING_GROUP_A_ID, + 'name' => 'Test Sharing Group 4321', + ] + ); + } + + public function testEditSharingGroupNotAllowedAsRegularUser(): void + { + $this->setAuthToken(AuthKeysFixture::REGULAR_USER_API_KEY); + + $url = sprintf('%s/%d', self::ENDPOINT, SharingGroupsFixture::SHARING_GROUP_B_ID); + $this->put( + $url, + [ + 'name' => 'Test Sharing Group 1234' + ] + ); + + $this->assertResponseCode(405); + $this->assertDbRecordNotExists( + 'SharingGroups', + [ + 'id' => SharingGroupsFixture::SHARING_GROUP_B_ID, + 'name' => 'Test Sharing Group 1234' + ] + ); + } +} diff --git a/tests/TestCase/Api/SharingGroups/IndexSharingGroupsApiTest.php b/tests/TestCase/Api/SharingGroups/IndexSharingGroupsApiTest.php new file mode 100644 index 0000000..5286af2 --- /dev/null +++ b/tests/TestCase/Api/SharingGroups/IndexSharingGroupsApiTest.php @@ -0,0 +1,36 @@ +setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + $this->get(self::ENDPOINT); + + $this->assertResponseOk(); + $this->assertResponseContains(sprintf('"id": %d', SharingGroupsFixture::SHARING_GROUP_A_ID)); + $this->assertResponseContains(sprintf('"id": %d', SharingGroupsFixture::SHARING_GROUP_B_ID)); + } +} diff --git a/tests/TestCase/Api/SharingGroups/ViewSharingGroupApiTest.php b/tests/TestCase/Api/SharingGroups/ViewSharingGroupApiTest.php new file mode 100644 index 0000000..06ceb93 --- /dev/null +++ b/tests/TestCase/Api/SharingGroups/ViewSharingGroupApiTest.php @@ -0,0 +1,36 @@ +setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + $url = sprintf('%s/%d', self::ENDPOINT, SharingGroupsFixture::SHARING_GROUP_A_ID); + $this->get($url); + + $this->assertResponseOk(); + $this->assertResponseContains(sprintf('"id": %d', SharingGroupsFixture::SHARING_GROUP_A_ID)); + } +} diff --git a/tests/TestCase/Api/Tags/IndexTagsApiTest.php b/tests/TestCase/Api/Tags/IndexTagsApiTest.php new file mode 100644 index 0000000..4b13b67 --- /dev/null +++ b/tests/TestCase/Api/Tags/IndexTagsApiTest.php @@ -0,0 +1,36 @@ +setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + $this->get(self::ENDPOINT); + + + $this->assertResponseOk(); + $this->assertResponseContains('"name": "red"'); + $this->assertResponseContains('"name": "green"'); + $this->assertResponseContains('"name": "blue"'); + } +} diff --git a/tests/TestCase/Api/Users/AddUserApiTest.php b/tests/TestCase/Api/Users/AddUserApiTest.php new file mode 100644 index 0000000..3437d29 --- /dev/null +++ b/tests/TestCase/Api/Users/AddUserApiTest.php @@ -0,0 +1,66 @@ +setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + $this->post( + self::ENDPOINT, + [ + 'individual_id' => UsersFixture::USER_REGULAR_USER_ID, + 'organisation_id' => OrganisationsFixture::ORGANISATION_A_ID, + 'role_id' => RolesFixture::ROLE_REGULAR_USER_ID, + 'disabled' => false, + 'username' => 'test', + 'password' => 'Password123456!', + ] + ); + + $this->assertResponseOk(); + $this->assertResponseContains('"username": "test"'); + $this->assertDbRecordExists('Users', ['username' => 'test']); + } + + public function testAddUserNotAllowedAsRegularUser(): void + { + $this->setAuthToken(AuthKeysFixture::REGULAR_USER_API_KEY); + $this->post( + self::ENDPOINT, + [ + 'individual_id' => UsersFixture::USER_REGULAR_USER_ID, + 'organisation_id' => OrganisationsFixture::ORGANISATION_A_ID, + 'role_id' => RolesFixture::ROLE_REGULAR_USER_ID, + 'disabled' => false, + 'username' => 'test', + 'password' => 'Password123456!' + ] + ); + + $this->assertResponseCode(405); + $this->assertDbRecordNotExists('Users', ['username' => 'test']); + } +} diff --git a/tests/TestCase/Api/Users/ChangePasswordApiTest.php b/tests/TestCase/Api/Users/ChangePasswordApiTest.php new file mode 100644 index 0000000..201cb74 --- /dev/null +++ b/tests/TestCase/Api/Users/ChangePasswordApiTest.php @@ -0,0 +1,74 @@ +initializeOpenApiValidator(); + + $this->collection = new ComponentRegistry(); + $this->auth = new FormAuthenticate($this->collection, [ + 'userModel' => 'Users', + ]); + } + + public function testChangePasswordOwnUser(): void + { + $this->setAuthToken(AuthKeysFixture::REGULAR_USER_API_KEY); + $newPassword = 'Test12345678!'; + + $this->put( + self::ENDPOINT, + [ + 'password' => $newPassword, + ] + ); + + $this->assertResponseOk(); + + // Test new password with form login + $request = new ServerRequest([ + 'url' => 'users/login', + 'post' => [ + 'username' => UsersFixture::USER_REGULAR_USER_USERNAME, + 'password' => $newPassword + ], + ]); + $result = $this->auth->authenticate($request, new Response()); + + $this->assertEquals(UsersFixture::USER_REGULAR_USER_ID, $result['id']); + $this->assertEquals(UsersFixture::USER_REGULAR_USER_USERNAME, $result['username']); + } +} diff --git a/tests/TestCase/Api/Users/DeleteUserApiTest.php b/tests/TestCase/Api/Users/DeleteUserApiTest.php new file mode 100644 index 0000000..9288b9a --- /dev/null +++ b/tests/TestCase/Api/Users/DeleteUserApiTest.php @@ -0,0 +1,45 @@ +setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + $url = sprintf('%s/%d', self::ENDPOINT, UsersFixture::USER_REGULAR_USER_ID); + $this->delete($url); + + $this->assertResponseOk(); + $this->assertDbRecordNotExists('Users', ['id' => UsersFixture::USER_REGULAR_USER_ID]); + } + + public function testDeleteUserNotAllowedAsRegularUser(): void + { + $this->setAuthToken(AuthKeysFixture::REGULAR_USER_API_KEY); + $url = sprintf('%s/%d', self::ENDPOINT, UsersFixture::USER_ORG_ADMIN_ID); + $this->delete($url); + + $this->assertResponseCode(405); + $this->assertDbRecordExists('Users', ['id' => UsersFixture::USER_ORG_ADMIN_ID]); + } +} diff --git a/tests/TestCase/Api/Users/EditUserApiTest.php b/tests/TestCase/Api/Users/EditUserApiTest.php new file mode 100644 index 0000000..dd685b9 --- /dev/null +++ b/tests/TestCase/Api/Users/EditUserApiTest.php @@ -0,0 +1,62 @@ +setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + $url = sprintf('%s/%d', self::ENDPOINT, UsersFixture::USER_REGULAR_USER_ID); + $this->put( + $url, + [ + 'id' => UsersFixture::USER_REGULAR_USER_ID, + 'role_id' => RolesFixture::ROLE_ORG_ADMIN_ID, + ] + ); + + $this->assertResponseOk(); + $this->assertDbRecordExists('Users', [ + 'id' => UsersFixture::USER_REGULAR_USER_ID, + 'role_id' => RolesFixture::ROLE_ORG_ADMIN_ID + ]); + } + + public function testEditRoleNotAllowedAsRegularUser(): void + { + $this->setAuthToken(AuthKeysFixture::REGULAR_USER_API_KEY); + $this->put( + self::ENDPOINT, + [ + 'role_id' => RolesFixture::ROLE_ADMIN_ID, + ] + ); + + $this->assertResponseOk(); + $this->assertDbRecordNotExists('Users', [ + 'id' => UsersFixture::USER_REGULAR_USER_ID, + 'role_id' => RolesFixture::ROLE_ADMIN_ID + ]); + } +} diff --git a/tests/TestCase/Api/Users/IndexUsersApiTest.php b/tests/TestCase/Api/Users/IndexUsersApiTest.php new file mode 100644 index 0000000..c30462b --- /dev/null +++ b/tests/TestCase/Api/Users/IndexUsersApiTest.php @@ -0,0 +1,34 @@ +setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + $this->get(self::ENDPOINT); + + $this->assertResponseOk(); + $this->assertResponseContains(sprintf('"username": "%s"', UsersFixture::USER_ADMIN_USERNAME)); + } +} diff --git a/tests/TestCase/Api/Users/ViewUserApiTest.php b/tests/TestCase/Api/Users/ViewUserApiTest.php new file mode 100644 index 0000000..e3ab847 --- /dev/null +++ b/tests/TestCase/Api/Users/ViewUserApiTest.php @@ -0,0 +1,44 @@ +setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + $this->get(self::ENDPOINT); + + $this->assertResponseOk(); + $this->assertResponseContains(sprintf('"username": "%s"', UsersFixture::USER_ADMIN_USERNAME)); + } + + public function testViewUserById(): void + { + $this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + $url = sprintf('%s/%d', self::ENDPOINT, UsersFixture::USER_REGULAR_USER_ID); + $this->get($url); + + $this->assertResponseOk(); + $this->assertResponseContains(sprintf('"username": "%s"', UsersFixture::USER_REGULAR_USER_USERNAME)); + } +} diff --git a/tests/TestCase/ApplicationTest.php b/tests/TestCase/ApplicationTest.php index cd09f1e..e2d3183 100644 --- a/tests/TestCase/ApplicationTest.php +++ b/tests/TestCase/ApplicationTest.php @@ -40,10 +40,14 @@ class ApplicationTest extends IntegrationTestCase $app->bootstrap(); $plugins = $app->getPlugins(); - $this->assertCount(3, $plugins); + $this->assertCount(7, $plugins); $this->assertSame('Bake', $plugins->get('Bake')->getName()); $this->assertSame('DebugKit', $plugins->get('DebugKit')->getName()); $this->assertSame('Migrations', $plugins->get('Migrations')->getName()); + $this->assertSame('Authentication', $plugins->get('Authentication')->getName()); + $this->assertSame('ADmad/SocialAuth', $plugins->get('ADmad/SocialAuth')->getName()); + $this->assertSame('Tags', $plugins->get('Tags')->getName()); + $this->assertSame('Cake/TwigView', $plugins->get('Cake/TwigView')->getName()); } /** diff --git a/tests/TestCase/Controller/PagesControllerTest.php b/tests/TestCase/Controller/PagesControllerTest.php deleted file mode 100644 index f2958f9..0000000 --- a/tests/TestCase/Controller/PagesControllerTest.php +++ /dev/null @@ -1,126 +0,0 @@ -get('/'); - $this->assertResponseOk(); - $this->get('/'); - $this->assertResponseOk(); - } - - /** - * testDisplay method - * - * @return void - */ - public function testDisplay() - { - $this->get('/pages/home'); - $this->assertResponseOk(); - $this->assertResponseContains('CakePHP'); - $this->assertResponseContains(''); - } - - /** - * Test that missing template renders 404 page in production - * - * @return void - */ - public function testMissingTemplate() - { - Configure::write('debug', false); - $this->get('/pages/not_existing'); - - $this->assertResponseError(); - $this->assertResponseContains('Error'); - } - - /** - * Test that missing template in debug mode renders missing_template error page - * - * @return void - */ - public function testMissingTemplateInDebug() - { - Configure::write('debug', true); - $this->get('/pages/not_existing'); - - $this->assertResponseFailure(); - $this->assertResponseContains('Missing Template'); - $this->assertResponseContains('Stacktrace'); - $this->assertResponseContains('not_existing.php'); - } - - /** - * Test directory traversal protection - * - * @return void - */ - public function testDirectoryTraversalProtection() - { - $this->get('/pages/../Layout/ajax'); - $this->assertResponseCode(403); - $this->assertResponseContains('Forbidden'); - } - - /** - * Test that CSRF protection is applied to page rendering. - * - * @reutrn void - */ - public function testCsrfAppliedError() - { - $this->post('/pages/home', ['hello' => 'world']); - - $this->assertResponseCode(403); - $this->assertResponseContains('CSRF'); - } - - /** - * Test that CSRF protection is applied to page rendering. - * - * @reutrn void - */ - public function testCsrfAppliedOk() - { - $this->enableCsrfToken(); - $this->post('/pages/home', ['hello' => 'world']); - - $this->assertResponseCode(200); - $this->assertResponseContains('CakePHP'); - } -} diff --git a/tests/TestCase/Controller/Users/UsersControllerTest.php b/tests/TestCase/Controller/Users/UsersControllerTest.php new file mode 100644 index 0000000..7ef6e4c --- /dev/null +++ b/tests/TestCase/Controller/Users/UsersControllerTest.php @@ -0,0 +1,32 @@ +enableCsrfToken(); + $this->enableSecurityToken(); + + $this->post('/users/login', [ + 'username' => UsersFixture::USER_ADMIN_USERNAME, + 'password' => UsersFixture::USER_ADMIN_PASSWORD, + ]); + + $this->assertSessionHasKey('authUser.uuid'); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 962815c..7523c28 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,4 +1,5 @@ runMany([ + ['connection' => 'test'], + ['plugin' => 'Tags', 'connection' => 'test'], + ['plugin' => 'ADmad/SocialAuth', 'connection' => 'test'] + ]); +} else { + echo "[ * ] Skipping DB migrations ...\n"; +} + +$specFile = $_ENV['OPENAPI_SPEC'] ?? APP . '../webroot/docs/openapi.yaml'; +// Initialize OpenAPI spec validator +Configure::write('App.OpenAPIValidator', (new ValidatorBuilder)->fromYamlFile($specFile)); diff --git a/webroot/css/CodeMirror/codemirror-additional.css b/webroot/css/CodeMirror/codemirror-additional.css index 0c62e18..4399c1a 100644 --- a/webroot/css/CodeMirror/codemirror-additional.css +++ b/webroot/css/CodeMirror/codemirror-additional.css @@ -26,4 +26,8 @@ .CodeMirror-hints { z-index: 1060 !important; /* Make sure hint is above modal */ +} + +.CodeMirror pre.CodeMirror-placeholder { + color: #999; } \ No newline at end of file diff --git a/webroot/css/themes/additional/bootstrap-additional.css b/webroot/css/themes/additional/bootstrap-additional.css index 856c413..1a992d8 100644 --- a/webroot/css/themes/additional/bootstrap-additional.css +++ b/webroot/css/themes/additional/bootstrap-additional.css @@ -1,322 +1,407 @@ /* Callout */ .callout { border: 1px solid #e9ecef; - border-radius: .25rem; + border-radius: 0.25rem; background-color: #fff; - box-shadow: 0 0 35px 0 rgba(154, 161, 171, 0.25); } + box-shadow: 0 0 35px 0 rgba(154, 161, 171, 0.25); +} .callout-primary { border-left-color: #0d6efd; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-secondary { border-left-color: #6c757d; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-success { border-left-color: #198754; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-info { border-left-color: #0dcaf0; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-warning { border-left-color: #ffc107; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-danger { border-left-color: #dc3545; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-light { border-left-color: #f8f9fa; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-dark { border-left-color: #212529; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} /* Toasts */ .toast { - min-width: 250px; } + min-width: 250px; +} .toast-primary { color: #04214c; background-color: #b6d4fe; - border-color: #9ec5fe; } - .toast-primary strong { - border-top-color: #85b6fe; } + border-color: #9ec5fe; +} +.toast-primary strong { + border-top-color: #85b6fe; +} .toast-secondary { color: #202326; background-color: #d3d6d8; - border-color: #c4c8cb; } - .toast-secondary strong { - border-top-color: #b6bbbf; } + border-color: #c4c8cb; +} +.toast-secondary strong { + border-top-color: #b6bbbf; +} .toast-success { color: #082919; background-color: #badbcc; - border-color: #a3cfbb; } - .toast-success strong { - border-top-color: #92c6af; } + border-color: #a3cfbb; +} +.toast-success strong { + border-top-color: #92c6af; +} .toast-info { color: #043d48; background-color: #b6effb; - border-color: #9eeaf9; } - .toast-info strong { - border-top-color: #86e5f8; } + border-color: #9eeaf9; +} +.toast-info strong { + border-top-color: #86e5f8; +} .toast-warning { color: #4d3a02; background-color: #ffecb5; - border-color: #ffe69c; } - .toast-warning strong { - border-top-color: #ffe083; } + border-color: #ffe69c; +} +.toast-warning strong { + border-top-color: #ffe083; +} .toast-danger { color: #421015; background-color: #f5c2c7; - border-color: #f1aeb5; } - .toast-danger strong { - border-top-color: #ed98a1; } + border-color: #f1aeb5; +} +.toast-danger strong { + border-top-color: #ed98a1; +} .toast-light { color: #4a4b4b; background-color: #fdfdfe; - border-color: #fcfdfd; } - .toast-light strong { - border-top-color: #edf3f3; } + border-color: #fcfdfd; +} +.toast-light strong { + border-top-color: #edf3f3; +} .toast-dark { color: #0a0b0c; background-color: #bcbebf; - border-color: #a6a8a9; } - .toast-dark strong { - border-top-color: #999b9c; } + border-color: #a6a8a9; +} +.toast-dark strong { + border-top-color: #999b9c; +} /* Dropdown-item */ .dropdown-item.dropdown-item-primary { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #0d6efd; } .dropdown-item.dropdown-item-primary:hover { color: #fff; background-color: #0b5ed7; } +======= + background-color: #0d6efd; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-primary:hover { color: #fff; - background-color: #0d6efd; } - + background-color: #0d6efd; +} .dropdown-item.dropdown-item-secondary { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #6c757d; } .dropdown-item.dropdown-item-secondary:hover { color: #fff; background-color: #5c636a; } +======= + background-color: #6c757d; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-secondary:hover { color: #fff; - background-color: #6c757d; } - + background-color: #6c757d; +} .dropdown-item.dropdown-item-success { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #198754; } .dropdown-item.dropdown-item-success:hover { color: #fff; background-color: #157347; } +======= + background-color: #198754; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-success:hover { color: #fff; - background-color: #198754; } - + background-color: #198754; +} .dropdown-item.dropdown-item-info { color: #000; text-decoration: none; +<<<<<<< HEAD background-color: #0dcaf0; } .dropdown-item.dropdown-item-info:hover { color: #000; background-color: #31d2f2; } +======= + background-color: #0dcaf0; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-info:hover { color: #000; - background-color: #0dcaf0; } - + background-color: #0dcaf0; +} .dropdown-item.dropdown-item-warning { color: #000; text-decoration: none; +<<<<<<< HEAD background-color: #ffc107; } .dropdown-item.dropdown-item-warning:hover { color: #000; background-color: #ffca2c; } +======= + background-color: #ffc107; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-warning:hover { color: #000; - background-color: #ffc107; } - + background-color: #ffc107; +} .dropdown-item.dropdown-item-danger { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #dc3545; } .dropdown-item.dropdown-item-danger:hover { color: #fff; background-color: #bb2d3b; } +======= + background-color: #dc3545; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-danger:hover { color: #fff; - background-color: #dc3545; } - + background-color: #dc3545; +} .dropdown-item.dropdown-item-light { color: #000; text-decoration: none; +<<<<<<< HEAD background-color: #f8f9fa; } .dropdown-item.dropdown-item-light:hover { color: #000; background-color: #f9fafb; } +======= + background-color: #f8f9fa; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-light:hover { color: #000; - background-color: #f8f9fa; } - + background-color: #f8f9fa; +} .dropdown-item.dropdown-item-dark { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #212529; } .dropdown-item.dropdown-item-dark:hover { color: #fff; background-color: #1c1f23; } +======= + background-color: #212529; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-dark:hover { color: #fff; - background-color: #212529; } + background-color: #212529; +} /* Progress Timeline */ .progress-timeline { - padding: 0.2em 0.2em 0.5em 0.2em; } - .progress-timeline ul { - position: relative; - padding: 0; } - .progress-timeline li { - list-style-type: none; - position: relative; } - .progress-timeline li.progress-inactive { - opacity: 0.5; } - .progress-timeline .progress-line { - height: 2px; } - .progress-timeline .progress-line.progress-inactive { - opacity: 0.5; } + padding: 0.2em 0.2em 0.5em 0.2em; +} +.progress-timeline ul { + position: relative; + padding: 0; +} +.progress-timeline li { + list-style-type: none; + position: relative; +} +.progress-timeline li.progress-inactive { + opacity: 0.5; +} +.progress-timeline .progress-line { + height: 2px; +} +.progress-timeline .progress-line.progress-inactive { + opacity: 0.5; +} /* Forms severity */ .form-control.is-invalid.info { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%230dcaf0' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%230dcaf0' stroke='none'/%3e%3c/svg%3e"); } - .form-control.is-invalid.info:focus { - border-color: #0dcaf0; - box-shadow: 0 0 0 0.25rem rgba(13, 202, 240, 0.25); } - + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%230dcaf0' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%230dcaf0' stroke='none'/%3e%3c/svg%3e"); +} +.form-control.is-invalid.info:focus { + border-color: #0dcaf0; + box-shadow: 0 0 0 0.25rem rgba(13, 202, 240, 0.25); +} .form-control.is-invalid.warning { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23ffc107' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23ffc107' stroke='none'/%3e%3c/svg%3e"); } - .form-control.is-invalid.warning:focus { - border-color: #ffc107; - box-shadow: 0 0 0 0.25rem rgba(255, 193, 7, 0.25); } + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23ffc107' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23ffc107' stroke='none'/%3e%3c/svg%3e"); +} +.form-control.is-invalid.warning:focus { + border-color: #ffc107; + box-shadow: 0 0 0 0.25rem rgba(255, 193, 7, 0.25); +} .form-select.is-invalid:not([multiple]):not([size]).info, -.form-select.is-invalid:not([multiple])[size="1"] -.form-select.is-invalid.info { +.form-select.is-invalid:not([multiple])[size="1"] .form-select.is-invalid.info { background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%230dcaf0'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%230dcaf0' stroke='none'/%3e%3c/svg%3e"); - background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); } - .form-select.is-invalid:not([multiple]):not([size]).info:focus, - .form-select.is-invalid:not([multiple])[size="1"] -.form-select.is-invalid.info:focus { - box-shadow: 0 0 0 0.25rem rgba(13, 202, 240, 0.25); } - + background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} +.form-select.is-invalid:not([multiple]):not([size]).info:focus, +.form-select.is-invalid:not([multiple])[size="1"] .form-select.is-invalid.info:focus { + box-shadow: 0 0 0 0.25rem rgba(13, 202, 240, 0.25); +} .form-select.is-invalid:not([multiple]):not([size]).warning, -.form-select.is-invalid:not([multiple])[size="1"] -.form-select.is-invalid.warning { +.form-select.is-invalid:not([multiple])[size="1"] .form-select.is-invalid.warning { background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23ffc107'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23ffc107' stroke='none'/%3e%3c/svg%3e"); - background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); } - .form-select.is-invalid:not([multiple]):not([size]).warning:focus, - .form-select.is-invalid:not([multiple])[size="1"] -.form-select.is-invalid.warning:focus { - box-shadow: 0 0 0 0.25rem rgba(255, 193, 7, 0.25); } + background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} +.form-select.is-invalid:not([multiple]):not([size]).warning:focus, +.form-select.is-invalid:not([multiple])[size="1"] .form-select.is-invalid.warning:focus { + box-shadow: 0 0 0 0.25rem rgba(255, 193, 7, 0.25); +} .form-check-input.is-invalid.info { - border-color: #0dcaf0; } - + border-color: #0dcaf0; +} .form-check-input.is-invalid.info:checked { - background-color: #0dcaf0; } - + background-color: #0dcaf0; +} .form-check-input.is-invalid.info ~ .form-check-label { - color: unset; } - + color: unset; +} .form-check-input.is-invalid.info:focus { - box-shadow: 0 0 0 0.2rem rgba(13, 202, 240, 0.25); } - + box-shadow: 0 0 0 0.2rem rgba(13, 202, 240, 0.25); +} .form-check-input.is-invalid.warning { - border-color: #ffc107; } - + border-color: #ffc107; +} .form-check-input.is-invalid.warning:checked { - background-color: #ffc107; } - + background-color: #ffc107; +} .form-check-input.is-invalid.warning ~ .form-check-label { - color: unset; } - + color: unset; +} .form-check-input.is-invalid.warning:focus { - box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.25); } + box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.25); +} /* Utilities */ .mw-75 { - max-width: 75% !important; } + max-width: 75% !important; +} .mw-66 { max-width: 66% !important; } .mw-50 { - max-width: 50% !important; } + max-width: 50% !important; +} .mw-33 { max-width: 33% !important; } .mw-25 { - max-width: 25% !important; } + max-width: 25% !important; +} .mh-75 { - max-height: 75% !important; } + max-height: 75% !important; +} .mh-66 { max-height: 66% !important; } .mh-50 { - max-height: 50% !important; } + max-height: 50% !important; +} .mh-33 { max-height: 33% !important; } .mh-25 { - max-height: 25% !important; } + max-height: 25% !important; +} .p-abs-center-y { top: 50%; - transform: translateY(-50%); } + transform: translateY(-50%); +} .p-abs-center-x { left: 50%; - transform: translateX(-50%); } + transform: translateX(-50%); +} .p-abs-center-both { top: 50%; left: 50%; +<<<<<<< HEAD transform: translateX(-50%) translateY(-50%); } .fs-7 { @@ -372,3 +457,7 @@ border-bottom-left-radius: 3px; border-bottom-right-radius: 3px; height: 100%; } +======= + transform: translateX(-50%) translateY(-50%); +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f diff --git a/webroot/css/themes/theme-darkly.css b/webroot/css/themes/theme-darkly.css index 20753ce..1243863 100644 --- a/webroot/css/themes/theme-darkly.css +++ b/webroot/css/themes/theme-darkly.css @@ -1,323 +1,408 @@ /* Callout */ .callout { border: 1px solid 1px solid none; - border-radius: .25rem; + border-radius: 0.25rem; background-color: #363636; - box-shadow: none; } + box-shadow: none; +} .callout-primary { border-left-color: #375a7f; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-secondary { border-left-color: #444; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-success { border-left-color: #00bc8c; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-info { border-left-color: #3498db; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-warning { border-left-color: #f39c12; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-danger { border-left-color: #e74c3c; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-light { border-left-color: #adb5bd; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-dark { border-left-color: #303030; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} /* Toasts */ .toast { - min-width: 250px; } + min-width: 250px; +} .toast-primary { color: #111b26; background-color: #c3ced9; - border-color: #afbdcc; } - .toast-primary strong { - border-top-color: #9fb0c2; } + border-color: #afbdcc; +} +.toast-primary strong { + border-top-color: #9fb0c2; +} .toast-secondary { color: #141414; background-color: #c7c7c7; - border-color: #b4b4b4; } - .toast-secondary strong { - border-top-color: #a7a7a7; } + border-color: #b4b4b4; +} +.toast-secondary strong { + border-top-color: #a7a7a7; +} .toast-success { color: #00382a; background-color: #b3ebdd; - border-color: #99e4d1; } - .toast-success strong { - border-top-color: #85dfc8; } + border-color: #99e4d1; +} +.toast-success strong { + border-top-color: #85dfc8; +} .toast-info { color: #102e42; background-color: #c2e0f4; - border-color: #aed6f1; } - .toast-info strong { - border-top-color: #98cbed; } + border-color: #aed6f1; +} +.toast-info strong { + border-top-color: #98cbed; +} .toast-warning { color: #492f05; background-color: #fbe1b8; - border-color: #fad7a0; } - .toast-warning strong { - border-top-color: #f9cd88; } + border-color: #fad7a0; +} +.toast-warning strong { + border-top-color: #f9cd88; +} .toast-danger { color: #451712; background-color: #f8c9c5; - border-color: #f5b7b1; } - .toast-danger strong { - border-top-color: #f2a29a; } + border-color: #f5b7b1; +} +.toast-danger strong { + border-top-color: #f2a29a; +} .toast-light { color: #343639; background-color: #e6e9eb; - border-color: #dee1e5; } - .toast-light strong { - border-top-color: #d0d4da; } + border-color: #dee1e5; +} +.toast-light strong { + border-top-color: #d0d4da; +} .toast-dark { color: #0e0e0e; background-color: #c1c1c1; - border-color: #acacac; } - .toast-dark strong { - border-top-color: #9f9f9f; } + border-color: #acacac; +} +.toast-dark strong { + border-top-color: #9f9f9f; +} /* Dropdown-item */ .dropdown-item.dropdown-item-primary { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #375a7f; } .dropdown-item.dropdown-item-primary:hover { color: #fff; background-color: #2f4d6c; } +======= + background-color: #375a7f; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-primary:hover { color: #fff; - background-color: #375a7f; } - + background-color: #375a7f; +} .dropdown-item.dropdown-item-secondary { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #444; } .dropdown-item.dropdown-item-secondary:hover { color: #fff; background-color: #3a3a3a; } +======= + background-color: #444; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-secondary:hover { color: #fff; - background-color: #444; } - + background-color: #444; +} .dropdown-item.dropdown-item-success { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #00bc8c; } .dropdown-item.dropdown-item-success:hover { color: #fff; background-color: #00a077; } +======= + background-color: #00bc8c; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-success:hover { color: #fff; - background-color: #00bc8c; } - + background-color: #00bc8c; +} .dropdown-item.dropdown-item-info { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #3498db; } .dropdown-item.dropdown-item-info:hover { color: #fff; background-color: #2c81ba; } +======= + background-color: #3498db; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-info:hover { color: #fff; - background-color: #3498db; } - + background-color: #3498db; +} .dropdown-item.dropdown-item-warning { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #f39c12; } .dropdown-item.dropdown-item-warning:hover { color: #fff; background-color: #cf850f; } +======= + background-color: #f39c12; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-warning:hover { color: #fff; - background-color: #f39c12; } - + background-color: #f39c12; +} .dropdown-item.dropdown-item-danger { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #e74c3c; } .dropdown-item.dropdown-item-danger:hover { color: #fff; background-color: #c44133; } +======= + background-color: #e74c3c; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-danger:hover { color: #fff; - background-color: #e74c3c; } - + background-color: #e74c3c; +} .dropdown-item.dropdown-item-light { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #adb5bd; } .dropdown-item.dropdown-item-light:hover { color: #fff; background-color: #939aa1; } +======= + background-color: #adb5bd; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-light:hover { color: #fff; - background-color: #adb5bd; } - + background-color: #adb5bd; +} .dropdown-item.dropdown-item-dark { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #303030; } .dropdown-item.dropdown-item-dark:hover { color: #fff; background-color: #292929; } +======= + background-color: #303030; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-dark:hover { color: #fff; - background-color: #303030; } + background-color: #303030; +} /* Progress Timeline */ .progress-timeline { - padding: 0.2em 0.2em 0.5em 0.2em; } - .progress-timeline ul { - position: relative; - padding: 0; } - .progress-timeline li { - list-style-type: none; - position: relative; } - .progress-timeline li.progress-inactive { - opacity: 0.5; } - .progress-timeline .progress-line { - height: 2px; } - .progress-timeline .progress-line.progress-inactive { - opacity: 0.5; } + padding: 0.2em 0.2em 0.5em 0.2em; +} +.progress-timeline ul { + position: relative; + padding: 0; +} +.progress-timeline li { + list-style-type: none; + position: relative; +} +.progress-timeline li.progress-inactive { + opacity: 0.5; +} +.progress-timeline .progress-line { + height: 2px; +} +.progress-timeline .progress-line.progress-inactive { + opacity: 0.5; +} /* Forms severity */ .form-control.is-invalid.info { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%233498db' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%233498db' stroke='none'/%3e%3c/svg%3e"); } - .form-control.is-invalid.info:focus { - border-color: #3498db; - box-shadow: 0 0 0 0.25rem rgba(52, 152, 219, 0.25); } - + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%233498db' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%233498db' stroke='none'/%3e%3c/svg%3e"); +} +.form-control.is-invalid.info:focus { + border-color: #3498db; + box-shadow: 0 0 0 0.25rem rgba(52, 152, 219, 0.25); +} .form-control.is-invalid.warning { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23f39c12' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23f39c12' stroke='none'/%3e%3c/svg%3e"); } - .form-control.is-invalid.warning:focus { - border-color: #f39c12; - box-shadow: 0 0 0 0.25rem rgba(243, 156, 18, 0.25); } + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23f39c12' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23f39c12' stroke='none'/%3e%3c/svg%3e"); +} +.form-control.is-invalid.warning:focus { + border-color: #f39c12; + box-shadow: 0 0 0 0.25rem rgba(243, 156, 18, 0.25); +} .form-select.is-invalid:not([multiple]):not([size]).info, -.form-select.is-invalid:not([multiple])[size="1"] -.form-select.is-invalid.info { +.form-select.is-invalid:not([multiple])[size="1"] .form-select.is-invalid.info { background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%233498db'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%233498db' stroke='none'/%3e%3c/svg%3e"); - background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); } - .form-select.is-invalid:not([multiple]):not([size]).info:focus, - .form-select.is-invalid:not([multiple])[size="1"] -.form-select.is-invalid.info:focus { - box-shadow: 0 0 0 0.25rem rgba(52, 152, 219, 0.25); } - + background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} +.form-select.is-invalid:not([multiple]):not([size]).info:focus, +.form-select.is-invalid:not([multiple])[size="1"] .form-select.is-invalid.info:focus { + box-shadow: 0 0 0 0.25rem rgba(52, 152, 219, 0.25); +} .form-select.is-invalid:not([multiple]):not([size]).warning, -.form-select.is-invalid:not([multiple])[size="1"] -.form-select.is-invalid.warning { +.form-select.is-invalid:not([multiple])[size="1"] .form-select.is-invalid.warning { background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23f39c12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23f39c12' stroke='none'/%3e%3c/svg%3e"); - background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); } - .form-select.is-invalid:not([multiple]):not([size]).warning:focus, - .form-select.is-invalid:not([multiple])[size="1"] -.form-select.is-invalid.warning:focus { - box-shadow: 0 0 0 0.25rem rgba(243, 156, 18, 0.25); } + background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} +.form-select.is-invalid:not([multiple]):not([size]).warning:focus, +.form-select.is-invalid:not([multiple])[size="1"] .form-select.is-invalid.warning:focus { + box-shadow: 0 0 0 0.25rem rgba(243, 156, 18, 0.25); +} .form-check-input.is-invalid.info { - border-color: #3498db; } - + border-color: #3498db; +} .form-check-input.is-invalid.info:checked { - background-color: #3498db; } - + background-color: #3498db; +} .form-check-input.is-invalid.info ~ .form-check-label { - color: unset; } - + color: unset; +} .form-check-input.is-invalid.info:focus { - box-shadow: 0 0 0 0.2rem rgba(52, 152, 219, 0.25); } - + box-shadow: 0 0 0 0.2rem rgba(52, 152, 219, 0.25); +} .form-check-input.is-invalid.warning { - border-color: #f39c12; } - + border-color: #f39c12; +} .form-check-input.is-invalid.warning:checked { - background-color: #f39c12; } - + background-color: #f39c12; +} .form-check-input.is-invalid.warning ~ .form-check-label { - color: unset; } - + color: unset; +} .form-check-input.is-invalid.warning:focus { - box-shadow: 0 0 0 0.2rem rgba(243, 156, 18, 0.25); } + box-shadow: 0 0 0 0.2rem rgba(243, 156, 18, 0.25); +} /* Utilities */ .mw-75 { - max-width: 75% !important; } + max-width: 75% !important; +} .mw-66 { max-width: 66% !important; } .mw-50 { - max-width: 50% !important; } + max-width: 50% !important; +} .mw-33 { max-width: 33% !important; } .mw-25 { - max-width: 25% !important; } + max-width: 25% !important; +} .mh-75 { - max-height: 75% !important; } + max-height: 75% !important; +} .mh-66 { max-height: 66% !important; } .mh-50 { - max-height: 50% !important; } + max-height: 50% !important; +} .mh-33 { max-height: 33% !important; } .mh-25 { - max-height: 25% !important; } + max-height: 25% !important; +} .p-abs-center-y { top: 50%; - transform: translateY(-50%); } + transform: translateY(-50%); +} .p-abs-center-x { left: 50%; - transform: translateX(-50%); } + transform: translateX(-50%); +} .p-abs-center-both { top: 50%; left: 50%; - transform: translateX(-50%) translateY(-50%); } + transform: translateX(-50%) translateY(-50%); +} .fs-7 { font-size: .875rem !important; } @@ -380,90 +465,114 @@ body { /* background by SVGBackgrounds.com */ background-attachment: fixed; background-size: cover; - background-blend-mode: soft-light; } + background-blend-mode: soft-light; +} .panel { background-color: #363636; border: 1px solid #454545; - box-shadow: none; } + box-shadow: none; +} .loading-overlay { background-color: #222; - opacity: 0.65; } + opacity: 0.65; +} /* Top navbar */ .top-navbar { - background-color: #375a7f; } + background-color: #375a7f; +} .center-navbar nav.header-breadcrumb { - color: #fff; } + color: #fff; +} header.top-navbar .header-menu > a:hover, header.top-navbar .header-breadcrumb .header-breadcrumb-item > a:hover { - color: #d6d6d6 !important; } + color: #d6d6d6 !important; +} .top-navbar .center-navbar nav.header-breadcrumb li.header-breadcrumb-item a { - color: #fff; } + color: #fff; +} .top-navbar .right-navbar .header-menu a.nav-link { - color: #fff; } + color: #fff; +} .top-navbar .left-navbar .navbar-brand img { - filter: invert(1); } + filter: invert(1); +} .top-navbar .left-navbar .navbar-brand:hover img { - filter: invert(1) drop-shadow(0px 0px 3px #fff); } + filter: invert(1) drop-shadow(0px 0px 3px #fff); +} .top-navbar .composed-app-icon-container > .app-icon { - background-color: #fff; } + background-color: #fff; +} .breadcrumb-link-container { box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 2px 6px 0 rgba(0, 0, 0, 0.12); - background-color: #adb5bd; } + background-color: #adb5bd; +} /* Sidebar */ .sidebar { - transition: width .08s linear; + transition: width 0.08s linear; box-shadow: none; - background-color: #444; } + background-color: #444; +} .sidebar ~ main.content:after { - background: #000; } + background: #000; +} .sidebar .sidebar-wrapper { - border-right: 1px solid none; } + border-right: 1px solid none; +} .sidebar .sidebar-wrapper { - border-right: 1px solid rgba(0, 0, 0, 0.125); } + border-right: 1px solid rgba(0, 0, 0, 0.125); +} .sidebar ul.sidebar-elements li > a.sidebar-link { - color: #fff; } + color: #fff; +} .sidebar ul.sidebar-elements li > a.sidebar-link.active { background-color: #595f64; - color: #fff; } + color: #fff; +} .sidebar ul.sidebar-elements li > a.sidebar-link.have-active-child { background-color: #595f64; - color: #fff; } + color: #fff; +} .sidebar ul.sidebar-elements li > a.sidebar-link.have-active-child::after { background-color: var(--cerebrate-color); } .sidebar ul.sidebar-elements li > a.sidebar-link:hover { background-color: #60676c; - color: #fff; } + color: #fff; +} .sidebar.expanded ul.sidebar-elements li > a.sidebar-link.have-active-child, .sidebar:hover ul.sidebar-elements li > a.sidebar-link.have-active-child { - background-color: unset; } + background-color: unset; +} .sidebar.expanded ul.sidebar-elements li > a.sidebar-link.have-active-child:hover, .sidebar:hover ul.sidebar-elements li > a.sidebar-link.have-active-child:hover { - background-color: #60676c; } + background-color: #60676c; +} ul.sidebar-elements li > a.sidebar-link.active::after { - background-color: var(--cerebrate-color); } + background-color: var(--cerebrate-color); +} .lock-sidebar > a.btn { - background-color: unset; } + background-color: unset; +} diff --git a/webroot/css/themes/theme-default.css b/webroot/css/themes/theme-default.css index cdcb24b..7ee279e 100644 --- a/webroot/css/themes/theme-default.css +++ b/webroot/css/themes/theme-default.css @@ -1,323 +1,408 @@ /* Callout */ .callout { border: 1px solid #e9ecef; - border-radius: .25rem; + border-radius: 0.25rem; background-color: #fff; - box-shadow: 0 0 35px 0 rgba(154, 161, 171, 0.25); } + box-shadow: 0 0 35px 0 rgba(154, 161, 171, 0.25); +} .callout-primary { border-left-color: #0d6efd; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-secondary { border-left-color: #6c757d; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-success { border-left-color: #198754; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-info { border-left-color: #0dcaf0; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-warning { border-left-color: #ffc107; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-danger { border-left-color: #dc3545; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-light { border-left-color: #f8f9fa; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-dark { border-left-color: #212529; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} /* Toasts */ .toast { - min-width: 250px; } + min-width: 250px; +} .toast-primary { color: #04214c; background-color: #b6d4fe; - border-color: #9ec5fe; } - .toast-primary strong { - border-top-color: #85b6fe; } + border-color: #9ec5fe; +} +.toast-primary strong { + border-top-color: #85b6fe; +} .toast-secondary { color: #202326; background-color: #d3d6d8; - border-color: #c4c8cb; } - .toast-secondary strong { - border-top-color: #b6bbbf; } + border-color: #c4c8cb; +} +.toast-secondary strong { + border-top-color: #b6bbbf; +} .toast-success { color: #082919; background-color: #badbcc; - border-color: #a3cfbb; } - .toast-success strong { - border-top-color: #92c6af; } + border-color: #a3cfbb; +} +.toast-success strong { + border-top-color: #92c6af; +} .toast-info { color: #043d48; background-color: #b6effb; - border-color: #9eeaf9; } - .toast-info strong { - border-top-color: #86e5f8; } + border-color: #9eeaf9; +} +.toast-info strong { + border-top-color: #86e5f8; +} .toast-warning { color: #4d3a02; background-color: #ffecb5; - border-color: #ffe69c; } - .toast-warning strong { - border-top-color: #ffe083; } + border-color: #ffe69c; +} +.toast-warning strong { + border-top-color: #ffe083; +} .toast-danger { color: #421015; background-color: #f5c2c7; - border-color: #f1aeb5; } - .toast-danger strong { - border-top-color: #ed98a1; } + border-color: #f1aeb5; +} +.toast-danger strong { + border-top-color: #ed98a1; +} .toast-light { color: #4a4b4b; background-color: #fdfdfe; - border-color: #fcfdfd; } - .toast-light strong { - border-top-color: #edf3f3; } + border-color: #fcfdfd; +} +.toast-light strong { + border-top-color: #edf3f3; +} .toast-dark { color: #0a0b0c; background-color: #bcbebf; - border-color: #a6a8a9; } - .toast-dark strong { - border-top-color: #999b9c; } + border-color: #a6a8a9; +} +.toast-dark strong { + border-top-color: #999b9c; +} /* Dropdown-item */ .dropdown-item.dropdown-item-primary { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #0d6efd; } .dropdown-item.dropdown-item-primary:hover { color: #fff; background-color: #0b5ed7; } +======= + background-color: #0d6efd; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-primary:hover { color: #fff; - background-color: #0d6efd; } - + background-color: #0d6efd; +} .dropdown-item.dropdown-item-secondary { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #6c757d; } .dropdown-item.dropdown-item-secondary:hover { color: #fff; background-color: #5c636a; } +======= + background-color: #6c757d; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-secondary:hover { color: #fff; - background-color: #6c757d; } - + background-color: #6c757d; +} .dropdown-item.dropdown-item-success { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #198754; } .dropdown-item.dropdown-item-success:hover { color: #fff; background-color: #157347; } +======= + background-color: #198754; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-success:hover { color: #fff; - background-color: #198754; } - + background-color: #198754; +} .dropdown-item.dropdown-item-info { color: #000; text-decoration: none; +<<<<<<< HEAD background-color: #0dcaf0; } .dropdown-item.dropdown-item-info:hover { color: #000; background-color: #31d2f2; } +======= + background-color: #0dcaf0; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-info:hover { color: #000; - background-color: #0dcaf0; } - + background-color: #0dcaf0; +} .dropdown-item.dropdown-item-warning { color: #000; text-decoration: none; +<<<<<<< HEAD background-color: #ffc107; } .dropdown-item.dropdown-item-warning:hover { color: #000; background-color: #ffca2c; } +======= + background-color: #ffc107; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-warning:hover { color: #000; - background-color: #ffc107; } - + background-color: #ffc107; +} .dropdown-item.dropdown-item-danger { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #dc3545; } .dropdown-item.dropdown-item-danger:hover { color: #fff; background-color: #bb2d3b; } +======= + background-color: #dc3545; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-danger:hover { color: #fff; - background-color: #dc3545; } - + background-color: #dc3545; +} .dropdown-item.dropdown-item-light { color: #000; text-decoration: none; +<<<<<<< HEAD background-color: #f8f9fa; } .dropdown-item.dropdown-item-light:hover { color: #000; background-color: #f9fafb; } +======= + background-color: #f8f9fa; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-light:hover { color: #000; - background-color: #f8f9fa; } - + background-color: #f8f9fa; +} .dropdown-item.dropdown-item-dark { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #212529; } .dropdown-item.dropdown-item-dark:hover { color: #fff; background-color: #1c1f23; } +======= + background-color: #212529; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-dark:hover { color: #fff; - background-color: #212529; } + background-color: #212529; +} /* Progress Timeline */ .progress-timeline { - padding: 0.2em 0.2em 0.5em 0.2em; } - .progress-timeline ul { - position: relative; - padding: 0; } - .progress-timeline li { - list-style-type: none; - position: relative; } - .progress-timeline li.progress-inactive { - opacity: 0.5; } - .progress-timeline .progress-line { - height: 2px; } - .progress-timeline .progress-line.progress-inactive { - opacity: 0.5; } + padding: 0.2em 0.2em 0.5em 0.2em; +} +.progress-timeline ul { + position: relative; + padding: 0; +} +.progress-timeline li { + list-style-type: none; + position: relative; +} +.progress-timeline li.progress-inactive { + opacity: 0.5; +} +.progress-timeline .progress-line { + height: 2px; +} +.progress-timeline .progress-line.progress-inactive { + opacity: 0.5; +} /* Forms severity */ .form-control.is-invalid.info { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%230dcaf0' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%230dcaf0' stroke='none'/%3e%3c/svg%3e"); } - .form-control.is-invalid.info:focus { - border-color: #0dcaf0; - box-shadow: 0 0 0 0.25rem rgba(13, 202, 240, 0.25); } - + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%230dcaf0' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%230dcaf0' stroke='none'/%3e%3c/svg%3e"); +} +.form-control.is-invalid.info:focus { + border-color: #0dcaf0; + box-shadow: 0 0 0 0.25rem rgba(13, 202, 240, 0.25); +} .form-control.is-invalid.warning { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23ffc107' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23ffc107' stroke='none'/%3e%3c/svg%3e"); } - .form-control.is-invalid.warning:focus { - border-color: #ffc107; - box-shadow: 0 0 0 0.25rem rgba(255, 193, 7, 0.25); } + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23ffc107' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23ffc107' stroke='none'/%3e%3c/svg%3e"); +} +.form-control.is-invalid.warning:focus { + border-color: #ffc107; + box-shadow: 0 0 0 0.25rem rgba(255, 193, 7, 0.25); +} .form-select.is-invalid:not([multiple]):not([size]).info, -.form-select.is-invalid:not([multiple])[size="1"] -.form-select.is-invalid.info { +.form-select.is-invalid:not([multiple])[size="1"] .form-select.is-invalid.info { background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%230dcaf0'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%230dcaf0' stroke='none'/%3e%3c/svg%3e"); - background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); } - .form-select.is-invalid:not([multiple]):not([size]).info:focus, - .form-select.is-invalid:not([multiple])[size="1"] -.form-select.is-invalid.info:focus { - box-shadow: 0 0 0 0.25rem rgba(13, 202, 240, 0.25); } - + background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} +.form-select.is-invalid:not([multiple]):not([size]).info:focus, +.form-select.is-invalid:not([multiple])[size="1"] .form-select.is-invalid.info:focus { + box-shadow: 0 0 0 0.25rem rgba(13, 202, 240, 0.25); +} .form-select.is-invalid:not([multiple]):not([size]).warning, -.form-select.is-invalid:not([multiple])[size="1"] -.form-select.is-invalid.warning { +.form-select.is-invalid:not([multiple])[size="1"] .form-select.is-invalid.warning { background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23ffc107'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23ffc107' stroke='none'/%3e%3c/svg%3e"); - background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); } - .form-select.is-invalid:not([multiple]):not([size]).warning:focus, - .form-select.is-invalid:not([multiple])[size="1"] -.form-select.is-invalid.warning:focus { - box-shadow: 0 0 0 0.25rem rgba(255, 193, 7, 0.25); } + background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} +.form-select.is-invalid:not([multiple]):not([size]).warning:focus, +.form-select.is-invalid:not([multiple])[size="1"] .form-select.is-invalid.warning:focus { + box-shadow: 0 0 0 0.25rem rgba(255, 193, 7, 0.25); +} .form-check-input.is-invalid.info { - border-color: #0dcaf0; } - + border-color: #0dcaf0; +} .form-check-input.is-invalid.info:checked { - background-color: #0dcaf0; } - + background-color: #0dcaf0; +} .form-check-input.is-invalid.info ~ .form-check-label { - color: unset; } - + color: unset; +} .form-check-input.is-invalid.info:focus { - box-shadow: 0 0 0 0.2rem rgba(13, 202, 240, 0.25); } - + box-shadow: 0 0 0 0.2rem rgba(13, 202, 240, 0.25); +} .form-check-input.is-invalid.warning { - border-color: #ffc107; } - + border-color: #ffc107; +} .form-check-input.is-invalid.warning:checked { - background-color: #ffc107; } - + background-color: #ffc107; +} .form-check-input.is-invalid.warning ~ .form-check-label { - color: unset; } - + color: unset; +} .form-check-input.is-invalid.warning:focus { - box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.25); } + box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.25); +} /* Utilities */ .mw-75 { - max-width: 75% !important; } + max-width: 75% !important; +} .mw-66 { max-width: 66% !important; } .mw-50 { - max-width: 50% !important; } + max-width: 50% !important; +} .mw-33 { max-width: 33% !important; } .mw-25 { - max-width: 25% !important; } + max-width: 25% !important; +} .mh-75 { - max-height: 75% !important; } + max-height: 75% !important; +} .mh-66 { max-height: 66% !important; } .mh-50 { - max-height: 50% !important; } + max-height: 50% !important; +} .mh-33 { max-height: 33% !important; } .mh-25 { - max-height: 25% !important; } + max-height: 25% !important; +} .p-abs-center-y { top: 50%; - transform: translateY(-50%); } + transform: translateY(-50%); +} .p-abs-center-x { left: 50%; - transform: translateX(-50%); } + transform: translateX(-50%); +} .p-abs-center-both { top: 50%; left: 50%; - transform: translateX(-50%) translateY(-50%); } + transform: translateX(-50%) translateY(-50%); +} .fs-7 { font-size: .875rem !important; } @@ -380,90 +465,114 @@ body { /* background by SVGBackgrounds.com */ background-attachment: fixed; background-size: cover; - background-blend-mode: normal; } + background-blend-mode: normal; +} .panel { background-color: #fff; border: none; - box-shadow: 0 0 35px 0 rgba(154, 161, 171, 0.25); } + box-shadow: 0 0 35px 0 rgba(154, 161, 171, 0.25); +} .loading-overlay { background-color: #f8f9fa; - opacity: 0.75; } + opacity: 0.75; +} /* Top navbar */ .top-navbar { - background-color: #212529; } + background-color: #212529; +} .center-navbar nav.header-breadcrumb { - color: #fff; } + color: #fff; +} header.top-navbar .header-menu > a:hover, header.top-navbar .header-breadcrumb .header-breadcrumb-item > a:hover { - color: #d6d6d6 !important; } + color: #d6d6d6 !important; +} .top-navbar .center-navbar nav.header-breadcrumb li.header-breadcrumb-item a { - color: #fff; } + color: #fff; +} .top-navbar .right-navbar .header-menu a.nav-link { - color: #fff; } + color: #fff; +} .top-navbar .left-navbar .navbar-brand img { - filter: invert(1); } + filter: invert(1); +} .top-navbar .left-navbar .navbar-brand:hover img { - filter: invert(1) drop-shadow(0px 0px 3px #fff); } + filter: invert(1) drop-shadow(0px 0px 3px #fff); +} .top-navbar .composed-app-icon-container > .app-icon { - background-color: #fff; } + background-color: #fff; +} .breadcrumb-link-container { box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 2px 6px 0 rgba(0, 0, 0, 0.12); - background-color: #f8f9fa; } + background-color: #f8f9fa; +} /* Sidebar */ .sidebar { - transition: width .08s linear; + transition: width 0.08s linear; box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 2px 6px 0 rgba(0, 0, 0, 0.12); - background-color: #f8f9fa; } + background-color: #f8f9fa; +} .sidebar ~ main.content:after { - background: #000; } + background: #000; +} .sidebar .sidebar-wrapper { - border-right: 1px solid #ddd; } + border-right: 1px solid #ddd; +} .sidebar .sidebar-wrapper { - border-right: 1px solid rgba(0, 0, 0, 0.125); } + border-right: 1px solid rgba(0, 0, 0, 0.125); +} .sidebar ul.sidebar-elements li > a.sidebar-link { - color: #000; } + color: #000; +} .sidebar ul.sidebar-elements li > a.sidebar-link.active { background-color: #dbdbdb; - color: #000; } + color: #000; +} .sidebar ul.sidebar-elements li > a.sidebar-link.have-active-child { background-color: #dbdbdb; - color: #000; } + color: #000; +} .sidebar ul.sidebar-elements li > a.sidebar-link.have-active-child::after { background-color: var(--cerebrate-color); } .sidebar ul.sidebar-elements li > a.sidebar-link:hover { background-color: #ebebeb; - color: #000; } + color: #000; +} .sidebar.expanded ul.sidebar-elements li > a.sidebar-link.have-active-child, .sidebar:hover ul.sidebar-elements li > a.sidebar-link.have-active-child { - background-color: unset; } + background-color: unset; +} .sidebar.expanded ul.sidebar-elements li > a.sidebar-link.have-active-child:hover, .sidebar:hover ul.sidebar-elements li > a.sidebar-link.have-active-child:hover { - background-color: #ebebeb; } + background-color: #ebebeb; +} ul.sidebar-elements li > a.sidebar-link.active::after { - background-color: var(--cerebrate-color); } + background-color: var(--cerebrate-color); +} .lock-sidebar > a.btn { - background-color: #f8f9fa; } + background-color: #f8f9fa; +} diff --git a/webroot/css/themes/theme-flatly.css b/webroot/css/themes/theme-flatly.css index 25fe7ea..3bd3ec9 100644 --- a/webroot/css/themes/theme-flatly.css +++ b/webroot/css/themes/theme-flatly.css @@ -1,323 +1,408 @@ /* Callout */ .callout { border: 1px solid #ecf0f1; - border-radius: .25rem; + border-radius: 0.25rem; background-color: #fff; - box-shadow: 0 0 35px 0 rgba(154, 161, 171, 0.25); } + box-shadow: 0 0 35px 0 rgba(154, 161, 171, 0.25); +} .callout-primary { border-left-color: #2c3e50; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-secondary { border-left-color: #95a5a6; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-success { border-left-color: #18bc9c; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-info { border-left-color: #3498db; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-warning { border-left-color: #f39c12; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-danger { border-left-color: #e74c3c; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-light { border-left-color: #ecf0f1; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-dark { border-left-color: #7b8a8b; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} /* Toasts */ .toast { - min-width: 250px; } + min-width: 250px; +} .toast-primary { color: #0d1318; background-color: #c0c5cb; - border-color: #abb2b9; } - .toast-primary strong { - border-top-color: #9da5ad; } + border-color: #abb2b9; +} +.toast-primary strong { + border-top-color: #9da5ad; +} .toast-secondary { color: #2d3232; background-color: #dfe4e4; - border-color: #d5dbdb; } - .toast-secondary strong { - border-top-color: #c7cfcf; } + border-color: #d5dbdb; +} +.toast-secondary strong { + border-top-color: #c7cfcf; +} .toast-success { color: #07382f; background-color: #baebe1; - border-color: #a3e4d7; } - .toast-success strong { - border-top-color: #8fdece; } + border-color: #a3e4d7; +} +.toast-success strong { + border-top-color: #8fdece; +} .toast-info { color: #102e42; background-color: #c2e0f4; - border-color: #aed6f1; } - .toast-info strong { - border-top-color: #98cbed; } + border-color: #aed6f1; +} +.toast-info strong { + border-top-color: #98cbed; +} .toast-warning { color: #492f05; background-color: #fbe1b8; - border-color: #fad7a0; } - .toast-warning strong { - border-top-color: #f9cd88; } + border-color: #fad7a0; +} +.toast-warning strong { + border-top-color: #f9cd88; +} .toast-danger { color: #451712; background-color: #f8c9c5; - border-color: #f5b7b1; } - .toast-danger strong { - border-top-color: #f2a29a; } + border-color: #f5b7b1; +} +.toast-danger strong { + border-top-color: #f2a29a; +} .toast-light { color: #474848; background-color: #f9fbfb; - border-color: #f7f9f9; } - .toast-light strong { - border-top-color: #e8eeee; } + border-color: #f7f9f9; +} +.toast-light strong { + border-top-color: #e8eeee; +} .toast-dark { color: #25292a; background-color: #d7dcdc; - border-color: #cad0d1; } - .toast-dark strong { - border-top-color: #bcc4c5; } + border-color: #cad0d1; +} +.toast-dark strong { + border-top-color: #bcc4c5; +} /* Dropdown-item */ .dropdown-item.dropdown-item-primary { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #2c3e50; } .dropdown-item.dropdown-item-primary:hover { color: #fff; background-color: #253544; } +======= + background-color: #2c3e50; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-primary:hover { color: #fff; - background-color: #2c3e50; } - + background-color: #2c3e50; +} .dropdown-item.dropdown-item-secondary { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #95a5a6; } .dropdown-item.dropdown-item-secondary:hover { color: #fff; background-color: #7f8c8d; } +======= + background-color: #95a5a6; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-secondary:hover { color: #fff; - background-color: #95a5a6; } - + background-color: #95a5a6; +} .dropdown-item.dropdown-item-success { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #18bc9c; } .dropdown-item.dropdown-item-success:hover { color: #fff; background-color: #14a085; } +======= + background-color: #18bc9c; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-success:hover { color: #fff; - background-color: #18bc9c; } - + background-color: #18bc9c; +} .dropdown-item.dropdown-item-info { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #3498db; } .dropdown-item.dropdown-item-info:hover { color: #fff; background-color: #2c81ba; } +======= + background-color: #3498db; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-info:hover { color: #fff; - background-color: #3498db; } - + background-color: #3498db; +} .dropdown-item.dropdown-item-warning { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #f39c12; } .dropdown-item.dropdown-item-warning:hover { color: #fff; background-color: #cf850f; } +======= + background-color: #f39c12; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-warning:hover { color: #fff; - background-color: #f39c12; } - + background-color: #f39c12; +} .dropdown-item.dropdown-item-danger { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #e74c3c; } .dropdown-item.dropdown-item-danger:hover { color: #fff; background-color: #c44133; } +======= + background-color: #e74c3c; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-danger:hover { color: #fff; - background-color: #e74c3c; } - + background-color: #e74c3c; +} .dropdown-item.dropdown-item-light { color: #000; text-decoration: none; +<<<<<<< HEAD background-color: #ecf0f1; } .dropdown-item.dropdown-item-light:hover { color: #000; background-color: #eff2f3; } +======= + background-color: #ecf0f1; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-light:hover { color: #000; - background-color: #ecf0f1; } - + background-color: #ecf0f1; +} .dropdown-item.dropdown-item-dark { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #7b8a8b; } .dropdown-item.dropdown-item-dark:hover { color: #fff; background-color: #697576; } +======= + background-color: #7b8a8b; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-dark:hover { color: #fff; - background-color: #7b8a8b; } + background-color: #7b8a8b; +} /* Progress Timeline */ .progress-timeline { - padding: 0.2em 0.2em 0.5em 0.2em; } - .progress-timeline ul { - position: relative; - padding: 0; } - .progress-timeline li { - list-style-type: none; - position: relative; } - .progress-timeline li.progress-inactive { - opacity: 0.5; } - .progress-timeline .progress-line { - height: 2px; } - .progress-timeline .progress-line.progress-inactive { - opacity: 0.5; } + padding: 0.2em 0.2em 0.5em 0.2em; +} +.progress-timeline ul { + position: relative; + padding: 0; +} +.progress-timeline li { + list-style-type: none; + position: relative; +} +.progress-timeline li.progress-inactive { + opacity: 0.5; +} +.progress-timeline .progress-line { + height: 2px; +} +.progress-timeline .progress-line.progress-inactive { + opacity: 0.5; +} /* Forms severity */ .form-control.is-invalid.info { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%233498db' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%233498db' stroke='none'/%3e%3c/svg%3e"); } - .form-control.is-invalid.info:focus { - border-color: #3498db; - box-shadow: 0 0 0 0.25rem rgba(52, 152, 219, 0.25); } - + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%233498db' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%233498db' stroke='none'/%3e%3c/svg%3e"); +} +.form-control.is-invalid.info:focus { + border-color: #3498db; + box-shadow: 0 0 0 0.25rem rgba(52, 152, 219, 0.25); +} .form-control.is-invalid.warning { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23f39c12' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23f39c12' stroke='none'/%3e%3c/svg%3e"); } - .form-control.is-invalid.warning:focus { - border-color: #f39c12; - box-shadow: 0 0 0 0.25rem rgba(243, 156, 18, 0.25); } + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23f39c12' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23f39c12' stroke='none'/%3e%3c/svg%3e"); +} +.form-control.is-invalid.warning:focus { + border-color: #f39c12; + box-shadow: 0 0 0 0.25rem rgba(243, 156, 18, 0.25); +} .form-select.is-invalid:not([multiple]):not([size]).info, -.form-select.is-invalid:not([multiple])[size="1"] -.form-select.is-invalid.info { +.form-select.is-invalid:not([multiple])[size="1"] .form-select.is-invalid.info { background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%233498db'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%233498db' stroke='none'/%3e%3c/svg%3e"); - background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); } - .form-select.is-invalid:not([multiple]):not([size]).info:focus, - .form-select.is-invalid:not([multiple])[size="1"] -.form-select.is-invalid.info:focus { - box-shadow: 0 0 0 0.25rem rgba(52, 152, 219, 0.25); } - + background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} +.form-select.is-invalid:not([multiple]):not([size]).info:focus, +.form-select.is-invalid:not([multiple])[size="1"] .form-select.is-invalid.info:focus { + box-shadow: 0 0 0 0.25rem rgba(52, 152, 219, 0.25); +} .form-select.is-invalid:not([multiple]):not([size]).warning, -.form-select.is-invalid:not([multiple])[size="1"] -.form-select.is-invalid.warning { +.form-select.is-invalid:not([multiple])[size="1"] .form-select.is-invalid.warning { background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23f39c12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23f39c12' stroke='none'/%3e%3c/svg%3e"); - background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); } - .form-select.is-invalid:not([multiple]):not([size]).warning:focus, - .form-select.is-invalid:not([multiple])[size="1"] -.form-select.is-invalid.warning:focus { - box-shadow: 0 0 0 0.25rem rgba(243, 156, 18, 0.25); } + background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} +.form-select.is-invalid:not([multiple]):not([size]).warning:focus, +.form-select.is-invalid:not([multiple])[size="1"] .form-select.is-invalid.warning:focus { + box-shadow: 0 0 0 0.25rem rgba(243, 156, 18, 0.25); +} .form-check-input.is-invalid.info { - border-color: #3498db; } - + border-color: #3498db; +} .form-check-input.is-invalid.info:checked { - background-color: #3498db; } - + background-color: #3498db; +} .form-check-input.is-invalid.info ~ .form-check-label { - color: unset; } - + color: unset; +} .form-check-input.is-invalid.info:focus { - box-shadow: 0 0 0 0.2rem rgba(52, 152, 219, 0.25); } - + box-shadow: 0 0 0 0.2rem rgba(52, 152, 219, 0.25); +} .form-check-input.is-invalid.warning { - border-color: #f39c12; } - + border-color: #f39c12; +} .form-check-input.is-invalid.warning:checked { - background-color: #f39c12; } - + background-color: #f39c12; +} .form-check-input.is-invalid.warning ~ .form-check-label { - color: unset; } - + color: unset; +} .form-check-input.is-invalid.warning:focus { - box-shadow: 0 0 0 0.2rem rgba(243, 156, 18, 0.25); } + box-shadow: 0 0 0 0.2rem rgba(243, 156, 18, 0.25); +} /* Utilities */ .mw-75 { - max-width: 75% !important; } + max-width: 75% !important; +} .mw-66 { max-width: 66% !important; } .mw-50 { - max-width: 50% !important; } + max-width: 50% !important; +} .mw-33 { max-width: 33% !important; } .mw-25 { - max-width: 25% !important; } + max-width: 25% !important; +} .mh-75 { - max-height: 75% !important; } + max-height: 75% !important; +} .mh-66 { max-height: 66% !important; } .mh-50 { - max-height: 50% !important; } + max-height: 50% !important; +} .mh-33 { max-height: 33% !important; } .mh-25 { - max-height: 25% !important; } + max-height: 25% !important; +} .p-abs-center-y { top: 50%; - transform: translateY(-50%); } + transform: translateY(-50%); +} .p-abs-center-x { left: 50%; - transform: translateX(-50%); } + transform: translateX(-50%); +} .p-abs-center-both { top: 50%; left: 50%; - transform: translateX(-50%) translateY(-50%); } + transform: translateX(-50%) translateY(-50%); +} .fs-7 { font-size: .875rem !important; } @@ -380,90 +465,114 @@ body { /* background by SVGBackgrounds.com */ background-attachment: fixed; background-size: cover; - background-blend-mode: normal; } + background-blend-mode: normal; +} .panel { background-color: #fff; border: none; - box-shadow: 0 0 35px 0 rgba(154, 161, 171, 0.25); } + box-shadow: 0 0 35px 0 rgba(154, 161, 171, 0.25); +} .loading-overlay { background-color: #ecf0f1; - opacity: 0.75; } + opacity: 0.75; +} /* Top navbar */ .top-navbar { - background-color: #2c3e50; } + background-color: #2c3e50; +} .center-navbar nav.header-breadcrumb { - color: #fff; } + color: #fff; +} header.top-navbar .header-menu > a:hover, header.top-navbar .header-breadcrumb .header-breadcrumb-item > a:hover { - color: #d6d6d6 !important; } + color: #d6d6d6 !important; +} .top-navbar .center-navbar nav.header-breadcrumb li.header-breadcrumb-item a { - color: #fff; } + color: #fff; +} .top-navbar .right-navbar .header-menu a.nav-link { - color: #fff; } + color: #fff; +} .top-navbar .left-navbar .navbar-brand img { - filter: invert(1); } + filter: invert(1); +} .top-navbar .left-navbar .navbar-brand:hover img { - filter: invert(1) drop-shadow(0px 0px 3px #fff); } + filter: invert(1) drop-shadow(0px 0px 3px #fff); +} .top-navbar .composed-app-icon-container > .app-icon { - background-color: #fff; } + background-color: #fff; +} .breadcrumb-link-container { box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 2px 6px 0 rgba(0, 0, 0, 0.12); - background-color: #ecf0f1; } + background-color: #ecf0f1; +} /* Sidebar */ .sidebar { - transition: width .08s linear; + transition: width 0.08s linear; box-shadow: none; - background-color: #ecf0f1; } + background-color: #ecf0f1; +} .sidebar ~ main.content:after { - background: #000; } + background: #000; +} .sidebar .sidebar-wrapper { - border-right: 1px solid none; } + border-right: 1px solid none; +} .sidebar .sidebar-wrapper { - border-right: 1px solid rgba(0, 0, 0, 0.125); } + border-right: 1px solid rgba(0, 0, 0, 0.125); +} .sidebar ul.sidebar-elements li > a.sidebar-link { - color: #000; } + color: #000; +} .sidebar ul.sidebar-elements li > a.sidebar-link.active { background-color: #dbdbdb; - color: #18bc9c; } + color: #18bc9c; +} .sidebar ul.sidebar-elements li > a.sidebar-link.have-active-child { background-color: #dbdbdb; - color: #18bc9c; } + color: #18bc9c; +} .sidebar ul.sidebar-elements li > a.sidebar-link.have-active-child::after { background-color: #18bc9c; } .sidebar ul.sidebar-elements li > a.sidebar-link:hover { background-color: #ebebeb; - color: #18bc9c; } + color: #18bc9c; +} .sidebar.expanded ul.sidebar-elements li > a.sidebar-link.have-active-child, .sidebar:hover ul.sidebar-elements li > a.sidebar-link.have-active-child { - background-color: unset; } + background-color: unset; +} .sidebar.expanded ul.sidebar-elements li > a.sidebar-link.have-active-child:hover, .sidebar:hover ul.sidebar-elements li > a.sidebar-link.have-active-child:hover { - background-color: #ebebeb; } + background-color: #ebebeb; +} ul.sidebar-elements li > a.sidebar-link.active::after { - background-color: #18bc9c; } + background-color: #18bc9c; +} .lock-sidebar > a.btn { - background-color: unset; } + background-color: unset; +} diff --git a/webroot/css/themes/theme-minty.css b/webroot/css/themes/theme-minty.css index cdeae1a..512ab02 100644 --- a/webroot/css/themes/theme-minty.css +++ b/webroot/css/themes/theme-minty.css @@ -1,323 +1,408 @@ /* Callout */ .callout { border: 1px solid #ecf0f1; - border-radius: .25rem; + border-radius: 0.25rem; background-color: #fff; - box-shadow: 0 0 35px 0 rgba(154, 161, 171, 0.25); } + box-shadow: 0 0 35px 0 rgba(154, 161, 171, 0.25); +} .callout-primary { border-left-color: #2c3e50; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-secondary { border-left-color: #95a5a6; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-success { border-left-color: #18bc9c; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-info { border-left-color: #3498db; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-warning { border-left-color: #f39c12; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-danger { border-left-color: #e74c3c; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-light { border-left-color: #ecf0f1; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-dark { border-left-color: #7b8a8b; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} /* Toasts */ .toast { - min-width: 250px; } + min-width: 250px; +} .toast-primary { color: #0d1318; background-color: #c0c5cb; - border-color: #abb2b9; } - .toast-primary strong { - border-top-color: #9da5ad; } + border-color: #abb2b9; +} +.toast-primary strong { + border-top-color: #9da5ad; +} .toast-secondary { color: #2d3232; background-color: #dfe4e4; - border-color: #d5dbdb; } - .toast-secondary strong { - border-top-color: #c7cfcf; } + border-color: #d5dbdb; +} +.toast-secondary strong { + border-top-color: #c7cfcf; +} .toast-success { color: #07382f; background-color: #baebe1; - border-color: #a3e4d7; } - .toast-success strong { - border-top-color: #8fdece; } + border-color: #a3e4d7; +} +.toast-success strong { + border-top-color: #8fdece; +} .toast-info { color: #102e42; background-color: #c2e0f4; - border-color: #aed6f1; } - .toast-info strong { - border-top-color: #98cbed; } + border-color: #aed6f1; +} +.toast-info strong { + border-top-color: #98cbed; +} .toast-warning { color: #492f05; background-color: #fbe1b8; - border-color: #fad7a0; } - .toast-warning strong { - border-top-color: #f9cd88; } + border-color: #fad7a0; +} +.toast-warning strong { + border-top-color: #f9cd88; +} .toast-danger { color: #451712; background-color: #f8c9c5; - border-color: #f5b7b1; } - .toast-danger strong { - border-top-color: #f2a29a; } + border-color: #f5b7b1; +} +.toast-danger strong { + border-top-color: #f2a29a; +} .toast-light { color: #474848; background-color: #f9fbfb; - border-color: #f7f9f9; } - .toast-light strong { - border-top-color: #e8eeee; } + border-color: #f7f9f9; +} +.toast-light strong { + border-top-color: #e8eeee; +} .toast-dark { color: #25292a; background-color: #d7dcdc; - border-color: #cad0d1; } - .toast-dark strong { - border-top-color: #bcc4c5; } + border-color: #cad0d1; +} +.toast-dark strong { + border-top-color: #bcc4c5; +} /* Dropdown-item */ .dropdown-item.dropdown-item-primary { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #2c3e50; } .dropdown-item.dropdown-item-primary:hover { color: #fff; background-color: #253544; } +======= + background-color: #2c3e50; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-primary:hover { color: #fff; - background-color: #2c3e50; } - + background-color: #2c3e50; +} .dropdown-item.dropdown-item-secondary { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #95a5a6; } .dropdown-item.dropdown-item-secondary:hover { color: #fff; background-color: #7f8c8d; } +======= + background-color: #95a5a6; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-secondary:hover { color: #fff; - background-color: #95a5a6; } - + background-color: #95a5a6; +} .dropdown-item.dropdown-item-success { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #18bc9c; } .dropdown-item.dropdown-item-success:hover { color: #fff; background-color: #14a085; } +======= + background-color: #18bc9c; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-success:hover { color: #fff; - background-color: #18bc9c; } - + background-color: #18bc9c; +} .dropdown-item.dropdown-item-info { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #3498db; } .dropdown-item.dropdown-item-info:hover { color: #fff; background-color: #2c81ba; } +======= + background-color: #3498db; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-info:hover { color: #fff; - background-color: #3498db; } - + background-color: #3498db; +} .dropdown-item.dropdown-item-warning { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #f39c12; } .dropdown-item.dropdown-item-warning:hover { color: #fff; background-color: #cf850f; } +======= + background-color: #f39c12; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-warning:hover { color: #fff; - background-color: #f39c12; } - + background-color: #f39c12; +} .dropdown-item.dropdown-item-danger { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #e74c3c; } .dropdown-item.dropdown-item-danger:hover { color: #fff; background-color: #c44133; } +======= + background-color: #e74c3c; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-danger:hover { color: #fff; - background-color: #e74c3c; } - + background-color: #e74c3c; +} .dropdown-item.dropdown-item-light { color: #000; text-decoration: none; +<<<<<<< HEAD background-color: #ecf0f1; } .dropdown-item.dropdown-item-light:hover { color: #000; background-color: #eff2f3; } +======= + background-color: #ecf0f1; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-light:hover { color: #000; - background-color: #ecf0f1; } - + background-color: #ecf0f1; +} .dropdown-item.dropdown-item-dark { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #7b8a8b; } .dropdown-item.dropdown-item-dark:hover { color: #fff; background-color: #697576; } +======= + background-color: #7b8a8b; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-dark:hover { color: #fff; - background-color: #7b8a8b; } + background-color: #7b8a8b; +} /* Progress Timeline */ .progress-timeline { - padding: 0.2em 0.2em 0.5em 0.2em; } - .progress-timeline ul { - position: relative; - padding: 0; } - .progress-timeline li { - list-style-type: none; - position: relative; } - .progress-timeline li.progress-inactive { - opacity: 0.5; } - .progress-timeline .progress-line { - height: 2px; } - .progress-timeline .progress-line.progress-inactive { - opacity: 0.5; } + padding: 0.2em 0.2em 0.5em 0.2em; +} +.progress-timeline ul { + position: relative; + padding: 0; +} +.progress-timeline li { + list-style-type: none; + position: relative; +} +.progress-timeline li.progress-inactive { + opacity: 0.5; +} +.progress-timeline .progress-line { + height: 2px; +} +.progress-timeline .progress-line.progress-inactive { + opacity: 0.5; +} /* Forms severity */ .form-control.is-invalid.info { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%233498db' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%233498db' stroke='none'/%3e%3c/svg%3e"); } - .form-control.is-invalid.info:focus { - border-color: #3498db; - box-shadow: 0 0 0 0.25rem rgba(52, 152, 219, 0.25); } - + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%233498db' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%233498db' stroke='none'/%3e%3c/svg%3e"); +} +.form-control.is-invalid.info:focus { + border-color: #3498db; + box-shadow: 0 0 0 0.25rem rgba(52, 152, 219, 0.25); +} .form-control.is-invalid.warning { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23f39c12' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23f39c12' stroke='none'/%3e%3c/svg%3e"); } - .form-control.is-invalid.warning:focus { - border-color: #f39c12; - box-shadow: 0 0 0 0.25rem rgba(243, 156, 18, 0.25); } + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23f39c12' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23f39c12' stroke='none'/%3e%3c/svg%3e"); +} +.form-control.is-invalid.warning:focus { + border-color: #f39c12; + box-shadow: 0 0 0 0.25rem rgba(243, 156, 18, 0.25); +} .form-select.is-invalid:not([multiple]):not([size]).info, -.form-select.is-invalid:not([multiple])[size="1"] -.form-select.is-invalid.info { +.form-select.is-invalid:not([multiple])[size="1"] .form-select.is-invalid.info { background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%233498db'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%233498db' stroke='none'/%3e%3c/svg%3e"); - background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); } - .form-select.is-invalid:not([multiple]):not([size]).info:focus, - .form-select.is-invalid:not([multiple])[size="1"] -.form-select.is-invalid.info:focus { - box-shadow: 0 0 0 0.25rem rgba(52, 152, 219, 0.25); } - + background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} +.form-select.is-invalid:not([multiple]):not([size]).info:focus, +.form-select.is-invalid:not([multiple])[size="1"] .form-select.is-invalid.info:focus { + box-shadow: 0 0 0 0.25rem rgba(52, 152, 219, 0.25); +} .form-select.is-invalid:not([multiple]):not([size]).warning, -.form-select.is-invalid:not([multiple])[size="1"] -.form-select.is-invalid.warning { +.form-select.is-invalid:not([multiple])[size="1"] .form-select.is-invalid.warning { background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23f39c12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23f39c12' stroke='none'/%3e%3c/svg%3e"); - background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); } - .form-select.is-invalid:not([multiple]):not([size]).warning:focus, - .form-select.is-invalid:not([multiple])[size="1"] -.form-select.is-invalid.warning:focus { - box-shadow: 0 0 0 0.25rem rgba(243, 156, 18, 0.25); } + background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} +.form-select.is-invalid:not([multiple]):not([size]).warning:focus, +.form-select.is-invalid:not([multiple])[size="1"] .form-select.is-invalid.warning:focus { + box-shadow: 0 0 0 0.25rem rgba(243, 156, 18, 0.25); +} .form-check-input.is-invalid.info { - border-color: #3498db; } - + border-color: #3498db; +} .form-check-input.is-invalid.info:checked { - background-color: #3498db; } - + background-color: #3498db; +} .form-check-input.is-invalid.info ~ .form-check-label { - color: unset; } - + color: unset; +} .form-check-input.is-invalid.info:focus { - box-shadow: 0 0 0 0.2rem rgba(52, 152, 219, 0.25); } - + box-shadow: 0 0 0 0.2rem rgba(52, 152, 219, 0.25); +} .form-check-input.is-invalid.warning { - border-color: #f39c12; } - + border-color: #f39c12; +} .form-check-input.is-invalid.warning:checked { - background-color: #f39c12; } - + background-color: #f39c12; +} .form-check-input.is-invalid.warning ~ .form-check-label { - color: unset; } - + color: unset; +} .form-check-input.is-invalid.warning:focus { - box-shadow: 0 0 0 0.2rem rgba(243, 156, 18, 0.25); } + box-shadow: 0 0 0 0.2rem rgba(243, 156, 18, 0.25); +} /* Utilities */ .mw-75 { - max-width: 75% !important; } + max-width: 75% !important; +} .mw-66 { max-width: 66% !important; } .mw-50 { - max-width: 50% !important; } + max-width: 50% !important; +} .mw-33 { max-width: 33% !important; } .mw-25 { - max-width: 25% !important; } + max-width: 25% !important; +} .mh-75 { - max-height: 75% !important; } + max-height: 75% !important; +} .mh-66 { max-height: 66% !important; } .mh-50 { - max-height: 50% !important; } + max-height: 50% !important; +} .mh-33 { max-height: 33% !important; } .mh-25 { - max-height: 25% !important; } + max-height: 25% !important; +} .p-abs-center-y { top: 50%; - transform: translateY(-50%); } + transform: translateY(-50%); +} .p-abs-center-x { left: 50%; - transform: translateX(-50%); } + transform: translateX(-50%); +} .p-abs-center-both { top: 50%; left: 50%; - transform: translateX(-50%) translateY(-50%); } + transform: translateX(-50%) translateY(-50%); +} .fs-7 { font-size: .875rem !important; } @@ -380,90 +465,114 @@ body { /* background by SVGBackgrounds.com */ background-attachment: fixed; background-size: cover; - background-blend-mode: normal; } + background-blend-mode: normal; +} .panel { background-color: #fff; border: none; - box-shadow: 0 0 35px 0 rgba(154, 161, 171, 0.25); } + box-shadow: 0 0 35px 0 rgba(154, 161, 171, 0.25); +} .loading-overlay { background-color: #ecf0f1; - opacity: 0.65; } + opacity: 0.65; +} /* Top navbar */ .top-navbar { - background-color: #2c3e50; } + background-color: #2c3e50; +} .center-navbar nav.header-breadcrumb { - color: #fff; } + color: #fff; +} header.top-navbar .header-menu > a:hover, header.top-navbar .header-breadcrumb .header-breadcrumb-item > a:hover { - color: #d6d6d6 !important; } + color: #d6d6d6 !important; +} .top-navbar .center-navbar nav.header-breadcrumb li.header-breadcrumb-item a { - color: #fff; } + color: #fff; +} .top-navbar .right-navbar .header-menu a.nav-link { - color: #fff; } + color: #fff; +} .top-navbar .left-navbar .navbar-brand img { - filter: invert(1); } + filter: invert(1); +} .top-navbar .left-navbar .navbar-brand:hover img { - filter: invert(1) drop-shadow(0px 0px 3px #fff); } + filter: invert(1) drop-shadow(0px 0px 3px #fff); +} .top-navbar .composed-app-icon-container > .app-icon { - background-color: #fff; } + background-color: #fff; +} .breadcrumb-link-container { box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 2px 6px 0 rgba(0, 0, 0, 0.12); - background-color: #ecf0f1; } + background-color: #ecf0f1; +} /* Sidebar */ .sidebar { - transition: width .08s linear; + transition: width 0.08s linear; box-shadow: none; - background-color: #ecf0f1; } + background-color: #ecf0f1; +} .sidebar ~ main.content:after { - background: #000; } + background: #000; +} .sidebar .sidebar-wrapper { - border-right: 1px solid none; } + border-right: 1px solid none; +} .sidebar .sidebar-wrapper { - border-right: 1px solid rgba(0, 0, 0, 0.125); } + border-right: 1px solid rgba(0, 0, 0, 0.125); +} .sidebar ul.sidebar-elements li > a.sidebar-link { - color: #343a40; } + color: #343a40; +} .sidebar ul.sidebar-elements li > a.sidebar-link.active { background-color: #dbdbdb; - color: #18bc9c; } + color: #18bc9c; +} .sidebar ul.sidebar-elements li > a.sidebar-link.have-active-child { background-color: #dbdbdb; - color: #18bc9c; } + color: #18bc9c; +} .sidebar ul.sidebar-elements li > a.sidebar-link.have-active-child::after { background-color: #18bc9c; } .sidebar ul.sidebar-elements li > a.sidebar-link:hover { background-color: #ebebeb; - color: #18bc9c; } + color: #18bc9c; +} .sidebar.expanded ul.sidebar-elements li > a.sidebar-link.have-active-child, .sidebar:hover ul.sidebar-elements li > a.sidebar-link.have-active-child { - background-color: unset; } + background-color: unset; +} .sidebar.expanded ul.sidebar-elements li > a.sidebar-link.have-active-child:hover, .sidebar:hover ul.sidebar-elements li > a.sidebar-link.have-active-child:hover { - background-color: #ebebeb; } + background-color: #ebebeb; +} ul.sidebar-elements li > a.sidebar-link.active::after { - background-color: #18bc9c; } + background-color: #18bc9c; +} .lock-sidebar > a.btn { - background-color: unset; } + background-color: unset; +} diff --git a/webroot/css/themes/theme-quartz.css b/webroot/css/themes/theme-quartz.css index f39d2b8..c676d28 100644 --- a/webroot/css/themes/theme-quartz.css +++ b/webroot/css/themes/theme-quartz.css @@ -1,323 +1,408 @@ /* Callout */ .callout { border: 1px solid #e9e9e8; - border-radius: .25rem; + border-radius: 0.25rem; background-color: transparent; - box-shadow: 0 0 35px 0 rgba(154, 161, 171, 0.25); } + box-shadow: 0 0 35px 0 rgba(154, 161, 171, 0.25); +} .callout-primary { border-left-color: #e83283; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-secondary { border-left-color: rgba(255, 255, 255, 0.4); - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-success { border-left-color: #41d7a7; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-info { border-left-color: #39cbfb; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-warning { border-left-color: #ffc107; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-danger { border-left-color: #fd7e14; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-light { border-left-color: #e9e9e8; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-dark { border-left-color: #212529; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} /* Toasts */ .toast { - min-width: 250px; } + min-width: 250px; +} .toast-primary { color: #460f27; background-color: #f8c2da; - border-color: #f6adcd; } - .toast-primary strong { - border-top-color: #f396bf; } + border-color: #f6adcd; +} +.toast-primary strong { + border-top-color: #f396bf; +} .toast-secondary { color: rgba(25, 25, 25, 0.82); background-color: rgba(255, 255, 255, 0.82); - border-color: rgba(255, 255, 255, 0.76); } - .toast-secondary strong { - border-top-color: rgba(242, 242, 242, 0.76); } + border-color: rgba(255, 255, 255, 0.76); +} +.toast-secondary strong { + border-top-color: rgba(242, 242, 242, 0.76); +} .toast-success { color: #144132; background-color: #c6f3e5; - border-color: #b3efdc; } - .toast-success strong { - border-top-color: #9eebd2; } + border-color: #b3efdc; +} +.toast-success strong { + border-top-color: #9eebd2; +} .toast-info { color: #113d4b; background-color: #c4effe; - border-color: #b0eafd; } - .toast-info strong { - border-top-color: #97e3fc; } + border-color: #b0eafd; +} +.toast-info strong { + border-top-color: #97e3fc; +} .toast-warning { color: #4d3a02; background-color: #ffecb5; - border-color: #ffe69c; } - .toast-warning strong { - border-top-color: #ffe083; } + border-color: #ffe69c; +} +.toast-warning strong { + border-top-color: #ffe083; +} .toast-danger { color: #4c2606; background-color: #fed8b9; - border-color: #fecba1; } - .toast-danger strong { - border-top-color: #febd88; } + border-color: #fecba1; +} +.toast-danger strong { + border-top-color: #febd88; +} .toast-light { color: #464646; background-color: #f8f8f8; - border-color: #f6f6f6; } - .toast-light strong { - border-top-color: #e9e9e9; } + border-color: #f6f6f6; +} +.toast-light strong { + border-top-color: #e9e9e9; +} .toast-dark { color: #0a0b0c; background-color: #bcbebf; - border-color: #a6a8a9; } - .toast-dark strong { - border-top-color: #999b9c; } + border-color: #a6a8a9; +} +.toast-dark strong { + border-top-color: #999b9c; +} /* Dropdown-item */ .dropdown-item.dropdown-item-primary { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #e83283; } .dropdown-item.dropdown-item-primary:hover { color: #fff; background-color: #c52b6f; } +======= + background-color: #e83283; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-primary:hover { color: #fff; - background-color: #e83283; } - + background-color: #e83283; +} .dropdown-item.dropdown-item-secondary { color: #000; text-decoration: none; +<<<<<<< HEAD background-color: rgba(255, 255, 255, 0.4); } .dropdown-item.dropdown-item-secondary:hover { color: #000; background-color: rgba(255, 255, 255, 0.49); } +======= + background-color: rgba(255, 255, 255, 0.4); +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-secondary:hover { color: #000; - background-color: rgba(255, 255, 255, 0.4); } - + background-color: rgba(255, 255, 255, 0.4); +} .dropdown-item.dropdown-item-success { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #41d7a7; } .dropdown-item.dropdown-item-success:hover { color: #fff; background-color: #37b78e; } +======= + background-color: #41d7a7; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-success:hover { color: #fff; - background-color: #41d7a7; } - + background-color: #41d7a7; +} .dropdown-item.dropdown-item-info { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #39cbfb; } .dropdown-item.dropdown-item-info:hover { color: #fff; background-color: #30add5; } +======= + background-color: #39cbfb; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-info:hover { color: #fff; - background-color: #39cbfb; } - + background-color: #39cbfb; +} .dropdown-item.dropdown-item-warning { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #ffc107; } .dropdown-item.dropdown-item-warning:hover { color: #fff; background-color: #d9a406; } +======= + background-color: #ffc107; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-warning:hover { color: #fff; - background-color: #ffc107; } - + background-color: #ffc107; +} .dropdown-item.dropdown-item-danger { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #fd7e14; } .dropdown-item.dropdown-item-danger:hover { color: #fff; background-color: #d76b11; } +======= + background-color: #fd7e14; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-danger:hover { color: #fff; - background-color: #fd7e14; } - + background-color: #fd7e14; +} .dropdown-item.dropdown-item-light { color: #000; text-decoration: none; +<<<<<<< HEAD background-color: #e9e9e8; } .dropdown-item.dropdown-item-light:hover { color: #000; background-color: #ececeb; } +======= + background-color: #e9e9e8; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-light:hover { color: #000; - background-color: #e9e9e8; } - + background-color: #e9e9e8; +} .dropdown-item.dropdown-item-dark { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #212529; } .dropdown-item.dropdown-item-dark:hover { color: #fff; background-color: #1c1f23; } +======= + background-color: #212529; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-dark:hover { color: #fff; - background-color: #212529; } + background-color: #212529; +} /* Progress Timeline */ .progress-timeline { - padding: 0.2em 0.2em 0.5em 0.2em; } - .progress-timeline ul { - position: relative; - padding: 0; } - .progress-timeline li { - list-style-type: none; - position: relative; } - .progress-timeline li.progress-inactive { - opacity: 0.5; } - .progress-timeline .progress-line { - height: 2px; } - .progress-timeline .progress-line.progress-inactive { - opacity: 0.5; } + padding: 0.2em 0.2em 0.5em 0.2em; +} +.progress-timeline ul { + position: relative; + padding: 0; +} +.progress-timeline li { + list-style-type: none; + position: relative; +} +.progress-timeline li.progress-inactive { + opacity: 0.5; +} +.progress-timeline .progress-line { + height: 2px; +} +.progress-timeline .progress-line.progress-inactive { + opacity: 0.5; +} /* Forms severity */ .form-control.is-invalid.info { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%2339cbfb' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%2339cbfb' stroke='none'/%3e%3c/svg%3e"); } - .form-control.is-invalid.info:focus { - border-color: #39cbfb; - box-shadow: 0 0 0 0.25rem rgba(57, 203, 251, 0.25); } - + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%2339cbfb' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%2339cbfb' stroke='none'/%3e%3c/svg%3e"); +} +.form-control.is-invalid.info:focus { + border-color: #39cbfb; + box-shadow: 0 0 0 0.25rem rgba(57, 203, 251, 0.25); +} .form-control.is-invalid.warning { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23ffc107' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23ffc107' stroke='none'/%3e%3c/svg%3e"); } - .form-control.is-invalid.warning:focus { - border-color: #ffc107; - box-shadow: 0 0 0 0.25rem rgba(255, 193, 7, 0.25); } + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23ffc107' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23ffc107' stroke='none'/%3e%3c/svg%3e"); +} +.form-control.is-invalid.warning:focus { + border-color: #ffc107; + box-shadow: 0 0 0 0.25rem rgba(255, 193, 7, 0.25); +} .form-select.is-invalid:not([multiple]):not([size]).info, -.form-select.is-invalid:not([multiple])[size="1"] -.form-select.is-invalid.info { +.form-select.is-invalid:not([multiple])[size="1"] .form-select.is-invalid.info { background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%2339cbfb'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%2339cbfb' stroke='none'/%3e%3c/svg%3e"); - background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); } - .form-select.is-invalid:not([multiple]):not([size]).info:focus, - .form-select.is-invalid:not([multiple])[size="1"] -.form-select.is-invalid.info:focus { - box-shadow: 0 0 0 0.25rem rgba(57, 203, 251, 0.25); } - + background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} +.form-select.is-invalid:not([multiple]):not([size]).info:focus, +.form-select.is-invalid:not([multiple])[size="1"] .form-select.is-invalid.info:focus { + box-shadow: 0 0 0 0.25rem rgba(57, 203, 251, 0.25); +} .form-select.is-invalid:not([multiple]):not([size]).warning, -.form-select.is-invalid:not([multiple])[size="1"] -.form-select.is-invalid.warning { +.form-select.is-invalid:not([multiple])[size="1"] .form-select.is-invalid.warning { background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23ffc107'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23ffc107' stroke='none'/%3e%3c/svg%3e"); - background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); } - .form-select.is-invalid:not([multiple]):not([size]).warning:focus, - .form-select.is-invalid:not([multiple])[size="1"] -.form-select.is-invalid.warning:focus { - box-shadow: 0 0 0 0.25rem rgba(255, 193, 7, 0.25); } + background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} +.form-select.is-invalid:not([multiple]):not([size]).warning:focus, +.form-select.is-invalid:not([multiple])[size="1"] .form-select.is-invalid.warning:focus { + box-shadow: 0 0 0 0.25rem rgba(255, 193, 7, 0.25); +} .form-check-input.is-invalid.info { - border-color: #39cbfb; } - + border-color: #39cbfb; +} .form-check-input.is-invalid.info:checked { - background-color: #39cbfb; } - + background-color: #39cbfb; +} .form-check-input.is-invalid.info ~ .form-check-label { - color: unset; } - + color: unset; +} .form-check-input.is-invalid.info:focus { - box-shadow: 0 0 0 0.2rem rgba(57, 203, 251, 0.25); } - + box-shadow: 0 0 0 0.2rem rgba(57, 203, 251, 0.25); +} .form-check-input.is-invalid.warning { - border-color: #ffc107; } - + border-color: #ffc107; +} .form-check-input.is-invalid.warning:checked { - background-color: #ffc107; } - + background-color: #ffc107; +} .form-check-input.is-invalid.warning ~ .form-check-label { - color: unset; } - + color: unset; +} .form-check-input.is-invalid.warning:focus { - box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.25); } + box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.25); +} /* Utilities */ .mw-75 { - max-width: 75% !important; } + max-width: 75% !important; +} .mw-66 { max-width: 66% !important; } .mw-50 { - max-width: 50% !important; } + max-width: 50% !important; +} .mw-33 { max-width: 33% !important; } .mw-25 { - max-width: 25% !important; } + max-width: 25% !important; +} .mh-75 { - max-height: 75% !important; } + max-height: 75% !important; +} .mh-66 { max-height: 66% !important; } .mh-50 { - max-height: 50% !important; } + max-height: 50% !important; +} .mh-33 { max-height: 33% !important; } .mh-25 { - max-height: 25% !important; } + max-height: 25% !important; +} .p-abs-center-y { top: 50%; - transform: translateY(-50%); } + transform: translateY(-50%); +} .p-abs-center-x { left: 50%; - transform: translateX(-50%); } + transform: translateX(-50%); +} .p-abs-center-both { top: 50%; left: 50%; - transform: translateX(-50%) translateY(-50%); } + transform: translateX(-50%) translateY(-50%); +} .fs-7 { font-size: .875rem !important; } @@ -377,88 +462,111 @@ .panel { background-color: transparent; border: none; - box-shadow: 0 0 35px 0 rgba(154, 161, 171, 0.25); } + box-shadow: 0 0 35px 0 rgba(154, 161, 171, 0.25); +} .loading-overlay { background-color: #e9e9e8; - opacity: 0.35; } + opacity: 0.35; +} /* Top navbar */ .top-navbar { - background-color: #e83283; } + background-color: #e83283; +} .center-navbar nav.header-breadcrumb { - color: #fff; } + color: #fff; +} header.top-navbar .header-menu > a:hover, header.top-navbar .header-breadcrumb .header-breadcrumb-item > a:hover { - color: #d6d6d6 !important; } + color: #d6d6d6 !important; +} .top-navbar .center-navbar nav.header-breadcrumb li.header-breadcrumb-item a { - color: #fff; } + color: #fff; +} .top-navbar .right-navbar .header-menu a.nav-link { - color: #fff; } + color: #fff; +} .top-navbar .left-navbar .navbar-brand img { - filter: invert(1); } + filter: invert(1); +} .top-navbar .left-navbar .navbar-brand:hover img { - filter: invert(1) drop-shadow(0px 0px 3px #fff); } + filter: invert(1) drop-shadow(0px 0px 3px #fff); +} .top-navbar .composed-app-icon-container > .app-icon { - background-color: #fff; } + background-color: #fff; +} .breadcrumb-link-container { box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 2px 6px 0 rgba(0, 0, 0, 0.12); - background-color: rgba(255, 255, 255, 0.4); } + background-color: rgba(255, 255, 255, 0.4); +} /* Sidebar */ .sidebar { - transition: width .08s linear; + transition: width 0.08s linear; box-shadow: none; - background-color: rgba(255, 255, 255, 0.4); } + background-color: rgba(255, 255, 255, 0.4); +} .sidebar ~ main.content:after { - background: #000; } + background: #000; +} .sidebar .sidebar-wrapper { - border-right: 1px solid none; } + border-right: 1px solid none; +} .sidebar .sidebar-wrapper { - border-right: 1px solid rgba(0, 0, 0, 0.125); } + border-right: 1px solid rgba(0, 0, 0, 0.125); +} .sidebar ul.sidebar-elements li > a.sidebar-link { - color: #fff; } + color: #fff; +} .sidebar ul.sidebar-elements li > a.sidebar-link.active { background-color: #fff; - color: #343a40; } + color: #343a40; +} .sidebar ul.sidebar-elements li > a.sidebar-link.have-active-child { background-color: #fff; - color: #343a40; } + color: #343a40; +} .sidebar ul.sidebar-elements li > a.sidebar-link.have-active-child::after { background-color: #e83283; } .sidebar ul.sidebar-elements li > a.sidebar-link:hover { background-color: #fff; - color: #343a40; } + color: #343a40; +} .sidebar.expanded ul.sidebar-elements li > a.sidebar-link.have-active-child, .sidebar:hover ul.sidebar-elements li > a.sidebar-link.have-active-child { - background-color: unset; } + background-color: unset; +} .sidebar.expanded ul.sidebar-elements li > a.sidebar-link.have-active-child:hover, .sidebar:hover ul.sidebar-elements li > a.sidebar-link.have-active-child:hover { - background-color: #fff; } + background-color: #fff; +} ul.sidebar-elements li > a.sidebar-link.active::after { - background-color: #e83283; } + background-color: #e83283; +} .lock-sidebar > a.btn { - background-color: rgba(255, 255, 255, 0.4); } + background-color: rgba(255, 255, 255, 0.4); +} .btn { display: inline-block; @@ -475,427 +583,485 @@ ul.sidebar-elements li > a.sidebar-link.active::after { padding: 0.75rem 1.5rem; font-size: 1rem; border-radius: 0.5rem; - transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; } - @media (prefers-reduced-motion: reduce) { - .btn { - transition: none; } } - .btn:hover { - color: #fff; } - .btn-check:focus + .btn, .btn:focus { - outline: 0; - box-shadow: 0 0 0 0.25rem rgba(232, 50, 131, 0.25); } - .btn:disabled, .btn.disabled, - fieldset:disabled .btn { - pointer-events: none; - opacity: 0.65; } + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .btn { + transition: none; + } +} +.btn:hover { + color: #fff; +} +.btn-check:focus + .btn, .btn:focus { + outline: 0; + box-shadow: 0 0 0 0.25rem rgba(232, 50, 131, 0.25); +} +.btn:disabled, .btn.disabled, fieldset:disabled .btn { + pointer-events: none; + opacity: 0.65; +} .btn-primary { color: #fff; background-color: #e83283; - border-color: #e83283; } - .btn-primary:hover { - color: #fff; - background-color: #c52b6f; - border-color: #ba2869; } - .btn-check:focus + .btn-primary, .btn-primary:focus { - color: #fff; - background-color: #c52b6f; - border-color: #ba2869; - box-shadow: 0 0 0 0.25rem rgba(235, 81, 150, 0.5); } - .btn-check:checked + .btn-primary, - .btn-check:active + .btn-primary, .btn-primary:active, .btn-primary.active, - .show > .btn-primary.dropdown-toggle { - color: #fff; - background-color: #ba2869; - border-color: #ae2662; } - .btn-check:checked + .btn-primary:focus, - .btn-check:active + .btn-primary:focus, .btn-primary:active:focus, .btn-primary.active:focus, - .show > .btn-primary.dropdown-toggle:focus { - box-shadow: 0 0 0 0.25rem rgba(235, 81, 150, 0.5); } - .btn-primary:disabled, .btn-primary.disabled { - color: #fff; - background-color: #e83283; - border-color: #e83283; } + border-color: #e83283; +} +.btn-primary:hover { + color: #fff; + background-color: #c52b6f; + border-color: #ba2869; +} +.btn-check:focus + .btn-primary, .btn-primary:focus { + color: #fff; + background-color: #c52b6f; + border-color: #ba2869; + box-shadow: 0 0 0 0.25rem rgba(235, 81, 150, 0.5); +} +.btn-check:checked + .btn-primary, .btn-check:active + .btn-primary, .btn-primary:active, .btn-primary.active, .show > .btn-primary.dropdown-toggle { + color: #fff; + background-color: #ba2869; + border-color: #ae2662; +} +.btn-check:checked + .btn-primary:focus, .btn-check:active + .btn-primary:focus, .btn-primary:active:focus, .btn-primary.active:focus, .show > .btn-primary.dropdown-toggle:focus { + box-shadow: 0 0 0 0.25rem rgba(235, 81, 150, 0.5); +} +.btn-primary:disabled, .btn-primary.disabled { + color: #fff; + background-color: #e83283; + border-color: #e83283; +} .btn-secondary { color: #000; background-color: rgba(255, 255, 255, 0.4); - border-color: rgba(255, 255, 255, 0.4); } - .btn-secondary:hover { - color: #000; - background-color: rgba(255, 255, 255, 0.49); - border-color: rgba(255, 255, 255, 0.46); } - .btn-check:focus + .btn-secondary, .btn-secondary:focus { - color: #000; - background-color: rgba(255, 255, 255, 0.49); - border-color: rgba(255, 255, 255, 0.46); - box-shadow: 0 0 0 0.25rem rgba(149, 149, 149, 0.5); } - .btn-check:checked + .btn-secondary, - .btn-check:active + .btn-secondary, .btn-secondary:active, .btn-secondary.active, - .show > .btn-secondary.dropdown-toggle { - color: #000; - background-color: rgba(255, 255, 255, 0.52); - border-color: rgba(255, 255, 255, 0.46); } - .btn-check:checked + .btn-secondary:focus, - .btn-check:active + .btn-secondary:focus, .btn-secondary:active:focus, .btn-secondary.active:focus, - .show > .btn-secondary.dropdown-toggle:focus { - box-shadow: 0 0 0 0.25rem rgba(149, 149, 149, 0.5); } - .btn-secondary:disabled, .btn-secondary.disabled { - color: #000; - background-color: rgba(255, 255, 255, 0.4); - border-color: rgba(255, 255, 255, 0.4); } + border-color: rgba(255, 255, 255, 0.4); +} +.btn-secondary:hover { + color: #000; + background-color: rgba(255, 255, 255, 0.49); + border-color: rgba(255, 255, 255, 0.46); +} +.btn-check:focus + .btn-secondary, .btn-secondary:focus { + color: #000; + background-color: rgba(255, 255, 255, 0.49); + border-color: rgba(255, 255, 255, 0.46); + box-shadow: 0 0 0 0.25rem rgba(149, 149, 149, 0.5); +} +.btn-check:checked + .btn-secondary, .btn-check:active + .btn-secondary, .btn-secondary:active, .btn-secondary.active, .show > .btn-secondary.dropdown-toggle { + color: #000; + background-color: rgba(255, 255, 255, 0.52); + border-color: rgba(255, 255, 255, 0.46); +} +.btn-check:checked + .btn-secondary:focus, .btn-check:active + .btn-secondary:focus, .btn-secondary:active:focus, .btn-secondary.active:focus, .show > .btn-secondary.dropdown-toggle:focus { + box-shadow: 0 0 0 0.25rem rgba(149, 149, 149, 0.5); +} +.btn-secondary:disabled, .btn-secondary.disabled { + color: #000; + background-color: rgba(255, 255, 255, 0.4); + border-color: rgba(255, 255, 255, 0.4); +} .btn-success { color: #fff; background-color: #41d7a7; - border-color: #41d7a7; } - .btn-success:hover { - color: #fff; - background-color: #37b78e; - border-color: #34ac86; } - .btn-check:focus + .btn-success, .btn-success:focus { - color: #fff; - background-color: #37b78e; - border-color: #34ac86; - box-shadow: 0 0 0 0.25rem rgba(94, 221, 180, 0.5); } - .btn-check:checked + .btn-success, - .btn-check:active + .btn-success, .btn-success:active, .btn-success.active, - .show > .btn-success.dropdown-toggle { - color: #fff; - background-color: #34ac86; - border-color: #31a17d; } - .btn-check:checked + .btn-success:focus, - .btn-check:active + .btn-success:focus, .btn-success:active:focus, .btn-success.active:focus, - .show > .btn-success.dropdown-toggle:focus { - box-shadow: 0 0 0 0.25rem rgba(94, 221, 180, 0.5); } - .btn-success:disabled, .btn-success.disabled { - color: #fff; - background-color: #41d7a7; - border-color: #41d7a7; } + border-color: #41d7a7; +} +.btn-success:hover { + color: #fff; + background-color: #37b78e; + border-color: #34ac86; +} +.btn-check:focus + .btn-success, .btn-success:focus { + color: #fff; + background-color: #37b78e; + border-color: #34ac86; + box-shadow: 0 0 0 0.25rem rgba(94, 221, 180, 0.5); +} +.btn-check:checked + .btn-success, .btn-check:active + .btn-success, .btn-success:active, .btn-success.active, .show > .btn-success.dropdown-toggle { + color: #fff; + background-color: #34ac86; + border-color: #31a17d; +} +.btn-check:checked + .btn-success:focus, .btn-check:active + .btn-success:focus, .btn-success:active:focus, .btn-success.active:focus, .show > .btn-success.dropdown-toggle:focus { + box-shadow: 0 0 0 0.25rem rgba(94, 221, 180, 0.5); +} +.btn-success:disabled, .btn-success.disabled { + color: #fff; + background-color: #41d7a7; + border-color: #41d7a7; +} .btn-info { color: #fff; background-color: #39cbfb; - border-color: #39cbfb; } - .btn-info:hover { - color: #fff; - background-color: #30add5; - border-color: #2ea2c9; } - .btn-check:focus + .btn-info, .btn-info:focus { - color: #fff; - background-color: #30add5; - border-color: #2ea2c9; - box-shadow: 0 0 0 0.25rem rgba(87, 211, 252, 0.5); } - .btn-check:checked + .btn-info, - .btn-check:active + .btn-info, .btn-info:active, .btn-info.active, - .show > .btn-info.dropdown-toggle { - color: #fff; - background-color: #2ea2c9; - border-color: #2b98bc; } - .btn-check:checked + .btn-info:focus, - .btn-check:active + .btn-info:focus, .btn-info:active:focus, .btn-info.active:focus, - .show > .btn-info.dropdown-toggle:focus { - box-shadow: 0 0 0 0.25rem rgba(87, 211, 252, 0.5); } - .btn-info:disabled, .btn-info.disabled { - color: #fff; - background-color: #39cbfb; - border-color: #39cbfb; } + border-color: #39cbfb; +} +.btn-info:hover { + color: #fff; + background-color: #30add5; + border-color: #2ea2c9; +} +.btn-check:focus + .btn-info, .btn-info:focus { + color: #fff; + background-color: #30add5; + border-color: #2ea2c9; + box-shadow: 0 0 0 0.25rem rgba(87, 211, 252, 0.5); +} +.btn-check:checked + .btn-info, .btn-check:active + .btn-info, .btn-info:active, .btn-info.active, .show > .btn-info.dropdown-toggle { + color: #fff; + background-color: #2ea2c9; + border-color: #2b98bc; +} +.btn-check:checked + .btn-info:focus, .btn-check:active + .btn-info:focus, .btn-info:active:focus, .btn-info.active:focus, .show > .btn-info.dropdown-toggle:focus { + box-shadow: 0 0 0 0.25rem rgba(87, 211, 252, 0.5); +} +.btn-info:disabled, .btn-info.disabled { + color: #fff; + background-color: #39cbfb; + border-color: #39cbfb; +} .btn-warning { color: #fff; background-color: #ffc107; - border-color: #ffc107; } - .btn-warning:hover { - color: #fff; - background-color: #d9a406; - border-color: #cc9a06; } - .btn-check:focus + .btn-warning, .btn-warning:focus { - color: #fff; - background-color: #d9a406; - border-color: #cc9a06; - box-shadow: 0 0 0 0.25rem rgba(255, 202, 44, 0.5); } - .btn-check:checked + .btn-warning, - .btn-check:active + .btn-warning, .btn-warning:active, .btn-warning.active, - .show > .btn-warning.dropdown-toggle { - color: #fff; - background-color: #cc9a06; - border-color: #bf9105; } - .btn-check:checked + .btn-warning:focus, - .btn-check:active + .btn-warning:focus, .btn-warning:active:focus, .btn-warning.active:focus, - .show > .btn-warning.dropdown-toggle:focus { - box-shadow: 0 0 0 0.25rem rgba(255, 202, 44, 0.5); } - .btn-warning:disabled, .btn-warning.disabled { - color: #fff; - background-color: #ffc107; - border-color: #ffc107; } + border-color: #ffc107; +} +.btn-warning:hover { + color: #fff; + background-color: #d9a406; + border-color: #cc9a06; +} +.btn-check:focus + .btn-warning, .btn-warning:focus { + color: #fff; + background-color: #d9a406; + border-color: #cc9a06; + box-shadow: 0 0 0 0.25rem rgba(255, 202, 44, 0.5); +} +.btn-check:checked + .btn-warning, .btn-check:active + .btn-warning, .btn-warning:active, .btn-warning.active, .show > .btn-warning.dropdown-toggle { + color: #fff; + background-color: #cc9a06; + border-color: #bf9105; +} +.btn-check:checked + .btn-warning:focus, .btn-check:active + .btn-warning:focus, .btn-warning:active:focus, .btn-warning.active:focus, .show > .btn-warning.dropdown-toggle:focus { + box-shadow: 0 0 0 0.25rem rgba(255, 202, 44, 0.5); +} +.btn-warning:disabled, .btn-warning.disabled { + color: #fff; + background-color: #ffc107; + border-color: #ffc107; +} .btn-danger { color: #fff; background-color: #fd7e14; - border-color: #fd7e14; } - .btn-danger:hover { - color: #fff; - background-color: #d76b11; - border-color: #ca6510; } - .btn-check:focus + .btn-danger, .btn-danger:focus { - color: #fff; - background-color: #d76b11; - border-color: #ca6510; - box-shadow: 0 0 0 0.25rem rgba(253, 145, 55, 0.5); } - .btn-check:checked + .btn-danger, - .btn-check:active + .btn-danger, .btn-danger:active, .btn-danger.active, - .show > .btn-danger.dropdown-toggle { - color: #fff; - background-color: #ca6510; - border-color: #be5f0f; } - .btn-check:checked + .btn-danger:focus, - .btn-check:active + .btn-danger:focus, .btn-danger:active:focus, .btn-danger.active:focus, - .show > .btn-danger.dropdown-toggle:focus { - box-shadow: 0 0 0 0.25rem rgba(253, 145, 55, 0.5); } - .btn-danger:disabled, .btn-danger.disabled { - color: #fff; - background-color: #fd7e14; - border-color: #fd7e14; } + border-color: #fd7e14; +} +.btn-danger:hover { + color: #fff; + background-color: #d76b11; + border-color: #ca6510; +} +.btn-check:focus + .btn-danger, .btn-danger:focus { + color: #fff; + background-color: #d76b11; + border-color: #ca6510; + box-shadow: 0 0 0 0.25rem rgba(253, 145, 55, 0.5); +} +.btn-check:checked + .btn-danger, .btn-check:active + .btn-danger, .btn-danger:active, .btn-danger.active, .show > .btn-danger.dropdown-toggle { + color: #fff; + background-color: #ca6510; + border-color: #be5f0f; +} +.btn-check:checked + .btn-danger:focus, .btn-check:active + .btn-danger:focus, .btn-danger:active:focus, .btn-danger.active:focus, .show > .btn-danger.dropdown-toggle:focus { + box-shadow: 0 0 0 0.25rem rgba(253, 145, 55, 0.5); +} +.btn-danger:disabled, .btn-danger.disabled { + color: #fff; + background-color: #fd7e14; + border-color: #fd7e14; +} .btn-light { color: #000; background-color: #e9e9e8; - border-color: #e9e9e8; } - .btn-light:hover { - color: #000; - background-color: #ececeb; - border-color: #ebebea; } - .btn-check:focus + .btn-light, .btn-light:focus { - color: #000; - background-color: #ececeb; - border-color: #ebebea; - box-shadow: 0 0 0 0.25rem rgba(198, 198, 197, 0.5); } - .btn-check:checked + .btn-light, - .btn-check:active + .btn-light, .btn-light:active, .btn-light.active, - .show > .btn-light.dropdown-toggle { - color: #000; - background-color: #ededed; - border-color: #ebebea; } - .btn-check:checked + .btn-light:focus, - .btn-check:active + .btn-light:focus, .btn-light:active:focus, .btn-light.active:focus, - .show > .btn-light.dropdown-toggle:focus { - box-shadow: 0 0 0 0.25rem rgba(198, 198, 197, 0.5); } - .btn-light:disabled, .btn-light.disabled { - color: #000; - background-color: #e9e9e8; - border-color: #e9e9e8; } + border-color: #e9e9e8; +} +.btn-light:hover { + color: #000; + background-color: #ececeb; + border-color: #ebebea; +} +.btn-check:focus + .btn-light, .btn-light:focus { + color: #000; + background-color: #ececeb; + border-color: #ebebea; + box-shadow: 0 0 0 0.25rem rgba(198, 198, 197, 0.5); +} +.btn-check:checked + .btn-light, .btn-check:active + .btn-light, .btn-light:active, .btn-light.active, .show > .btn-light.dropdown-toggle { + color: #000; + background-color: #ededed; + border-color: #ebebea; +} +.btn-check:checked + .btn-light:focus, .btn-check:active + .btn-light:focus, .btn-light:active:focus, .btn-light.active:focus, .show > .btn-light.dropdown-toggle:focus { + box-shadow: 0 0 0 0.25rem rgba(198, 198, 197, 0.5); +} +.btn-light:disabled, .btn-light.disabled { + color: #000; + background-color: #e9e9e8; + border-color: #e9e9e8; +} .btn-dark { color: #fff; background-color: #212529; - border-color: #212529; } - .btn-dark:hover { - color: #fff; - background-color: #1c1f23; - border-color: #1a1e21; } - .btn-check:focus + .btn-dark, .btn-dark:focus { - color: #fff; - background-color: #1c1f23; - border-color: #1a1e21; - box-shadow: 0 0 0 0.25rem rgba(66, 70, 73, 0.5); } - .btn-check:checked + .btn-dark, - .btn-check:active + .btn-dark, .btn-dark:active, .btn-dark.active, - .show > .btn-dark.dropdown-toggle { - color: #fff; - background-color: #1a1e21; - border-color: #191c1f; } - .btn-check:checked + .btn-dark:focus, - .btn-check:active + .btn-dark:focus, .btn-dark:active:focus, .btn-dark.active:focus, - .show > .btn-dark.dropdown-toggle:focus { - box-shadow: 0 0 0 0.25rem rgba(66, 70, 73, 0.5); } - .btn-dark:disabled, .btn-dark.disabled { - color: #fff; - background-color: #212529; - border-color: #212529; } + border-color: #212529; +} +.btn-dark:hover { + color: #fff; + background-color: #1c1f23; + border-color: #1a1e21; +} +.btn-check:focus + .btn-dark, .btn-dark:focus { + color: #fff; + background-color: #1c1f23; + border-color: #1a1e21; + box-shadow: 0 0 0 0.25rem rgba(66, 70, 73, 0.5); +} +.btn-check:checked + .btn-dark, .btn-check:active + .btn-dark, .btn-dark:active, .btn-dark.active, .show > .btn-dark.dropdown-toggle { + color: #fff; + background-color: #1a1e21; + border-color: #191c1f; +} +.btn-check:checked + .btn-dark:focus, .btn-check:active + .btn-dark:focus, .btn-dark:active:focus, .btn-dark.active:focus, .show > .btn-dark.dropdown-toggle:focus { + box-shadow: 0 0 0 0.25rem rgba(66, 70, 73, 0.5); +} +.btn-dark:disabled, .btn-dark.disabled { + color: #fff; + background-color: #212529; + border-color: #212529; +} .btn-outline-primary { color: #e83283; - border-color: #e83283; } - .btn-outline-primary:hover { - color: #fff; - background-color: #e83283; - border-color: #e83283; } - .btn-check:focus + .btn-outline-primary, .btn-outline-primary:focus { - box-shadow: 0 0 0 0.25rem rgba(232, 50, 131, 0.5); } - .btn-check:checked + .btn-outline-primary, - .btn-check:active + .btn-outline-primary, .btn-outline-primary:active, .btn-outline-primary.active, .btn-outline-primary.dropdown-toggle.show { - color: #fff; - background-color: #e83283; - border-color: #e83283; } - .btn-check:checked + .btn-outline-primary:focus, - .btn-check:active + .btn-outline-primary:focus, .btn-outline-primary:active:focus, .btn-outline-primary.active:focus, .btn-outline-primary.dropdown-toggle.show:focus { - box-shadow: 0 0 0 0.25rem rgba(232, 50, 131, 0.5); } - .btn-outline-primary:disabled, .btn-outline-primary.disabled { - color: #e83283; - background-color: transparent; } + border-color: #e83283; +} +.btn-outline-primary:hover { + color: #fff; + background-color: #e83283; + border-color: #e83283; +} +.btn-check:focus + .btn-outline-primary, .btn-outline-primary:focus { + box-shadow: 0 0 0 0.25rem rgba(232, 50, 131, 0.5); +} +.btn-check:checked + .btn-outline-primary, .btn-check:active + .btn-outline-primary, .btn-outline-primary:active, .btn-outline-primary.active, .btn-outline-primary.dropdown-toggle.show { + color: #fff; + background-color: #e83283; + border-color: #e83283; +} +.btn-check:checked + .btn-outline-primary:focus, .btn-check:active + .btn-outline-primary:focus, .btn-outline-primary:active:focus, .btn-outline-primary.active:focus, .btn-outline-primary.dropdown-toggle.show:focus { + box-shadow: 0 0 0 0.25rem rgba(232, 50, 131, 0.5); +} +.btn-outline-primary:disabled, .btn-outline-primary.disabled { + color: #e83283; + background-color: transparent; +} .btn-outline-secondary { color: rgba(255, 255, 255, 0.4); - border-color: rgba(255, 255, 255, 0.4); } - .btn-outline-secondary:hover { - color: #000; - background-color: rgba(255, 255, 255, 0.4); - border-color: rgba(255, 255, 255, 0.4); } - .btn-check:focus + .btn-outline-secondary, .btn-outline-secondary:focus { - box-shadow: 0 0 0 0.25rem rgba(255, 255, 255, 0.5); } - .btn-check:checked + .btn-outline-secondary, - .btn-check:active + .btn-outline-secondary, .btn-outline-secondary:active, .btn-outline-secondary.active, .btn-outline-secondary.dropdown-toggle.show { - color: #000; - background-color: rgba(255, 255, 255, 0.4); - border-color: rgba(255, 255, 255, 0.4); } - .btn-check:checked + .btn-outline-secondary:focus, - .btn-check:active + .btn-outline-secondary:focus, .btn-outline-secondary:active:focus, .btn-outline-secondary.active:focus, .btn-outline-secondary.dropdown-toggle.show:focus { - box-shadow: 0 0 0 0.25rem rgba(255, 255, 255, 0.5); } - .btn-outline-secondary:disabled, .btn-outline-secondary.disabled { - color: rgba(255, 255, 255, 0.4); - background-color: transparent; } + border-color: rgba(255, 255, 255, 0.4); +} +.btn-outline-secondary:hover { + color: #000; + background-color: rgba(255, 255, 255, 0.4); + border-color: rgba(255, 255, 255, 0.4); +} +.btn-check:focus + .btn-outline-secondary, .btn-outline-secondary:focus { + box-shadow: 0 0 0 0.25rem rgba(255, 255, 255, 0.5); +} +.btn-check:checked + .btn-outline-secondary, .btn-check:active + .btn-outline-secondary, .btn-outline-secondary:active, .btn-outline-secondary.active, .btn-outline-secondary.dropdown-toggle.show { + color: #000; + background-color: rgba(255, 255, 255, 0.4); + border-color: rgba(255, 255, 255, 0.4); +} +.btn-check:checked + .btn-outline-secondary:focus, .btn-check:active + .btn-outline-secondary:focus, .btn-outline-secondary:active:focus, .btn-outline-secondary.active:focus, .btn-outline-secondary.dropdown-toggle.show:focus { + box-shadow: 0 0 0 0.25rem rgba(255, 255, 255, 0.5); +} +.btn-outline-secondary:disabled, .btn-outline-secondary.disabled { + color: rgba(255, 255, 255, 0.4); + background-color: transparent; +} .btn-outline-success { color: #41d7a7; - border-color: #41d7a7; } - .btn-outline-success:hover { - color: #fff; - background-color: #41d7a7; - border-color: #41d7a7; } - .btn-check:focus + .btn-outline-success, .btn-outline-success:focus { - box-shadow: 0 0 0 0.25rem rgba(65, 215, 167, 0.5); } - .btn-check:checked + .btn-outline-success, - .btn-check:active + .btn-outline-success, .btn-outline-success:active, .btn-outline-success.active, .btn-outline-success.dropdown-toggle.show { - color: #fff; - background-color: #41d7a7; - border-color: #41d7a7; } - .btn-check:checked + .btn-outline-success:focus, - .btn-check:active + .btn-outline-success:focus, .btn-outline-success:active:focus, .btn-outline-success.active:focus, .btn-outline-success.dropdown-toggle.show:focus { - box-shadow: 0 0 0 0.25rem rgba(65, 215, 167, 0.5); } - .btn-outline-success:disabled, .btn-outline-success.disabled { - color: #41d7a7; - background-color: transparent; } + border-color: #41d7a7; +} +.btn-outline-success:hover { + color: #fff; + background-color: #41d7a7; + border-color: #41d7a7; +} +.btn-check:focus + .btn-outline-success, .btn-outline-success:focus { + box-shadow: 0 0 0 0.25rem rgba(65, 215, 167, 0.5); +} +.btn-check:checked + .btn-outline-success, .btn-check:active + .btn-outline-success, .btn-outline-success:active, .btn-outline-success.active, .btn-outline-success.dropdown-toggle.show { + color: #fff; + background-color: #41d7a7; + border-color: #41d7a7; +} +.btn-check:checked + .btn-outline-success:focus, .btn-check:active + .btn-outline-success:focus, .btn-outline-success:active:focus, .btn-outline-success.active:focus, .btn-outline-success.dropdown-toggle.show:focus { + box-shadow: 0 0 0 0.25rem rgba(65, 215, 167, 0.5); +} +.btn-outline-success:disabled, .btn-outline-success.disabled { + color: #41d7a7; + background-color: transparent; +} .btn-outline-info { color: #39cbfb; - border-color: #39cbfb; } - .btn-outline-info:hover { - color: #fff; - background-color: #39cbfb; - border-color: #39cbfb; } - .btn-check:focus + .btn-outline-info, .btn-outline-info:focus { - box-shadow: 0 0 0 0.25rem rgba(57, 203, 251, 0.5); } - .btn-check:checked + .btn-outline-info, - .btn-check:active + .btn-outline-info, .btn-outline-info:active, .btn-outline-info.active, .btn-outline-info.dropdown-toggle.show { - color: #fff; - background-color: #39cbfb; - border-color: #39cbfb; } - .btn-check:checked + .btn-outline-info:focus, - .btn-check:active + .btn-outline-info:focus, .btn-outline-info:active:focus, .btn-outline-info.active:focus, .btn-outline-info.dropdown-toggle.show:focus { - box-shadow: 0 0 0 0.25rem rgba(57, 203, 251, 0.5); } - .btn-outline-info:disabled, .btn-outline-info.disabled { - color: #39cbfb; - background-color: transparent; } + border-color: #39cbfb; +} +.btn-outline-info:hover { + color: #fff; + background-color: #39cbfb; + border-color: #39cbfb; +} +.btn-check:focus + .btn-outline-info, .btn-outline-info:focus { + box-shadow: 0 0 0 0.25rem rgba(57, 203, 251, 0.5); +} +.btn-check:checked + .btn-outline-info, .btn-check:active + .btn-outline-info, .btn-outline-info:active, .btn-outline-info.active, .btn-outline-info.dropdown-toggle.show { + color: #fff; + background-color: #39cbfb; + border-color: #39cbfb; +} +.btn-check:checked + .btn-outline-info:focus, .btn-check:active + .btn-outline-info:focus, .btn-outline-info:active:focus, .btn-outline-info.active:focus, .btn-outline-info.dropdown-toggle.show:focus { + box-shadow: 0 0 0 0.25rem rgba(57, 203, 251, 0.5); +} +.btn-outline-info:disabled, .btn-outline-info.disabled { + color: #39cbfb; + background-color: transparent; +} .btn-outline-warning { color: #ffc107; - border-color: #ffc107; } - .btn-outline-warning:hover { - color: #fff; - background-color: #ffc107; - border-color: #ffc107; } - .btn-check:focus + .btn-outline-warning, .btn-outline-warning:focus { - box-shadow: 0 0 0 0.25rem rgba(255, 193, 7, 0.5); } - .btn-check:checked + .btn-outline-warning, - .btn-check:active + .btn-outline-warning, .btn-outline-warning:active, .btn-outline-warning.active, .btn-outline-warning.dropdown-toggle.show { - color: #fff; - background-color: #ffc107; - border-color: #ffc107; } - .btn-check:checked + .btn-outline-warning:focus, - .btn-check:active + .btn-outline-warning:focus, .btn-outline-warning:active:focus, .btn-outline-warning.active:focus, .btn-outline-warning.dropdown-toggle.show:focus { - box-shadow: 0 0 0 0.25rem rgba(255, 193, 7, 0.5); } - .btn-outline-warning:disabled, .btn-outline-warning.disabled { - color: #ffc107; - background-color: transparent; } + border-color: #ffc107; +} +.btn-outline-warning:hover { + color: #fff; + background-color: #ffc107; + border-color: #ffc107; +} +.btn-check:focus + .btn-outline-warning, .btn-outline-warning:focus { + box-shadow: 0 0 0 0.25rem rgba(255, 193, 7, 0.5); +} +.btn-check:checked + .btn-outline-warning, .btn-check:active + .btn-outline-warning, .btn-outline-warning:active, .btn-outline-warning.active, .btn-outline-warning.dropdown-toggle.show { + color: #fff; + background-color: #ffc107; + border-color: #ffc107; +} +.btn-check:checked + .btn-outline-warning:focus, .btn-check:active + .btn-outline-warning:focus, .btn-outline-warning:active:focus, .btn-outline-warning.active:focus, .btn-outline-warning.dropdown-toggle.show:focus { + box-shadow: 0 0 0 0.25rem rgba(255, 193, 7, 0.5); +} +.btn-outline-warning:disabled, .btn-outline-warning.disabled { + color: #ffc107; + background-color: transparent; +} .btn-outline-danger { color: #fd7e14; - border-color: #fd7e14; } - .btn-outline-danger:hover { - color: #fff; - background-color: #fd7e14; - border-color: #fd7e14; } - .btn-check:focus + .btn-outline-danger, .btn-outline-danger:focus { - box-shadow: 0 0 0 0.25rem rgba(253, 126, 20, 0.5); } - .btn-check:checked + .btn-outline-danger, - .btn-check:active + .btn-outline-danger, .btn-outline-danger:active, .btn-outline-danger.active, .btn-outline-danger.dropdown-toggle.show { - color: #fff; - background-color: #fd7e14; - border-color: #fd7e14; } - .btn-check:checked + .btn-outline-danger:focus, - .btn-check:active + .btn-outline-danger:focus, .btn-outline-danger:active:focus, .btn-outline-danger.active:focus, .btn-outline-danger.dropdown-toggle.show:focus { - box-shadow: 0 0 0 0.25rem rgba(253, 126, 20, 0.5); } - .btn-outline-danger:disabled, .btn-outline-danger.disabled { - color: #fd7e14; - background-color: transparent; } + border-color: #fd7e14; +} +.btn-outline-danger:hover { + color: #fff; + background-color: #fd7e14; + border-color: #fd7e14; +} +.btn-check:focus + .btn-outline-danger, .btn-outline-danger:focus { + box-shadow: 0 0 0 0.25rem rgba(253, 126, 20, 0.5); +} +.btn-check:checked + .btn-outline-danger, .btn-check:active + .btn-outline-danger, .btn-outline-danger:active, .btn-outline-danger.active, .btn-outline-danger.dropdown-toggle.show { + color: #fff; + background-color: #fd7e14; + border-color: #fd7e14; +} +.btn-check:checked + .btn-outline-danger:focus, .btn-check:active + .btn-outline-danger:focus, .btn-outline-danger:active:focus, .btn-outline-danger.active:focus, .btn-outline-danger.dropdown-toggle.show:focus { + box-shadow: 0 0 0 0.25rem rgba(253, 126, 20, 0.5); +} +.btn-outline-danger:disabled, .btn-outline-danger.disabled { + color: #fd7e14; + background-color: transparent; +} .btn-outline-light { color: #e9e9e8; - border-color: #e9e9e8; } - .btn-outline-light:hover { - color: #000; - background-color: #e9e9e8; - border-color: #e9e9e8; } - .btn-check:focus + .btn-outline-light, .btn-outline-light:focus { - box-shadow: 0 0 0 0.25rem rgba(233, 233, 232, 0.5); } - .btn-check:checked + .btn-outline-light, - .btn-check:active + .btn-outline-light, .btn-outline-light:active, .btn-outline-light.active, .btn-outline-light.dropdown-toggle.show { - color: #000; - background-color: #e9e9e8; - border-color: #e9e9e8; } - .btn-check:checked + .btn-outline-light:focus, - .btn-check:active + .btn-outline-light:focus, .btn-outline-light:active:focus, .btn-outline-light.active:focus, .btn-outline-light.dropdown-toggle.show:focus { - box-shadow: 0 0 0 0.25rem rgba(233, 233, 232, 0.5); } - .btn-outline-light:disabled, .btn-outline-light.disabled { - color: #e9e9e8; - background-color: transparent; } + border-color: #e9e9e8; +} +.btn-outline-light:hover { + color: #000; + background-color: #e9e9e8; + border-color: #e9e9e8; +} +.btn-check:focus + .btn-outline-light, .btn-outline-light:focus { + box-shadow: 0 0 0 0.25rem rgba(233, 233, 232, 0.5); +} +.btn-check:checked + .btn-outline-light, .btn-check:active + .btn-outline-light, .btn-outline-light:active, .btn-outline-light.active, .btn-outline-light.dropdown-toggle.show { + color: #000; + background-color: #e9e9e8; + border-color: #e9e9e8; +} +.btn-check:checked + .btn-outline-light:focus, .btn-check:active + .btn-outline-light:focus, .btn-outline-light:active:focus, .btn-outline-light.active:focus, .btn-outline-light.dropdown-toggle.show:focus { + box-shadow: 0 0 0 0.25rem rgba(233, 233, 232, 0.5); +} +.btn-outline-light:disabled, .btn-outline-light.disabled { + color: #e9e9e8; + background-color: transparent; +} .btn-outline-dark { color: #212529; - border-color: #212529; } - .btn-outline-dark:hover { - color: #fff; - background-color: #212529; - border-color: #212529; } - .btn-check:focus + .btn-outline-dark, .btn-outline-dark:focus { - box-shadow: 0 0 0 0.25rem rgba(33, 37, 41, 0.5); } - .btn-check:checked + .btn-outline-dark, - .btn-check:active + .btn-outline-dark, .btn-outline-dark:active, .btn-outline-dark.active, .btn-outline-dark.dropdown-toggle.show { - color: #fff; - background-color: #212529; - border-color: #212529; } - .btn-check:checked + .btn-outline-dark:focus, - .btn-check:active + .btn-outline-dark:focus, .btn-outline-dark:active:focus, .btn-outline-dark.active:focus, .btn-outline-dark.dropdown-toggle.show:focus { - box-shadow: 0 0 0 0.25rem rgba(33, 37, 41, 0.5); } - .btn-outline-dark:disabled, .btn-outline-dark.disabled { - color: #212529; - background-color: transparent; } + border-color: #212529; +} +.btn-outline-dark:hover { + color: #fff; + background-color: #212529; + border-color: #212529; +} +.btn-check:focus + .btn-outline-dark, .btn-outline-dark:focus { + box-shadow: 0 0 0 0.25rem rgba(33, 37, 41, 0.5); +} +.btn-check:checked + .btn-outline-dark, .btn-check:active + .btn-outline-dark, .btn-outline-dark:active, .btn-outline-dark.active, .btn-outline-dark.dropdown-toggle.show { + color: #fff; + background-color: #212529; + border-color: #212529; +} +.btn-check:checked + .btn-outline-dark:focus, .btn-check:active + .btn-outline-dark:focus, .btn-outline-dark:active:focus, .btn-outline-dark.active:focus, .btn-outline-dark.dropdown-toggle.show:focus { + box-shadow: 0 0 0 0.25rem rgba(33, 37, 41, 0.5); +} +.btn-outline-dark:disabled, .btn-outline-dark.disabled { + color: #212529; + background-color: transparent; +} .btn-link { font-weight: 400; color: #fff; - text-decoration: underline; } - .btn-link:hover { - color: #cccccc; } - .btn-link:disabled, .btn-link.disabled { - color: #6c757d; } + text-decoration: underline; +} +.btn-link:hover { + color: #cccccc; +} +.btn-link:disabled, .btn-link.disabled { + color: #6c757d; +} .btn-lg { padding: 0.5rem 1rem; font-size: 1.25rem; - border-radius: 0.7rem; } + border-radius: 0.7rem; +} .btn-sm { padding: 0.25rem 0.5rem; font-size: 0.875rem; - border-radius: 0.6rem; } + border-radius: 0.6rem; +} diff --git a/webroot/css/themes/theme-slate.css b/webroot/css/themes/theme-slate.css index 6fc39ae..ff30587 100644 --- a/webroot/css/themes/theme-slate.css +++ b/webroot/css/themes/theme-slate.css @@ -1,319 +1,408 @@ /* Callout */ .callout { border: 1px solid #e9ecef; - border-radius: .25rem; + border-radius: 0.25rem; background-color: #363636; - box-shadow: none; } + box-shadow: none; +} .callout-primary { border-left-color: #3a3f44; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-secondary { border-left-color: #7a8288; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-success { border-left-color: #62c462; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-info { border-left-color: #5bc0de; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-warning { border-left-color: #f89406; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-danger { border-left-color: #ee5f5b; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-light { border-left-color: #e9ecef; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-dark { border-left-color: #272b30; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} /* Toasts */ .toast { - min-width: 250px; } + min-width: 250px; +} .toast-primary { color: #111314; background-color: #c4c5c7; - border-color: #b0b2b4; } - .toast-primary strong { - border-top-color: #a3a5a8; } + border-color: #b0b2b4; +} +.toast-primary strong { + border-top-color: #a3a5a8; +} .toast-secondary { color: #252729; background-color: #d7dadb; - border-color: #cacdcf; } - .toast-secondary strong { - border-top-color: #bdc0c3; } + border-color: #cacdcf; +} +.toast-secondary strong { + border-top-color: #bdc0c3; +} .toast-success { color: #1d3b1d; background-color: #d0edd0; - border-color: #c0e7c0; } - .toast-success strong { - border-top-color: #aee0ae; } + border-color: #c0e7c0; +} +.toast-success strong { + border-top-color: #aee0ae; +} .toast-info { color: #1b3a43; background-color: #ceecf5; - border-color: #bde6f2; } - .toast-info strong { - border-top-color: #a8deee; } + border-color: #bde6f2; +} +.toast-info strong { + border-top-color: #a8deee; +} .toast-warning { color: #4a2c02; background-color: #fddfb4; - border-color: #fcd49b; } - .toast-warning strong { - border-top-color: #fbc982; } + border-color: #fcd49b; +} +.toast-warning strong { + border-top-color: #fbc982; +} .toast-danger { color: #471d1b; background-color: #facfce; - border-color: #f8bfbd; } - .toast-danger strong { - border-top-color: #f6a9a6; } + border-color: #f8bfbd; +} +.toast-danger strong { + border-top-color: #f6a9a6; +} .toast-light { color: #464748; background-color: #f8f9fa; - border-color: #f6f7f9; } - .toast-light strong { - border-top-color: #e7e9ef; } + border-color: #f6f7f9; +} +.toast-light strong { + border-top-color: #e7e9ef; +} .toast-dark { color: #0c0d0e; background-color: #bebfc1; - border-color: #a9aaac; } - .toast-dark strong { - border-top-color: #9c9d9f; } + border-color: #a9aaac; +} +.toast-dark strong { + border-top-color: #9c9d9f; +} /* Dropdown-item */ .dropdown-item.dropdown-item-primary { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #3a3f44; } .dropdown-item.dropdown-item-primary:hover { color: #fff; background-color: #31363a; } +======= + background-color: #3a3f44; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-primary:hover { color: #fff; - background-color: #3a3f44; } - + background-color: #3a3f44; +} .dropdown-item.dropdown-item-secondary { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #7a8288; } .dropdown-item.dropdown-item-secondary:hover { color: #fff; background-color: #686f74; } +======= + background-color: #7a8288; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-secondary:hover { color: #fff; - background-color: #7a8288; } - + background-color: #7a8288; +} .dropdown-item.dropdown-item-success { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #62c462; } .dropdown-item.dropdown-item-success:hover { color: #fff; background-color: #53a753; } +======= + background-color: #62c462; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-success:hover { color: #fff; - background-color: #62c462; } - + background-color: #62c462; +} .dropdown-item.dropdown-item-info { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #5bc0de; } .dropdown-item.dropdown-item-info:hover { color: #fff; background-color: #4da3bd; } +======= + background-color: #5bc0de; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-info:hover { color: #fff; - background-color: #5bc0de; } - + background-color: #5bc0de; +} .dropdown-item.dropdown-item-warning { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #f89406; } .dropdown-item.dropdown-item-warning:hover { color: #fff; background-color: #d37e05; } +======= + background-color: #f89406; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-warning:hover { color: #fff; - background-color: #f89406; } - + background-color: #f89406; +} .dropdown-item.dropdown-item-danger { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #ee5f5b; } .dropdown-item.dropdown-item-danger:hover { color: #fff; background-color: #ca514d; } +======= + background-color: #ee5f5b; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-danger:hover { color: #fff; - background-color: #ee5f5b; } - + background-color: #ee5f5b; +} .dropdown-item.dropdown-item-light { color: #000; text-decoration: none; +<<<<<<< HEAD background-color: #e9ecef; } .dropdown-item.dropdown-item-light:hover { color: #000; background-color: #eceff1; } +======= + background-color: #e9ecef; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-light:hover { color: #000; - background-color: #e9ecef; } - + background-color: #e9ecef; +} .dropdown-item.dropdown-item-dark { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #272b30; } .dropdown-item.dropdown-item-dark:hover { color: #fff; background-color: #212529; } +======= + background-color: #272b30; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-dark:hover { color: #fff; - background-color: #272b30; } + background-color: #272b30; +} /* Progress Timeline */ .progress-timeline { - padding: 0.2em 0.2em 0.5em 0.2em; } - .progress-timeline ul { - position: relative; - padding: 0; } - .progress-timeline li { - list-style-type: none; - position: relative; } - .progress-timeline li.progress-inactive { - opacity: 0.5; } - .progress-timeline .progress-line { - height: 2px; } - .progress-timeline .progress-line.progress-inactive { - opacity: 0.5; } + padding: 0.2em 0.2em 0.5em 0.2em; +} +.progress-timeline ul { + position: relative; + padding: 0; +} +.progress-timeline li { + list-style-type: none; + position: relative; +} +.progress-timeline li.progress-inactive { + opacity: 0.5; +} +.progress-timeline .progress-line { + height: 2px; +} +.progress-timeline .progress-line.progress-inactive { + opacity: 0.5; +} /* Forms severity */ .form-control.is-invalid.info { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%235bc0de' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%235bc0de' stroke='none'/%3e%3c/svg%3e"); } - .form-control.is-invalid.info:focus { - border-color: #5bc0de; - box-shadow: 0 0 0 0.25rem rgba(91, 192, 222, 0.25); } - + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%235bc0de' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%235bc0de' stroke='none'/%3e%3c/svg%3e"); +} +.form-control.is-invalid.info:focus { + border-color: #5bc0de; + box-shadow: 0 0 0 0.25rem rgba(91, 192, 222, 0.25); +} .form-control.is-invalid.warning { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23f89406' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23f89406' stroke='none'/%3e%3c/svg%3e"); } - .form-control.is-invalid.warning:focus { - border-color: #f89406; - box-shadow: 0 0 0 0.25rem rgba(248, 148, 6, 0.25); } + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23f89406' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23f89406' stroke='none'/%3e%3c/svg%3e"); +} +.form-control.is-invalid.warning:focus { + border-color: #f89406; + box-shadow: 0 0 0 0.25rem rgba(248, 148, 6, 0.25); +} .form-select.is-invalid:not([multiple]):not([size]).info, .form-select.is-invalid:not([multiple])[size="1"] .form-select.is-invalid.info { background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%235bc0de'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%235bc0de' stroke='none'/%3e%3c/svg%3e"); - background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); } - .form-select.is-invalid:not([multiple]):not([size]).info:focus, - .form-select.is-invalid:not([multiple])[size="1"] .form-select.is-invalid.info:focus { - box-shadow: 0 0 0 0.25rem rgba(91, 192, 222, 0.25); } - + background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} +.form-select.is-invalid:not([multiple]):not([size]).info:focus, +.form-select.is-invalid:not([multiple])[size="1"] .form-select.is-invalid.info:focus { + box-shadow: 0 0 0 0.25rem rgba(91, 192, 222, 0.25); +} .form-select.is-invalid:not([multiple]):not([size]).warning, .form-select.is-invalid:not([multiple])[size="1"] .form-select.is-invalid.warning { background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23f89406'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23f89406' stroke='none'/%3e%3c/svg%3e"); - background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); } - .form-select.is-invalid:not([multiple]):not([size]).warning:focus, - .form-select.is-invalid:not([multiple])[size="1"] .form-select.is-invalid.warning:focus { - box-shadow: 0 0 0 0.25rem rgba(248, 148, 6, 0.25); } + background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} +.form-select.is-invalid:not([multiple]):not([size]).warning:focus, +.form-select.is-invalid:not([multiple])[size="1"] .form-select.is-invalid.warning:focus { + box-shadow: 0 0 0 0.25rem rgba(248, 148, 6, 0.25); +} .form-check-input.is-invalid.info { - border-color: #5bc0de; } - + border-color: #5bc0de; +} .form-check-input.is-invalid.info:checked { - background-color: #5bc0de; } - + background-color: #5bc0de; +} .form-check-input.is-invalid.info ~ .form-check-label { - color: unset; } - + color: unset; +} .form-check-input.is-invalid.info:focus { - box-shadow: 0 0 0 0.2rem rgba(91, 192, 222, 0.25); } - + box-shadow: 0 0 0 0.2rem rgba(91, 192, 222, 0.25); +} .form-check-input.is-invalid.warning { - border-color: #f89406; } - + border-color: #f89406; +} .form-check-input.is-invalid.warning:checked { - background-color: #f89406; } - + background-color: #f89406; +} .form-check-input.is-invalid.warning ~ .form-check-label { - color: unset; } - + color: unset; +} .form-check-input.is-invalid.warning:focus { - box-shadow: 0 0 0 0.2rem rgba(248, 148, 6, 0.25); } + box-shadow: 0 0 0 0.2rem rgba(248, 148, 6, 0.25); +} /* Utilities */ .mw-75 { - max-width: 75% !important; } + max-width: 75% !important; +} .mw-66 { max-width: 66% !important; } .mw-50 { - max-width: 50% !important; } + max-width: 50% !important; +} .mw-33 { max-width: 33% !important; } .mw-25 { - max-width: 25% !important; } + max-width: 25% !important; +} .mh-75 { - max-height: 75% !important; } + max-height: 75% !important; +} .mh-66 { max-height: 66% !important; } .mh-50 { - max-height: 50% !important; } + max-height: 50% !important; +} .mh-33 { max-height: 33% !important; } .mh-25 { - max-height: 25% !important; } + max-height: 25% !important; +} .p-abs-center-y { top: 50%; - transform: translateY(-50%); } + transform: translateY(-50%); +} .p-abs-center-x { left: 50%; - transform: translateX(-50%); } + transform: translateX(-50%); +} .p-abs-center-both { top: 50%; left: 50%; - transform: translateX(-50%) translateY(-50%); } + transform: translateX(-50%) translateY(-50%); +} .fs-7 { font-size: .875rem !important; } @@ -376,93 +465,117 @@ body { /* background by SVGBackgrounds.com */ background-attachment: fixed; background-size: cover; - background-blend-mode: normal; } + background-blend-mode: normal; +} .panel { background-color: #363636; border: 1px solid #454545; - box-shadow: none; } + box-shadow: none; +} .loading-overlay { background-color: #272b30; - opacity: 0.65; } + opacity: 0.65; +} /* Top navbar */ .top-navbar { - background-color: #3a3f44; } + background-color: #3a3f44; +} .center-navbar nav.header-breadcrumb { - color: #fff; } + color: #fff; +} header.top-navbar .header-menu > a:hover, header.top-navbar .header-breadcrumb .header-breadcrumb-item > a:hover { - color: #d6d6d6 !important; } + color: #d6d6d6 !important; +} .top-navbar .center-navbar nav.header-breadcrumb li.header-breadcrumb-item a { - color: #fff; } + color: #fff; +} .top-navbar .right-navbar .header-menu a.nav-link { - color: #fff; } + color: #fff; +} .top-navbar .left-navbar .navbar-brand img { - filter: invert(1); } + filter: invert(1); +} .top-navbar .left-navbar .navbar-brand:hover img { - filter: invert(1) drop-shadow(0px 0px 3px #fff); } + filter: invert(1) drop-shadow(0px 0px 3px #fff); +} .top-navbar .composed-app-icon-container > .app-icon { - background-color: #fff; } + background-color: #fff; +} .breadcrumb-link-container { box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 2px 6px 0 rgba(0, 0, 0, 0.12); - background-color: #e9ecef; } + background-color: #e9ecef; +} /* Sidebar */ .sidebar { - transition: width .08s linear; + transition: width 0.08s linear; box-shadow: none; - background-color: #7a8288; } + background-color: #7a8288; +} .sidebar ~ main.content:after { - background: #000; } + background: #000; +} .sidebar .sidebar-wrapper { - border-right: 1px solid none; } + border-right: 1px solid none; +} .sidebar .sidebar-wrapper { - border-right: 1px solid rgba(0, 0, 0, 0.125); } + border-right: 1px solid rgba(0, 0, 0, 0.125); +} .sidebar ul.sidebar-elements li > a.sidebar-link { - color: #fff; } + color: #fff; +} .sidebar ul.sidebar-elements li > a.sidebar-link.active { background-color: #595f64; - color: #fff; } + color: #fff; +} .sidebar ul.sidebar-elements li > a.sidebar-link.have-active-child { background-color: #595f64; - color: #fff; } + color: #fff; +} .sidebar ul.sidebar-elements li > a.sidebar-link.have-active-child::after { background-color: var(--cerebrate-color); } .sidebar ul.sidebar-elements li > a.sidebar-link:hover { background-color: #60676c; - color: #fff; } + color: #fff; +} .sidebar.expanded ul.sidebar-elements li > a.sidebar-link.have-active-child, .sidebar:hover ul.sidebar-elements li > a.sidebar-link.have-active-child { - background-color: unset; } + background-color: unset; +} .sidebar.expanded ul.sidebar-elements li > a.sidebar-link.have-active-child:hover, .sidebar:hover ul.sidebar-elements li > a.sidebar-link.have-active-child:hover { - background-color: #60676c; } + background-color: #60676c; +} ul.sidebar-elements li > a.sidebar-link.active::after { - background-color: var(--cerebrate-color); } + background-color: var(--cerebrate-color); +} .lock-sidebar > a.btn { - background-color: #7a8288; } + background-color: #7a8288; +} .btn { display: inline-block; @@ -479,455 +592,518 @@ ul.sidebar-elements li > a.sidebar-link.active::after { padding: 0.375rem 0.75rem; font-size: 1rem; border-radius: 0.25rem; - transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; } - @media (prefers-reduced-motion: reduce) { - .btn { - transition: none; } } - .btn:hover { - color: #aaa; } - .btn-check:focus + .btn, .btn:focus { - outline: 0; - box-shadow: 0 0 0 0.25rem rgba(58, 63, 68, 0.25); } - .btn:disabled, .btn.disabled, - fieldset:disabled .btn { - pointer-events: none; - opacity: 0.65; } + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .btn { + transition: none; + } +} +.btn:hover { + color: #aaa; +} +.btn-check:focus + .btn, .btn:focus { + outline: 0; + box-shadow: 0 0 0 0.25rem rgba(58, 63, 68, 0.25); +} +.btn:disabled, .btn.disabled, fieldset:disabled .btn { + pointer-events: none; + opacity: 0.65; +} .btn-primary { color: #fff; background-color: #3a3f44; - border-color: #3a3f44; } - .btn-primary:hover { - color: #fff; - background-color: #31363a; - border-color: #2e3236; } - .btn-check:focus + .btn-primary, .btn-primary:focus { - color: #fff; - background-color: #31363a; - border-color: #2e3236; - box-shadow: 0 0 0 0.25rem rgba(88, 92, 96, 0.5); } - .btn-check:checked + .btn-primary, - .btn-check:active + .btn-primary, .btn-primary:active, .btn-primary.active, - .show > .btn-primary.dropdown-toggle { - color: #fff; - background-color: #2e3236; - border-color: #2c2f33; } - .btn-check:checked + .btn-primary:focus, - .btn-check:active + .btn-primary:focus, .btn-primary:active:focus, .btn-primary.active:focus, - .show > .btn-primary.dropdown-toggle:focus { - box-shadow: 0 0 0 0.25rem rgba(88, 92, 96, 0.5); } - .btn-primary:disabled, .btn-primary.disabled { - color: #fff; - background-color: #3a3f44; - border-color: #3a3f44; } + border-color: #3a3f44; +} +.btn-primary:hover { + color: #fff; + background-color: #31363a; + border-color: #2e3236; +} +.btn-check:focus + .btn-primary, .btn-primary:focus { + color: #fff; + background-color: #31363a; + border-color: #2e3236; + box-shadow: 0 0 0 0.25rem rgba(88, 92, 96, 0.5); +} +.btn-check:checked + .btn-primary, .btn-check:active + .btn-primary, .btn-primary:active, .btn-primary.active, .show > .btn-primary.dropdown-toggle { + color: #fff; + background-color: #2e3236; + border-color: #2c2f33; +} +.btn-check:checked + .btn-primary:focus, .btn-check:active + .btn-primary:focus, .btn-primary:active:focus, .btn-primary.active:focus, .show > .btn-primary.dropdown-toggle:focus { + box-shadow: 0 0 0 0.25rem rgba(88, 92, 96, 0.5); +} +.btn-primary:disabled, .btn-primary.disabled { + color: #fff; + background-color: #3a3f44; + border-color: #3a3f44; +} .btn-secondary { color: #fff; background-color: #7a8288; - border-color: #7a8288; } - .btn-secondary:hover { - color: #fff; - background-color: #686f74; - border-color: #62686d; } - .btn-check:focus + .btn-secondary, .btn-secondary:focus { - color: #fff; - background-color: #686f74; - border-color: #62686d; - box-shadow: 0 0 0 0.25rem rgba(142, 149, 154, 0.5); } - .btn-check:checked + .btn-secondary, - .btn-check:active + .btn-secondary, .btn-secondary:active, .btn-secondary.active, - .show > .btn-secondary.dropdown-toggle { - color: #fff; - background-color: #62686d; - border-color: #5c6266; } - .btn-check:checked + .btn-secondary:focus, - .btn-check:active + .btn-secondary:focus, .btn-secondary:active:focus, .btn-secondary.active:focus, - .show > .btn-secondary.dropdown-toggle:focus { - box-shadow: 0 0 0 0.25rem rgba(142, 149, 154, 0.5); } - .btn-secondary:disabled, .btn-secondary.disabled { - color: #fff; - background-color: #7a8288; - border-color: #7a8288; } + border-color: #7a8288; +} +.btn-secondary:hover { + color: #fff; + background-color: #686f74; + border-color: #62686d; +} +.btn-check:focus + .btn-secondary, .btn-secondary:focus { + color: #fff; + background-color: #686f74; + border-color: #62686d; + box-shadow: 0 0 0 0.25rem rgba(142, 149, 154, 0.5); +} +.btn-check:checked + .btn-secondary, .btn-check:active + .btn-secondary, .btn-secondary:active, .btn-secondary.active, .show > .btn-secondary.dropdown-toggle { + color: #fff; + background-color: #62686d; + border-color: #5c6266; +} +.btn-check:checked + .btn-secondary:focus, .btn-check:active + .btn-secondary:focus, .btn-secondary:active:focus, .btn-secondary.active:focus, .show > .btn-secondary.dropdown-toggle:focus { + box-shadow: 0 0 0 0.25rem rgba(142, 149, 154, 0.5); +} +.btn-secondary:disabled, .btn-secondary.disabled { + color: #fff; + background-color: #7a8288; + border-color: #7a8288; +} .btn-success { color: #fff; background-color: #62c462; - border-color: #62c462; } - .btn-success:hover { - color: #fff; - background-color: #53a753; - border-color: #4e9d4e; } - .btn-check:focus + .btn-success, .btn-success:focus { - color: #fff; - background-color: #53a753; - border-color: #4e9d4e; - box-shadow: 0 0 0 0.25rem rgba(122, 205, 122, 0.5); } - .btn-check:checked + .btn-success, - .btn-check:active + .btn-success, .btn-success:active, .btn-success.active, - .show > .btn-success.dropdown-toggle { - color: #fff; - background-color: #4e9d4e; - border-color: #4a934a; } - .btn-check:checked + .btn-success:focus, - .btn-check:active + .btn-success:focus, .btn-success:active:focus, .btn-success.active:focus, - .show > .btn-success.dropdown-toggle:focus { - box-shadow: 0 0 0 0.25rem rgba(122, 205, 122, 0.5); } - .btn-success:disabled, .btn-success.disabled { - color: #fff; - background-color: #62c462; - border-color: #62c462; } + border-color: #62c462; +} +.btn-success:hover { + color: #fff; + background-color: #53a753; + border-color: #4e9d4e; +} +.btn-check:focus + .btn-success, .btn-success:focus { + color: #fff; + background-color: #53a753; + border-color: #4e9d4e; + box-shadow: 0 0 0 0.25rem rgba(122, 205, 122, 0.5); +} +.btn-check:checked + .btn-success, .btn-check:active + .btn-success, .btn-success:active, .btn-success.active, .show > .btn-success.dropdown-toggle { + color: #fff; + background-color: #4e9d4e; + border-color: #4a934a; +} +.btn-check:checked + .btn-success:focus, .btn-check:active + .btn-success:focus, .btn-success:active:focus, .btn-success.active:focus, .show > .btn-success.dropdown-toggle:focus { + box-shadow: 0 0 0 0.25rem rgba(122, 205, 122, 0.5); +} +.btn-success:disabled, .btn-success.disabled { + color: #fff; + background-color: #62c462; + border-color: #62c462; +} .btn-info { color: #fff; background-color: #5bc0de; - border-color: #5bc0de; } - .btn-info:hover { - color: #fff; - background-color: #4da3bd; - border-color: #499ab2; } - .btn-check:focus + .btn-info, .btn-info:focus { - color: #fff; - background-color: #4da3bd; - border-color: #499ab2; - box-shadow: 0 0 0 0.25rem rgba(116, 201, 227, 0.5); } - .btn-check:checked + .btn-info, - .btn-check:active + .btn-info, .btn-info:active, .btn-info.active, - .show > .btn-info.dropdown-toggle { - color: #fff; - background-color: #499ab2; - border-color: #4490a7; } - .btn-check:checked + .btn-info:focus, - .btn-check:active + .btn-info:focus, .btn-info:active:focus, .btn-info.active:focus, - .show > .btn-info.dropdown-toggle:focus { - box-shadow: 0 0 0 0.25rem rgba(116, 201, 227, 0.5); } - .btn-info:disabled, .btn-info.disabled { - color: #fff; - background-color: #5bc0de; - border-color: #5bc0de; } + border-color: #5bc0de; +} +.btn-info:hover { + color: #fff; + background-color: #4da3bd; + border-color: #499ab2; +} +.btn-check:focus + .btn-info, .btn-info:focus { + color: #fff; + background-color: #4da3bd; + border-color: #499ab2; + box-shadow: 0 0 0 0.25rem rgba(116, 201, 227, 0.5); +} +.btn-check:checked + .btn-info, .btn-check:active + .btn-info, .btn-info:active, .btn-info.active, .show > .btn-info.dropdown-toggle { + color: #fff; + background-color: #499ab2; + border-color: #4490a7; +} +.btn-check:checked + .btn-info:focus, .btn-check:active + .btn-info:focus, .btn-info:active:focus, .btn-info.active:focus, .show > .btn-info.dropdown-toggle:focus { + box-shadow: 0 0 0 0.25rem rgba(116, 201, 227, 0.5); +} +.btn-info:disabled, .btn-info.disabled { + color: #fff; + background-color: #5bc0de; + border-color: #5bc0de; +} .btn-warning { color: #fff; background-color: #f89406; - border-color: #f89406; } - .btn-warning:hover { - color: #fff; - background-color: #d37e05; - border-color: #c67605; } - .btn-check:focus + .btn-warning, .btn-warning:focus { - color: #fff; - background-color: #d37e05; - border-color: #c67605; - box-shadow: 0 0 0 0.25rem rgba(249, 164, 43, 0.5); } - .btn-check:checked + .btn-warning, - .btn-check:active + .btn-warning, .btn-warning:active, .btn-warning.active, - .show > .btn-warning.dropdown-toggle { - color: #fff; - background-color: #c67605; - border-color: #ba6f05; } - .btn-check:checked + .btn-warning:focus, - .btn-check:active + .btn-warning:focus, .btn-warning:active:focus, .btn-warning.active:focus, - .show > .btn-warning.dropdown-toggle:focus { - box-shadow: 0 0 0 0.25rem rgba(249, 164, 43, 0.5); } - .btn-warning:disabled, .btn-warning.disabled { - color: #fff; - background-color: #f89406; - border-color: #f89406; } + border-color: #f89406; +} +.btn-warning:hover { + color: #fff; + background-color: #d37e05; + border-color: #c67605; +} +.btn-check:focus + .btn-warning, .btn-warning:focus { + color: #fff; + background-color: #d37e05; + border-color: #c67605; + box-shadow: 0 0 0 0.25rem rgba(249, 164, 43, 0.5); +} +.btn-check:checked + .btn-warning, .btn-check:active + .btn-warning, .btn-warning:active, .btn-warning.active, .show > .btn-warning.dropdown-toggle { + color: #fff; + background-color: #c67605; + border-color: #ba6f05; +} +.btn-check:checked + .btn-warning:focus, .btn-check:active + .btn-warning:focus, .btn-warning:active:focus, .btn-warning.active:focus, .show > .btn-warning.dropdown-toggle:focus { + box-shadow: 0 0 0 0.25rem rgba(249, 164, 43, 0.5); +} +.btn-warning:disabled, .btn-warning.disabled { + color: #fff; + background-color: #f89406; + border-color: #f89406; +} .btn-danger { color: #fff; background-color: #ee5f5b; - border-color: #ee5f5b; } - .btn-danger:hover { - color: #fff; - background-color: #ca514d; - border-color: #be4c49; } - .btn-check:focus + .btn-danger, .btn-danger:focus { - color: #fff; - background-color: #ca514d; - border-color: #be4c49; - box-shadow: 0 0 0 0.25rem rgba(241, 119, 116, 0.5); } - .btn-check:checked + .btn-danger, - .btn-check:active + .btn-danger, .btn-danger:active, .btn-danger.active, - .show > .btn-danger.dropdown-toggle { - color: #fff; - background-color: #be4c49; - border-color: #b34744; } - .btn-check:checked + .btn-danger:focus, - .btn-check:active + .btn-danger:focus, .btn-danger:active:focus, .btn-danger.active:focus, - .show > .btn-danger.dropdown-toggle:focus { - box-shadow: 0 0 0 0.25rem rgba(241, 119, 116, 0.5); } - .btn-danger:disabled, .btn-danger.disabled { - color: #fff; - background-color: #ee5f5b; - border-color: #ee5f5b; } + border-color: #ee5f5b; +} +.btn-danger:hover { + color: #fff; + background-color: #ca514d; + border-color: #be4c49; +} +.btn-check:focus + .btn-danger, .btn-danger:focus { + color: #fff; + background-color: #ca514d; + border-color: #be4c49; + box-shadow: 0 0 0 0.25rem rgba(241, 119, 116, 0.5); +} +.btn-check:checked + .btn-danger, .btn-check:active + .btn-danger, .btn-danger:active, .btn-danger.active, .show > .btn-danger.dropdown-toggle { + color: #fff; + background-color: #be4c49; + border-color: #b34744; +} +.btn-check:checked + .btn-danger:focus, .btn-check:active + .btn-danger:focus, .btn-danger:active:focus, .btn-danger.active:focus, .show > .btn-danger.dropdown-toggle:focus { + box-shadow: 0 0 0 0.25rem rgba(241, 119, 116, 0.5); +} +.btn-danger:disabled, .btn-danger.disabled { + color: #fff; + background-color: #ee5f5b; + border-color: #ee5f5b; +} .btn-light { color: #000; background-color: #e9ecef; - border-color: #e9ecef; } - .btn-light:hover { - color: #000; - background-color: #eceff1; - border-color: #ebeef1; } - .btn-check:focus + .btn-light, .btn-light:focus { - color: #000; - background-color: #eceff1; - border-color: #ebeef1; - box-shadow: 0 0 0 0.25rem rgba(198, 201, 203, 0.5); } - .btn-check:checked + .btn-light, - .btn-check:active + .btn-light, .btn-light:active, .btn-light.active, - .show > .btn-light.dropdown-toggle { - color: #000; - background-color: #edf0f2; - border-color: #ebeef1; } - .btn-check:checked + .btn-light:focus, - .btn-check:active + .btn-light:focus, .btn-light:active:focus, .btn-light.active:focus, - .show > .btn-light.dropdown-toggle:focus { - box-shadow: 0 0 0 0.25rem rgba(198, 201, 203, 0.5); } - .btn-light:disabled, .btn-light.disabled { - color: #000; - background-color: #e9ecef; - border-color: #e9ecef; } + border-color: #e9ecef; +} +.btn-light:hover { + color: #000; + background-color: #eceff1; + border-color: #ebeef1; +} +.btn-check:focus + .btn-light, .btn-light:focus { + color: #000; + background-color: #eceff1; + border-color: #ebeef1; + box-shadow: 0 0 0 0.25rem rgba(198, 201, 203, 0.5); +} +.btn-check:checked + .btn-light, .btn-check:active + .btn-light, .btn-light:active, .btn-light.active, .show > .btn-light.dropdown-toggle { + color: #000; + background-color: #edf0f2; + border-color: #ebeef1; +} +.btn-check:checked + .btn-light:focus, .btn-check:active + .btn-light:focus, .btn-light:active:focus, .btn-light.active:focus, .show > .btn-light.dropdown-toggle:focus { + box-shadow: 0 0 0 0.25rem rgba(198, 201, 203, 0.5); +} +.btn-light:disabled, .btn-light.disabled { + color: #000; + background-color: #e9ecef; + border-color: #e9ecef; +} .btn-dark { color: #fff; background-color: #272b30; - border-color: #272b30; } - .btn-dark:hover { - color: #fff; - background-color: #212529; - border-color: #1f2226; } - .btn-check:focus + .btn-dark, .btn-dark:focus { - color: #fff; - background-color: #212529; - border-color: #1f2226; - box-shadow: 0 0 0 0.25rem rgba(71, 75, 79, 0.5); } - .btn-check:checked + .btn-dark, - .btn-check:active + .btn-dark, .btn-dark:active, .btn-dark.active, - .show > .btn-dark.dropdown-toggle { - color: #fff; - background-color: #1f2226; - border-color: #1d2024; } - .btn-check:checked + .btn-dark:focus, - .btn-check:active + .btn-dark:focus, .btn-dark:active:focus, .btn-dark.active:focus, - .show > .btn-dark.dropdown-toggle:focus { - box-shadow: 0 0 0 0.25rem rgba(71, 75, 79, 0.5); } - .btn-dark:disabled, .btn-dark.disabled { - color: #fff; - background-color: #272b30; - border-color: #272b30; } + border-color: #272b30; +} +.btn-dark:hover { + color: #fff; + background-color: #212529; + border-color: #1f2226; +} +.btn-check:focus + .btn-dark, .btn-dark:focus { + color: #fff; + background-color: #212529; + border-color: #1f2226; + box-shadow: 0 0 0 0.25rem rgba(71, 75, 79, 0.5); +} +.btn-check:checked + .btn-dark, .btn-check:active + .btn-dark, .btn-dark:active, .btn-dark.active, .show > .btn-dark.dropdown-toggle { + color: #fff; + background-color: #1f2226; + border-color: #1d2024; +} +.btn-check:checked + .btn-dark:focus, .btn-check:active + .btn-dark:focus, .btn-dark:active:focus, .btn-dark.active:focus, .show > .btn-dark.dropdown-toggle:focus { + box-shadow: 0 0 0 0.25rem rgba(71, 75, 79, 0.5); +} +.btn-dark:disabled, .btn-dark.disabled { + color: #fff; + background-color: #272b30; + border-color: #272b30; +} .btn-outline-primary { color: #3a3f44; - border-color: #3a3f44; } - .btn-outline-primary:hover { - color: #fff; - background-color: #3a3f44; - border-color: #3a3f44; } - .btn-check:focus + .btn-outline-primary, .btn-outline-primary:focus { - box-shadow: 0 0 0 0.25rem rgba(58, 63, 68, 0.5); } - .btn-check:checked + .btn-outline-primary, - .btn-check:active + .btn-outline-primary, .btn-outline-primary:active, .btn-outline-primary.active, .btn-outline-primary.dropdown-toggle.show { - color: #fff; - background-color: #3a3f44; - border-color: #3a3f44; } - .btn-check:checked + .btn-outline-primary:focus, - .btn-check:active + .btn-outline-primary:focus, .btn-outline-primary:active:focus, .btn-outline-primary.active:focus, .btn-outline-primary.dropdown-toggle.show:focus { - box-shadow: 0 0 0 0.25rem rgba(58, 63, 68, 0.5); } - .btn-outline-primary:disabled, .btn-outline-primary.disabled { - color: #3a3f44; - background-color: transparent; } + border-color: #3a3f44; +} +.btn-outline-primary:hover { + color: #fff; + background-color: #3a3f44; + border-color: #3a3f44; +} +.btn-check:focus + .btn-outline-primary, .btn-outline-primary:focus { + box-shadow: 0 0 0 0.25rem rgba(58, 63, 68, 0.5); +} +.btn-check:checked + .btn-outline-primary, .btn-check:active + .btn-outline-primary, .btn-outline-primary:active, .btn-outline-primary.active, .btn-outline-primary.dropdown-toggle.show { + color: #fff; + background-color: #3a3f44; + border-color: #3a3f44; +} +.btn-check:checked + .btn-outline-primary:focus, .btn-check:active + .btn-outline-primary:focus, .btn-outline-primary:active:focus, .btn-outline-primary.active:focus, .btn-outline-primary.dropdown-toggle.show:focus { + box-shadow: 0 0 0 0.25rem rgba(58, 63, 68, 0.5); +} +.btn-outline-primary:disabled, .btn-outline-primary.disabled { + color: #3a3f44; + background-color: transparent; +} .btn-outline-secondary { color: #7a8288; - border-color: #7a8288; } - .btn-outline-secondary:hover { - color: #fff; - background-color: #7a8288; - border-color: #7a8288; } - .btn-check:focus + .btn-outline-secondary, .btn-outline-secondary:focus { - box-shadow: 0 0 0 0.25rem rgba(122, 130, 136, 0.5); } - .btn-check:checked + .btn-outline-secondary, - .btn-check:active + .btn-outline-secondary, .btn-outline-secondary:active, .btn-outline-secondary.active, .btn-outline-secondary.dropdown-toggle.show { - color: #fff; - background-color: #7a8288; - border-color: #7a8288; } - .btn-check:checked + .btn-outline-secondary:focus, - .btn-check:active + .btn-outline-secondary:focus, .btn-outline-secondary:active:focus, .btn-outline-secondary.active:focus, .btn-outline-secondary.dropdown-toggle.show:focus { - box-shadow: 0 0 0 0.25rem rgba(122, 130, 136, 0.5); } - .btn-outline-secondary:disabled, .btn-outline-secondary.disabled { - color: #7a8288; - background-color: transparent; } + border-color: #7a8288; +} +.btn-outline-secondary:hover { + color: #fff; + background-color: #7a8288; + border-color: #7a8288; +} +.btn-check:focus + .btn-outline-secondary, .btn-outline-secondary:focus { + box-shadow: 0 0 0 0.25rem rgba(122, 130, 136, 0.5); +} +.btn-check:checked + .btn-outline-secondary, .btn-check:active + .btn-outline-secondary, .btn-outline-secondary:active, .btn-outline-secondary.active, .btn-outline-secondary.dropdown-toggle.show { + color: #fff; + background-color: #7a8288; + border-color: #7a8288; +} +.btn-check:checked + .btn-outline-secondary:focus, .btn-check:active + .btn-outline-secondary:focus, .btn-outline-secondary:active:focus, .btn-outline-secondary.active:focus, .btn-outline-secondary.dropdown-toggle.show:focus { + box-shadow: 0 0 0 0.25rem rgba(122, 130, 136, 0.5); +} +.btn-outline-secondary:disabled, .btn-outline-secondary.disabled { + color: #7a8288; + background-color: transparent; +} .btn-outline-success { color: #62c462; - border-color: #62c462; } - .btn-outline-success:hover { - color: #fff; - background-color: #62c462; - border-color: #62c462; } - .btn-check:focus + .btn-outline-success, .btn-outline-success:focus { - box-shadow: 0 0 0 0.25rem rgba(98, 196, 98, 0.5); } - .btn-check:checked + .btn-outline-success, - .btn-check:active + .btn-outline-success, .btn-outline-success:active, .btn-outline-success.active, .btn-outline-success.dropdown-toggle.show { - color: #fff; - background-color: #62c462; - border-color: #62c462; } - .btn-check:checked + .btn-outline-success:focus, - .btn-check:active + .btn-outline-success:focus, .btn-outline-success:active:focus, .btn-outline-success.active:focus, .btn-outline-success.dropdown-toggle.show:focus { - box-shadow: 0 0 0 0.25rem rgba(98, 196, 98, 0.5); } - .btn-outline-success:disabled, .btn-outline-success.disabled { - color: #62c462; - background-color: transparent; } + border-color: #62c462; +} +.btn-outline-success:hover { + color: #fff; + background-color: #62c462; + border-color: #62c462; +} +.btn-check:focus + .btn-outline-success, .btn-outline-success:focus { + box-shadow: 0 0 0 0.25rem rgba(98, 196, 98, 0.5); +} +.btn-check:checked + .btn-outline-success, .btn-check:active + .btn-outline-success, .btn-outline-success:active, .btn-outline-success.active, .btn-outline-success.dropdown-toggle.show { + color: #fff; + background-color: #62c462; + border-color: #62c462; +} +.btn-check:checked + .btn-outline-success:focus, .btn-check:active + .btn-outline-success:focus, .btn-outline-success:active:focus, .btn-outline-success.active:focus, .btn-outline-success.dropdown-toggle.show:focus { + box-shadow: 0 0 0 0.25rem rgba(98, 196, 98, 0.5); +} +.btn-outline-success:disabled, .btn-outline-success.disabled { + color: #62c462; + background-color: transparent; +} .btn-outline-info { color: #5bc0de; - border-color: #5bc0de; } - .btn-outline-info:hover { - color: #fff; - background-color: #5bc0de; - border-color: #5bc0de; } - .btn-check:focus + .btn-outline-info, .btn-outline-info:focus { - box-shadow: 0 0 0 0.25rem rgba(91, 192, 222, 0.5); } - .btn-check:checked + .btn-outline-info, - .btn-check:active + .btn-outline-info, .btn-outline-info:active, .btn-outline-info.active, .btn-outline-info.dropdown-toggle.show { - color: #fff; - background-color: #5bc0de; - border-color: #5bc0de; } - .btn-check:checked + .btn-outline-info:focus, - .btn-check:active + .btn-outline-info:focus, .btn-outline-info:active:focus, .btn-outline-info.active:focus, .btn-outline-info.dropdown-toggle.show:focus { - box-shadow: 0 0 0 0.25rem rgba(91, 192, 222, 0.5); } - .btn-outline-info:disabled, .btn-outline-info.disabled { - color: #5bc0de; - background-color: transparent; } + border-color: #5bc0de; +} +.btn-outline-info:hover { + color: #fff; + background-color: #5bc0de; + border-color: #5bc0de; +} +.btn-check:focus + .btn-outline-info, .btn-outline-info:focus { + box-shadow: 0 0 0 0.25rem rgba(91, 192, 222, 0.5); +} +.btn-check:checked + .btn-outline-info, .btn-check:active + .btn-outline-info, .btn-outline-info:active, .btn-outline-info.active, .btn-outline-info.dropdown-toggle.show { + color: #fff; + background-color: #5bc0de; + border-color: #5bc0de; +} +.btn-check:checked + .btn-outline-info:focus, .btn-check:active + .btn-outline-info:focus, .btn-outline-info:active:focus, .btn-outline-info.active:focus, .btn-outline-info.dropdown-toggle.show:focus { + box-shadow: 0 0 0 0.25rem rgba(91, 192, 222, 0.5); +} +.btn-outline-info:disabled, .btn-outline-info.disabled { + color: #5bc0de; + background-color: transparent; +} .btn-outline-warning { color: #f89406; - border-color: #f89406; } - .btn-outline-warning:hover { - color: #fff; - background-color: #f89406; - border-color: #f89406; } - .btn-check:focus + .btn-outline-warning, .btn-outline-warning:focus { - box-shadow: 0 0 0 0.25rem rgba(248, 148, 6, 0.5); } - .btn-check:checked + .btn-outline-warning, - .btn-check:active + .btn-outline-warning, .btn-outline-warning:active, .btn-outline-warning.active, .btn-outline-warning.dropdown-toggle.show { - color: #fff; - background-color: #f89406; - border-color: #f89406; } - .btn-check:checked + .btn-outline-warning:focus, - .btn-check:active + .btn-outline-warning:focus, .btn-outline-warning:active:focus, .btn-outline-warning.active:focus, .btn-outline-warning.dropdown-toggle.show:focus { - box-shadow: 0 0 0 0.25rem rgba(248, 148, 6, 0.5); } - .btn-outline-warning:disabled, .btn-outline-warning.disabled { - color: #f89406; - background-color: transparent; } + border-color: #f89406; +} +.btn-outline-warning:hover { + color: #fff; + background-color: #f89406; + border-color: #f89406; +} +.btn-check:focus + .btn-outline-warning, .btn-outline-warning:focus { + box-shadow: 0 0 0 0.25rem rgba(248, 148, 6, 0.5); +} +.btn-check:checked + .btn-outline-warning, .btn-check:active + .btn-outline-warning, .btn-outline-warning:active, .btn-outline-warning.active, .btn-outline-warning.dropdown-toggle.show { + color: #fff; + background-color: #f89406; + border-color: #f89406; +} +.btn-check:checked + .btn-outline-warning:focus, .btn-check:active + .btn-outline-warning:focus, .btn-outline-warning:active:focus, .btn-outline-warning.active:focus, .btn-outline-warning.dropdown-toggle.show:focus { + box-shadow: 0 0 0 0.25rem rgba(248, 148, 6, 0.5); +} +.btn-outline-warning:disabled, .btn-outline-warning.disabled { + color: #f89406; + background-color: transparent; +} .btn-outline-danger { color: #ee5f5b; - border-color: #ee5f5b; } - .btn-outline-danger:hover { - color: #fff; - background-color: #ee5f5b; - border-color: #ee5f5b; } - .btn-check:focus + .btn-outline-danger, .btn-outline-danger:focus { - box-shadow: 0 0 0 0.25rem rgba(238, 95, 91, 0.5); } - .btn-check:checked + .btn-outline-danger, - .btn-check:active + .btn-outline-danger, .btn-outline-danger:active, .btn-outline-danger.active, .btn-outline-danger.dropdown-toggle.show { - color: #fff; - background-color: #ee5f5b; - border-color: #ee5f5b; } - .btn-check:checked + .btn-outline-danger:focus, - .btn-check:active + .btn-outline-danger:focus, .btn-outline-danger:active:focus, .btn-outline-danger.active:focus, .btn-outline-danger.dropdown-toggle.show:focus { - box-shadow: 0 0 0 0.25rem rgba(238, 95, 91, 0.5); } - .btn-outline-danger:disabled, .btn-outline-danger.disabled { - color: #ee5f5b; - background-color: transparent; } + border-color: #ee5f5b; +} +.btn-outline-danger:hover { + color: #fff; + background-color: #ee5f5b; + border-color: #ee5f5b; +} +.btn-check:focus + .btn-outline-danger, .btn-outline-danger:focus { + box-shadow: 0 0 0 0.25rem rgba(238, 95, 91, 0.5); +} +.btn-check:checked + .btn-outline-danger, .btn-check:active + .btn-outline-danger, .btn-outline-danger:active, .btn-outline-danger.active, .btn-outline-danger.dropdown-toggle.show { + color: #fff; + background-color: #ee5f5b; + border-color: #ee5f5b; +} +.btn-check:checked + .btn-outline-danger:focus, .btn-check:active + .btn-outline-danger:focus, .btn-outline-danger:active:focus, .btn-outline-danger.active:focus, .btn-outline-danger.dropdown-toggle.show:focus { + box-shadow: 0 0 0 0.25rem rgba(238, 95, 91, 0.5); +} +.btn-outline-danger:disabled, .btn-outline-danger.disabled { + color: #ee5f5b; + background-color: transparent; +} .btn-outline-light { color: #e9ecef; - border-color: #e9ecef; } - .btn-outline-light:hover { - color: #000; - background-color: #e9ecef; - border-color: #e9ecef; } - .btn-check:focus + .btn-outline-light, .btn-outline-light:focus { - box-shadow: 0 0 0 0.25rem rgba(233, 236, 239, 0.5); } - .btn-check:checked + .btn-outline-light, - .btn-check:active + .btn-outline-light, .btn-outline-light:active, .btn-outline-light.active, .btn-outline-light.dropdown-toggle.show { - color: #000; - background-color: #e9ecef; - border-color: #e9ecef; } - .btn-check:checked + .btn-outline-light:focus, - .btn-check:active + .btn-outline-light:focus, .btn-outline-light:active:focus, .btn-outline-light.active:focus, .btn-outline-light.dropdown-toggle.show:focus { - box-shadow: 0 0 0 0.25rem rgba(233, 236, 239, 0.5); } - .btn-outline-light:disabled, .btn-outline-light.disabled { - color: #e9ecef; - background-color: transparent; } + border-color: #e9ecef; +} +.btn-outline-light:hover { + color: #000; + background-color: #e9ecef; + border-color: #e9ecef; +} +.btn-check:focus + .btn-outline-light, .btn-outline-light:focus { + box-shadow: 0 0 0 0.25rem rgba(233, 236, 239, 0.5); +} +.btn-check:checked + .btn-outline-light, .btn-check:active + .btn-outline-light, .btn-outline-light:active, .btn-outline-light.active, .btn-outline-light.dropdown-toggle.show { + color: #000; + background-color: #e9ecef; + border-color: #e9ecef; +} +.btn-check:checked + .btn-outline-light:focus, .btn-check:active + .btn-outline-light:focus, .btn-outline-light:active:focus, .btn-outline-light.active:focus, .btn-outline-light.dropdown-toggle.show:focus { + box-shadow: 0 0 0 0.25rem rgba(233, 236, 239, 0.5); +} +.btn-outline-light:disabled, .btn-outline-light.disabled { + color: #e9ecef; + background-color: transparent; +} .btn-outline-dark { color: #272b30; - border-color: #272b30; } - .btn-outline-dark:hover { - color: #fff; - background-color: #272b30; - border-color: #272b30; } - .btn-check:focus + .btn-outline-dark, .btn-outline-dark:focus { - box-shadow: 0 0 0 0.25rem rgba(39, 43, 48, 0.5); } - .btn-check:checked + .btn-outline-dark, - .btn-check:active + .btn-outline-dark, .btn-outline-dark:active, .btn-outline-dark.active, .btn-outline-dark.dropdown-toggle.show { - color: #fff; - background-color: #272b30; - border-color: #272b30; } - .btn-check:checked + .btn-outline-dark:focus, - .btn-check:active + .btn-outline-dark:focus, .btn-outline-dark:active:focus, .btn-outline-dark.active:focus, .btn-outline-dark.dropdown-toggle.show:focus { - box-shadow: 0 0 0 0.25rem rgba(39, 43, 48, 0.5); } - .btn-outline-dark:disabled, .btn-outline-dark.disabled { - color: #272b30; - background-color: transparent; } + border-color: #272b30; +} +.btn-outline-dark:hover { + color: #fff; + background-color: #272b30; + border-color: #272b30; +} +.btn-check:focus + .btn-outline-dark, .btn-outline-dark:focus { + box-shadow: 0 0 0 0.25rem rgba(39, 43, 48, 0.5); +} +.btn-check:checked + .btn-outline-dark, .btn-check:active + .btn-outline-dark, .btn-outline-dark:active, .btn-outline-dark.active, .btn-outline-dark.dropdown-toggle.show { + color: #fff; + background-color: #272b30; + border-color: #272b30; +} +.btn-check:checked + .btn-outline-dark:focus, .btn-check:active + .btn-outline-dark:focus, .btn-outline-dark:active:focus, .btn-outline-dark.active:focus, .btn-outline-dark.dropdown-toggle.show:focus { + box-shadow: 0 0 0 0.25rem rgba(39, 43, 48, 0.5); +} +.btn-outline-dark:disabled, .btn-outline-dark.disabled { + color: #272b30; + background-color: transparent; +} .btn-link { font-weight: 400; color: #fff; - text-decoration: underline; } - .btn-link:hover { - color: #cccccc; } - .btn-link:disabled, .btn-link.disabled { - color: #7a8288; } + text-decoration: underline; +} +.btn-link:hover { + color: #cccccc; +} +.btn-link:disabled, .btn-link.disabled { + color: #7a8288; +} .btn-lg { padding: 0.5rem 1rem; font-size: 1.25rem; - border-radius: 0.3rem; } + border-radius: 0.3rem; +} .btn-sm { padding: 0.25rem 0.5rem; font-size: 0.875rem; - border-radius: 0.2rem; } + border-radius: 0.2rem; +} .form-label { - margin-bottom: 0.5rem; } + margin-bottom: 0.5rem; +} .col-form-label { padding-top: calc(0.375rem + 1px); padding-bottom: calc(0.375rem + 1px); margin-bottom: 0; font-size: inherit; - line-height: 1.5; } + line-height: 1.5; +} .col-form-label-lg { padding-top: calc(0.5rem + 1px); padding-bottom: calc(0.5rem + 1px); - font-size: 1.25rem; } + font-size: 1.25rem; +} .col-form-label-sm { padding-top: calc(0.25rem + 1px); padding-bottom: calc(0.25rem + 1px); - font-size: 0.875rem; } + font-size: 0.875rem; +} .form-text { margin-top: 0.25rem; font-size: 0.875em; - color: #7a8288; } + color: #7a8288; +} .form-control { display: block; @@ -942,64 +1118,81 @@ ul.sidebar-elements li > a.sidebar-link.active::after { border: 1px solid #ced4da; appearance: none; border-radius: 0.25rem; - transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; } - @media (prefers-reduced-motion: reduce) { - .form-control { - transition: none; } } - .form-control[type="file"] { - overflow: hidden; } - .form-control[type="file"]:not(:disabled):not([readonly]) { - cursor: pointer; } - .form-control:focus { - color: #272b30; - background-color: #fff; - border-color: #9d9fa2; - outline: 0; - box-shadow: 0 0 0 0.25rem rgba(58, 63, 68, 0.25); } - .form-control::-webkit-date-and-time-value { - height: 1.5em; } - .form-control::placeholder { - color: #7a8288; - opacity: 1; } - .form-control:disabled, .form-control[readonly] { - background-color: #ccc; - opacity: 1; } + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .form-control { + transition: none; + } +} +.form-control[type=file] { + overflow: hidden; +} +.form-control[type=file]:not(:disabled):not([readonly]) { + cursor: pointer; +} +.form-control:focus { + color: #272b30; + background-color: #fff; + border-color: #9d9fa2; + outline: 0; + box-shadow: 0 0 0 0.25rem rgba(58, 63, 68, 0.25); +} +.form-control::-webkit-date-and-time-value { + height: 1.5em; +} +.form-control::placeholder { + color: #7a8288; + opacity: 1; +} +.form-control:disabled, .form-control[readonly] { + background-color: #ccc; + opacity: 1; +} +.form-control::file-selector-button { + padding: 0.375rem 0.75rem; + margin: -0.375rem -0.75rem; + margin-inline-end: 0.75rem; + color: #272b30; + background-color: #e9ecef; + pointer-events: none; + border-color: inherit; + border-style: solid; + border-width: 0; + border-inline-end-width: 1px; + border-radius: 0; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { .form-control::file-selector-button { - padding: 0.375rem 0.75rem; - margin: -0.375rem -0.75rem; - margin-inline-end: 0.75rem; - color: #272b30; - background-color: #e9ecef; - pointer-events: none; - border-color: inherit; - border-style: solid; - border-width: 0; - border-inline-end-width: 1px; - border-radius: 0; - transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; } - @media (prefers-reduced-motion: reduce) { - .form-control::file-selector-button { - transition: none; } } - .form-control:hover:not(:disabled):not([readonly])::file-selector-button { - background-color: #dde0e3; } + transition: none; + } +} +.form-control:hover:not(:disabled):not([readonly])::file-selector-button { + background-color: #dde0e3; +} +.form-control::-webkit-file-upload-button { + padding: 0.375rem 0.75rem; + margin: -0.375rem -0.75rem; + margin-inline-end: 0.75rem; + color: #272b30; + background-color: #e9ecef; + pointer-events: none; + border-color: inherit; + border-style: solid; + border-width: 0; + border-inline-end-width: 1px; + border-radius: 0; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { .form-control::-webkit-file-upload-button { - padding: 0.375rem 0.75rem; - margin: -0.375rem -0.75rem; - margin-inline-end: 0.75rem; - color: #272b30; - background-color: #e9ecef; - pointer-events: none; - border-color: inherit; - border-style: solid; - border-width: 0; - border-inline-end-width: 1px; - border-radius: 0; - transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; } - @media (prefers-reduced-motion: reduce) { - .form-control::-webkit-file-upload-button { - transition: none; } } - .form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button { - background-color: #dde0e3; } + transition: none; + } +} +.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button { + background-color: #dde0e3; +} .form-control-plaintext { display: block; @@ -1010,60 +1203,73 @@ ul.sidebar-elements li > a.sidebar-link.active::after { color: #aaa; background-color: transparent; border: solid transparent; - border-width: 1px 0; } - .form-control-plaintext.form-control-sm, .form-control-plaintext.form-control-lg { - padding-right: 0; - padding-left: 0; } + border-width: 1px 0; +} +.form-control-plaintext.form-control-sm, .form-control-plaintext.form-control-lg { + padding-right: 0; + padding-left: 0; +} .form-control-sm { min-height: calc(1.5em + 0.5rem + 2px); padding: 0.25rem 0.5rem; font-size: 0.875rem; - border-radius: 0.2rem; } - .form-control-sm::file-selector-button { - padding: 0.25rem 0.5rem; - margin: -0.25rem -0.5rem; - margin-inline-end: 0.5rem; } - .form-control-sm::-webkit-file-upload-button { - padding: 0.25rem 0.5rem; - margin: -0.25rem -0.5rem; - margin-inline-end: 0.5rem; } + border-radius: 0.2rem; +} +.form-control-sm::file-selector-button { + padding: 0.25rem 0.5rem; + margin: -0.25rem -0.5rem; + margin-inline-end: 0.5rem; +} +.form-control-sm::-webkit-file-upload-button { + padding: 0.25rem 0.5rem; + margin: -0.25rem -0.5rem; + margin-inline-end: 0.5rem; +} .form-control-lg { min-height: calc(1.5em + 1rem + 2px); padding: 0.5rem 1rem; font-size: 1.25rem; - border-radius: 0.3rem; } - .form-control-lg::file-selector-button { - padding: 0.5rem 1rem; - margin: -0.5rem -1rem; - margin-inline-end: 1rem; } - .form-control-lg::-webkit-file-upload-button { - padding: 0.5rem 1rem; - margin: -0.5rem -1rem; - margin-inline-end: 1rem; } + border-radius: 0.3rem; +} +.form-control-lg::file-selector-button { + padding: 0.5rem 1rem; + margin: -0.5rem -1rem; + margin-inline-end: 1rem; +} +.form-control-lg::-webkit-file-upload-button { + padding: 0.5rem 1rem; + margin: -0.5rem -1rem; + margin-inline-end: 1rem; +} textarea.form-control { - min-height: calc(1.5em + 0.75rem + 2px); } - + min-height: calc(1.5em + 0.75rem + 2px); +} textarea.form-control-sm { - min-height: calc(1.5em + 0.5rem + 2px); } - + min-height: calc(1.5em + 0.5rem + 2px); +} textarea.form-control-lg { - min-height: calc(1.5em + 1rem + 2px); } + min-height: calc(1.5em + 1rem + 2px); +} .form-control-color { width: 3rem; height: auto; - padding: 0.375rem; } - .form-control-color:not(:disabled):not([readonly]) { - cursor: pointer; } - .form-control-color::-moz-color-swatch { - height: 1.5em; - border-radius: 0.25rem; } - .form-control-color::-webkit-color-swatch { - height: 1.5em; - border-radius: 0.25rem; } + padding: 0.375rem; +} +.form-control-color:not(:disabled):not([readonly]) { + cursor: pointer; +} +.form-control-color::-moz-color-swatch { + height: 1.5em; + border-radius: 0.25rem; +} +.form-control-color::-webkit-color-swatch { + height: 1.5em; + border-radius: 0.25rem; +} .form-select { display: block; @@ -1082,43 +1288,56 @@ textarea.form-control-lg { border: 1px solid #ced4da; border-radius: 0.25rem; transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; - appearance: none; } - @media (prefers-reduced-motion: reduce) { - .form-select { - transition: none; } } - .form-select:focus { - border-color: #9d9fa2; - outline: 0; - box-shadow: 0 0 0 0.25rem rgba(58, 63, 68, 0.25); } - .form-select[multiple], .form-select[size]:not([size="1"]) { - padding-right: 0.75rem; - background-image: none; } - .form-select:disabled { - background-color: #e9ecef; } - .form-select:-moz-focusring { - color: transparent; - text-shadow: 0 0 0 #272b30; } + appearance: none; +} +@media (prefers-reduced-motion: reduce) { + .form-select { + transition: none; + } +} +.form-select:focus { + border-color: #9d9fa2; + outline: 0; + box-shadow: 0 0 0 0.25rem rgba(58, 63, 68, 0.25); +} +.form-select[multiple], .form-select[size]:not([size="1"]) { + padding-right: 0.75rem; + background-image: none; +} +.form-select:disabled { + background-color: #e9ecef; +} +.form-select:-moz-focusring { + color: transparent; + text-shadow: 0 0 0 #272b30; +} .form-select-sm { padding-top: 0.25rem; padding-bottom: 0.25rem; padding-left: 0.5rem; - font-size: 0.875rem; } + font-size: 0.875rem; + border-radius: 0.2rem; +} .form-select-lg { padding-top: 0.5rem; padding-bottom: 0.5rem; padding-left: 1rem; - font-size: 1.25rem; } + font-size: 1.25rem; + border-radius: 0.3rem; +} .form-check { display: block; min-height: 1.5rem; padding-left: 1.5em; - margin-bottom: 0.125rem; } - .form-check .form-check-input { - float: left; - margin-left: -1.5em; } + margin-bottom: 0.125rem; +} +.form-check .form-check-input { + float: left; + margin-left: -1.5em; +} .form-check-input { width: 1em; @@ -1131,191 +1350,246 @@ textarea.form-control-lg { background-size: contain; border: 1px solid rgba(0, 0, 0, 0.25); appearance: none; - color-adjust: exact; } - .form-check-input[type="checkbox"] { - border-radius: 0.25em; } - .form-check-input[type="radio"] { - border-radius: 50%; } - .form-check-input:active { - filter: brightness(90%); } - .form-check-input:focus { - border-color: #9d9fa2; - outline: 0; - box-shadow: 0 0 0 0.25rem rgba(58, 63, 68, 0.25); } - .form-check-input:checked { - background-color: #3a3f44; - border-color: #3a3f44; } - .form-check-input:checked[type="checkbox"] { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e"); } - .form-check-input:checked[type="radio"] { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e"); } - .form-check-input[type="checkbox"]:indeterminate { - background-color: #3a3f44; - border-color: #3a3f44; - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e"); } - .form-check-input:disabled { - pointer-events: none; - filter: none; - opacity: 0.5; } - .form-check-input[disabled] ~ .form-check-label, .form-check-input:disabled ~ .form-check-label { - opacity: 0.5; } + color-adjust: exact; +} +.form-check-input[type=checkbox] { + border-radius: 0.25em; +} +.form-check-input[type=radio] { + border-radius: 50%; +} +.form-check-input:active { + filter: brightness(90%); +} +.form-check-input:focus { + border-color: #9d9fa2; + outline: 0; + box-shadow: 0 0 0 0.25rem rgba(58, 63, 68, 0.25); +} +.form-check-input:checked { + background-color: #3a3f44; + border-color: #3a3f44; +} +.form-check-input:checked[type=checkbox] { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e"); +} +.form-check-input:checked[type=radio] { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e"); +} +.form-check-input[type=checkbox]:indeterminate { + background-color: #3a3f44; + border-color: #3a3f44; + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e"); +} +.form-check-input:disabled { + pointer-events: none; + filter: none; + opacity: 0.5; +} +.form-check-input[disabled] ~ .form-check-label, .form-check-input:disabled ~ .form-check-label { + opacity: 0.5; +} .form-switch { - padding-left: 2.5em; } + padding-left: 2.5em; +} +.form-switch .form-check-input { + width: 2em; + margin-left: -2.5em; + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e"); + background-position: left center; + border-radius: 2em; + transition: background-position 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { .form-switch .form-check-input { - width: 2em; - margin-left: -2.5em; - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e"); - background-position: left center; - border-radius: 2em; - transition: background-position 0.15s ease-in-out; } - @media (prefers-reduced-motion: reduce) { - .form-switch .form-check-input { - transition: none; } } - .form-switch .form-check-input:focus { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%239d9fa2'/%3e%3c/svg%3e"); } - .form-switch .form-check-input:checked { - background-position: right center; - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e"); } + transition: none; + } +} +.form-switch .form-check-input:focus { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%239d9fa2'/%3e%3c/svg%3e"); +} +.form-switch .form-check-input:checked { + background-position: right center; + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e"); +} .form-check-inline { display: inline-block; - margin-right: 1rem; } + margin-right: 1rem; +} .btn-check { position: absolute; clip: rect(0, 0, 0, 0); - pointer-events: none; } - .btn-check[disabled] + .btn, .btn-check:disabled + .btn { - pointer-events: none; - filter: none; - opacity: 0.65; } + pointer-events: none; +} +.btn-check[disabled] + .btn, .btn-check:disabled + .btn { + pointer-events: none; + filter: none; + opacity: 0.65; +} .form-range { width: 100%; height: 1.5rem; padding: 0; background-color: transparent; - appearance: none; } - .form-range:focus { - outline: 0; } - .form-range:focus::-webkit-slider-thumb { - box-shadow: 0 0 0 1px #272b30, 0 0 0 0.25rem rgba(58, 63, 68, 0.25); } - .form-range:focus::-moz-range-thumb { - box-shadow: 0 0 0 1px #272b30, 0 0 0 0.25rem rgba(58, 63, 68, 0.25); } - .form-range::-moz-focus-outer { - border: 0; } + appearance: none; +} +.form-range:focus { + outline: 0; +} +.form-range:focus::-webkit-slider-thumb { + box-shadow: 0 0 0 1px #272b30, 0 0 0 0.25rem rgba(58, 63, 68, 0.25); +} +.form-range:focus::-moz-range-thumb { + box-shadow: 0 0 0 1px #272b30, 0 0 0 0.25rem rgba(58, 63, 68, 0.25); +} +.form-range::-moz-focus-outer { + border: 0; +} +.form-range::-webkit-slider-thumb { + width: 1rem; + height: 1rem; + margin-top: -0.25rem; + background-color: #3a3f44; + border: 0; + border-radius: 1rem; + transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + appearance: none; +} +@media (prefers-reduced-motion: reduce) { .form-range::-webkit-slider-thumb { - width: 1rem; - height: 1rem; - margin-top: -0.25rem; - background-color: #3a3f44; - border: 0; - border-radius: 1rem; - transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; - appearance: none; } - @media (prefers-reduced-motion: reduce) { - .form-range::-webkit-slider-thumb { - transition: none; } } - .form-range::-webkit-slider-thumb:active { - background-color: #c4c5c7; } - .form-range::-webkit-slider-runnable-track { - width: 100%; - height: 0.5rem; - color: transparent; - cursor: pointer; - background-color: #dee2e6; - border-color: transparent; - border-radius: 1rem; } + transition: none; + } +} +.form-range::-webkit-slider-thumb:active { + background-color: #c4c5c7; +} +.form-range::-webkit-slider-runnable-track { + width: 100%; + height: 0.5rem; + color: transparent; + cursor: pointer; + background-color: #dee2e6; + border-color: transparent; + border-radius: 1rem; +} +.form-range::-moz-range-thumb { + width: 1rem; + height: 1rem; + background-color: #3a3f44; + border: 0; + border-radius: 1rem; + transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + appearance: none; +} +@media (prefers-reduced-motion: reduce) { .form-range::-moz-range-thumb { - width: 1rem; - height: 1rem; - background-color: #3a3f44; - border: 0; - border-radius: 1rem; - transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; - appearance: none; } - @media (prefers-reduced-motion: reduce) { - .form-range::-moz-range-thumb { - transition: none; } } - .form-range::-moz-range-thumb:active { - background-color: #c4c5c7; } - .form-range::-moz-range-track { - width: 100%; - height: 0.5rem; - color: transparent; - cursor: pointer; - background-color: #dee2e6; - border-color: transparent; - border-radius: 1rem; } - .form-range:disabled { - pointer-events: none; } - .form-range:disabled::-webkit-slider-thumb { - background-color: #999; } - .form-range:disabled::-moz-range-thumb { - background-color: #999; } + transition: none; + } +} +.form-range::-moz-range-thumb:active { + background-color: #c4c5c7; +} +.form-range::-moz-range-track { + width: 100%; + height: 0.5rem; + color: transparent; + cursor: pointer; + background-color: #dee2e6; + border-color: transparent; + border-radius: 1rem; +} +.form-range:disabled { + pointer-events: none; +} +.form-range:disabled::-webkit-slider-thumb { + background-color: #999; +} +.form-range:disabled::-moz-range-thumb { + background-color: #999; +} .form-floating { - position: relative; } - .form-floating > .form-control, - .form-floating > .form-select { - height: calc(3.5rem + 2px); - line-height: 1.25; } + position: relative; +} +.form-floating > .form-control, +.form-floating > .form-select { + height: calc(3.5rem + 2px); + line-height: 1.25; +} +.form-floating > label { + position: absolute; + top: 0; + left: 0; + height: 100%; + padding: 1rem 0.75rem; + pointer-events: none; + border: 1px solid transparent; + transform-origin: 0 0; + transition: opacity 0.1s ease-in-out, transform 0.1s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { .form-floating > label { - position: absolute; - top: 0; - left: 0; - height: 100%; - padding: 1rem 0.75rem; - pointer-events: none; - border: 1px solid transparent; - transform-origin: 0 0; - transition: opacity 0.1s ease-in-out, transform 0.1s ease-in-out; } - @media (prefers-reduced-motion: reduce) { - .form-floating > label { - transition: none; } } - .form-floating > .form-control { - padding: 1rem 0.75rem; } - .form-floating > .form-control::placeholder { - color: transparent; } - .form-floating > .form-control:focus, .form-floating > .form-control:not(:placeholder-shown) { - padding-top: 1.625rem; - padding-bottom: 0.625rem; } - .form-floating > .form-control:-webkit-autofill { - padding-top: 1.625rem; - padding-bottom: 0.625rem; } - .form-floating > .form-select { - padding-top: 1.625rem; - padding-bottom: 0.625rem; } - .form-floating > .form-control:focus ~ label, - .form-floating > .form-control:not(:placeholder-shown) ~ label, - .form-floating > .form-select ~ label { - opacity: 0.65; - transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem); } - .form-floating > .form-control:-webkit-autofill ~ label { - opacity: 0.65; - transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem); } + transition: none; + } +} +.form-floating > .form-control { + padding: 1rem 0.75rem; +} +.form-floating > .form-control::placeholder { + color: transparent; +} +.form-floating > .form-control:focus, .form-floating > .form-control:not(:placeholder-shown) { + padding-top: 1.625rem; + padding-bottom: 0.625rem; +} +.form-floating > .form-control:-webkit-autofill { + padding-top: 1.625rem; + padding-bottom: 0.625rem; +} +.form-floating > .form-select { + padding-top: 1.625rem; + padding-bottom: 0.625rem; +} +.form-floating > .form-control:focus ~ label, +.form-floating > .form-control:not(:placeholder-shown) ~ label, +.form-floating > .form-select ~ label { + opacity: 0.65; + transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem); +} +.form-floating > .form-control:-webkit-autofill ~ label { + opacity: 0.65; + transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem); +} .input-group { position: relative; display: flex; flex-wrap: wrap; align-items: stretch; - width: 100%; } - .input-group > .form-control, - .input-group > .form-select { - position: relative; - flex: 1 1 auto; - width: 1%; - min-width: 0; } - .input-group > .form-control:focus, - .input-group > .form-select:focus { - z-index: 3; } - .input-group .btn { - position: relative; - z-index: 2; } - .input-group .btn:focus { - z-index: 3; } + width: 100%; +} +.input-group > .form-control, +.input-group > .form-select { + position: relative; + flex: 1 1 auto; + width: 1%; + min-width: 0; +} +.input-group > .form-control:focus, +.input-group > .form-select:focus { + z-index: 3; +} +.input-group .btn { + position: relative; + z-index: 2; +} +.input-group .btn:focus { + z-index: 3; +} .input-group-text { display: flex; @@ -1329,7 +1603,8 @@ textarea.form-control-lg { white-space: nowrap; background-color: #e9ecef; border: 1px solid #ced4da; - border-radius: 0.25rem; } + border-radius: 0.25rem; +} .input-group-lg > .form-control, .input-group-lg > .form-select, @@ -1337,7 +1612,8 @@ textarea.form-control-lg { .input-group-lg > .btn { padding: 0.5rem 1rem; font-size: 1.25rem; - border-radius: 0.3rem; } + border-radius: 0.3rem; +} .input-group-sm > .form-control, .input-group-sm > .form-select, @@ -1345,33 +1621,37 @@ textarea.form-control-lg { .input-group-sm > .btn { padding: 0.25rem 0.5rem; font-size: 0.875rem; - border-radius: 0.2rem; } + border-radius: 0.2rem; +} .input-group-lg > .form-select, .input-group-sm > .form-select { - padding-right: 3rem; } + padding-right: 3rem; +} .input-group:not(.has-validation) > :not(:last-child):not(.dropdown-toggle):not(.dropdown-menu), -.input-group:not(.has-validation) > .dropdown-toggle:nth-last-child(n + 3) { +.input-group:not(.has-validation) > .dropdown-toggle:nth-last-child(n+3) { border-top-right-radius: 0; - border-bottom-right-radius: 0; } - -.input-group.has-validation > :nth-last-child(n + 3):not(.dropdown-toggle):not(.dropdown-menu), -.input-group.has-validation > .dropdown-toggle:nth-last-child(n + 4) { + border-bottom-right-radius: 0; +} +.input-group.has-validation > :nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu), +.input-group.has-validation > .dropdown-toggle:nth-last-child(n+4) { border-top-right-radius: 0; - border-bottom-right-radius: 0; } - + border-bottom-right-radius: 0; +} .input-group > :not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback) { margin-left: -1px; border-top-left-radius: 0; - border-bottom-left-radius: 0; } + border-bottom-left-radius: 0; +} .valid-feedback { display: none; width: 100%; margin-top: 0.25rem; font-size: 0.875em; - color: #62c462; } + color: #62c462; +} .valid-tooltip { position: absolute; @@ -1380,17 +1660,19 @@ textarea.form-control-lg { display: none; max-width: 100%; padding: 0.25rem 0.5rem; - margin-top: .1rem; + margin-top: 0.1rem; font-size: 0.875rem; color: #fff; background-color: rgba(98, 196, 98, 0.9); - border-radius: 0.25rem; } + border-radius: 0.25rem; +} .was-validated :valid ~ .valid-feedback, .was-validated :valid ~ .valid-tooltip, .is-valid ~ .valid-feedback, .is-valid ~ .valid-tooltip { - display: block; } + display: block; +} .was-validated .form-control:valid, .form-control.is-valid { border-color: #62c462; @@ -1398,53 +1680,67 @@ textarea.form-control-lg { background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2362c462' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e"); background-repeat: no-repeat; background-position: right calc(0.375em + 0.1875rem) center; - background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); } - .was-validated .form-control:valid:focus, .form-control.is-valid:focus { - border-color: #62c462; - box-shadow: 0 0 0 0.25rem rgba(98, 196, 98, 0.25); } + background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} +.was-validated .form-control:valid:focus, .form-control.is-valid:focus { + border-color: #62c462; + box-shadow: 0 0 0 0.25rem rgba(98, 196, 98, 0.25); +} .was-validated textarea.form-control:valid, textarea.form-control.is-valid { padding-right: calc(1.5em + 0.75rem); - background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem); } + background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem); +} .was-validated .form-select:valid, .form-select.is-valid { - border-color: #62c462; } - .was-validated .form-select:valid:not([multiple]):not([size]), .was-validated .form-select:valid:not([multiple])[size="1"], .form-select.is-valid:not([multiple]):not([size]), .form-select.is-valid:not([multiple])[size="1"] { - padding-right: 4.125rem; - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%233a3f44' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"), url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2362c462' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e"); - background-position: right 0.75rem center, center right 2.25rem; - background-size: 16px 12px, calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); } - .was-validated .form-select:valid:focus, .form-select.is-valid:focus { - border-color: #62c462; - box-shadow: 0 0 0 0.25rem rgba(98, 196, 98, 0.25); } + border-color: #62c462; +} +.was-validated .form-select:valid:not([multiple]):not([size]), .was-validated .form-select:valid:not([multiple])[size="1"], .form-select.is-valid:not([multiple]):not([size]), .form-select.is-valid:not([multiple])[size="1"] { + padding-right: 4.125rem; + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%233a3f44' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"), url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2362c462' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e"); + background-position: right 0.75rem center, center right 2.25rem; + background-size: 16px 12px, calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} +.was-validated .form-select:valid:focus, .form-select.is-valid:focus { + border-color: #62c462; + box-shadow: 0 0 0 0.25rem rgba(98, 196, 98, 0.25); +} .was-validated .form-check-input:valid, .form-check-input.is-valid { - border-color: #62c462; } - .was-validated .form-check-input:valid:checked, .form-check-input.is-valid:checked { - background-color: #62c462; } - .was-validated .form-check-input:valid:focus, .form-check-input.is-valid:focus { - box-shadow: 0 0 0 0.25rem rgba(98, 196, 98, 0.25); } - .was-validated .form-check-input:valid ~ .form-check-label, .form-check-input.is-valid ~ .form-check-label { - color: #62c462; } + border-color: #62c462; +} +.was-validated .form-check-input:valid:checked, .form-check-input.is-valid:checked { + background-color: #62c462; +} +.was-validated .form-check-input:valid:focus, .form-check-input.is-valid:focus { + box-shadow: 0 0 0 0.25rem rgba(98, 196, 98, 0.25); +} +.was-validated .form-check-input:valid ~ .form-check-label, .form-check-input.is-valid ~ .form-check-label { + color: #62c462; +} .form-check-inline .form-check-input ~ .valid-feedback { - margin-left: .5em; } + margin-left: 0.5em; +} -.was-validated .input-group .form-control:valid, .input-group .form-control.is-valid, .was-validated -.input-group .form-select:valid, +.was-validated .input-group .form-control:valid, .input-group .form-control.is-valid, +.was-validated .input-group .form-select:valid, .input-group .form-select.is-valid { - z-index: 1; } - .was-validated .input-group .form-control:valid:focus, .input-group .form-control.is-valid:focus, .was-validated - .input-group .form-select:valid:focus, - .input-group .form-select.is-valid:focus { - z-index: 3; } + z-index: 1; +} +.was-validated .input-group .form-control:valid:focus, .input-group .form-control.is-valid:focus, +.was-validated .input-group .form-select:valid:focus, +.input-group .form-select.is-valid:focus { + z-index: 3; +} .invalid-feedback { display: none; width: 100%; margin-top: 0.25rem; font-size: 0.875em; - color: #ee5f5b; } + color: #ee5f5b; +} .invalid-tooltip { position: absolute; @@ -1453,17 +1749,19 @@ textarea.form-control-lg { display: none; max-width: 100%; padding: 0.25rem 0.5rem; - margin-top: .1rem; + margin-top: 0.1rem; font-size: 0.875rem; color: #fff; background-color: rgba(238, 95, 91, 0.9); - border-radius: 0.25rem; } + border-radius: 0.25rem; +} .was-validated :invalid ~ .invalid-feedback, .was-validated :invalid ~ .invalid-tooltip, .is-invalid ~ .invalid-feedback, .is-invalid ~ .invalid-tooltip { - display: block; } + display: block; +} .was-validated .form-control:invalid, .form-control.is-invalid { border-color: #ee5f5b; @@ -1471,46 +1769,59 @@ textarea.form-control-lg { background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23ee5f5b'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23ee5f5b' stroke='none'/%3e%3c/svg%3e"); background-repeat: no-repeat; background-position: right calc(0.375em + 0.1875rem) center; - background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); } - .was-validated .form-control:invalid:focus, .form-control.is-invalid:focus { - border-color: #ee5f5b; - box-shadow: 0 0 0 0.25rem rgba(238, 95, 91, 0.25); } + background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} +.was-validated .form-control:invalid:focus, .form-control.is-invalid:focus { + border-color: #ee5f5b; + box-shadow: 0 0 0 0.25rem rgba(238, 95, 91, 0.25); +} .was-validated textarea.form-control:invalid, textarea.form-control.is-invalid { padding-right: calc(1.5em + 0.75rem); - background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem); } + background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem); +} .was-validated .form-select:invalid, .form-select.is-invalid { - border-color: #ee5f5b; } - .was-validated .form-select:invalid:not([multiple]):not([size]), .was-validated .form-select:invalid:not([multiple])[size="1"], .form-select.is-invalid:not([multiple]):not([size]), .form-select.is-invalid:not([multiple])[size="1"] { - padding-right: 4.125rem; - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%233a3f44' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"), url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23ee5f5b'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23ee5f5b' stroke='none'/%3e%3c/svg%3e"); - background-position: right 0.75rem center, center right 2.25rem; - background-size: 16px 12px, calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); } - .was-validated .form-select:invalid:focus, .form-select.is-invalid:focus { - border-color: #ee5f5b; - box-shadow: 0 0 0 0.25rem rgba(238, 95, 91, 0.25); } + border-color: #ee5f5b; +} +.was-validated .form-select:invalid:not([multiple]):not([size]), .was-validated .form-select:invalid:not([multiple])[size="1"], .form-select.is-invalid:not([multiple]):not([size]), .form-select.is-invalid:not([multiple])[size="1"] { + padding-right: 4.125rem; + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%233a3f44' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"), url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23ee5f5b'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23ee5f5b' stroke='none'/%3e%3c/svg%3e"); + background-position: right 0.75rem center, center right 2.25rem; + background-size: 16px 12px, calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} +.was-validated .form-select:invalid:focus, .form-select.is-invalid:focus { + border-color: #ee5f5b; + box-shadow: 0 0 0 0.25rem rgba(238, 95, 91, 0.25); +} .was-validated .form-check-input:invalid, .form-check-input.is-invalid { - border-color: #ee5f5b; } - .was-validated .form-check-input:invalid:checked, .form-check-input.is-invalid:checked { - background-color: #ee5f5b; } - .was-validated .form-check-input:invalid:focus, .form-check-input.is-invalid:focus { - box-shadow: 0 0 0 0.25rem rgba(238, 95, 91, 0.25); } - .was-validated .form-check-input:invalid ~ .form-check-label, .form-check-input.is-invalid ~ .form-check-label { - color: #ee5f5b; } + border-color: #ee5f5b; +} +.was-validated .form-check-input:invalid:checked, .form-check-input.is-invalid:checked { + background-color: #ee5f5b; +} +.was-validated .form-check-input:invalid:focus, .form-check-input.is-invalid:focus { + box-shadow: 0 0 0 0.25rem rgba(238, 95, 91, 0.25); +} +.was-validated .form-check-input:invalid ~ .form-check-label, .form-check-input.is-invalid ~ .form-check-label { + color: #ee5f5b; +} .form-check-inline .form-check-input ~ .invalid-feedback { - margin-left: .5em; } + margin-left: 0.5em; +} -.was-validated .input-group .form-control:invalid, .input-group .form-control.is-invalid, .was-validated -.input-group .form-select:invalid, +.was-validated .input-group .form-control:invalid, .input-group .form-control.is-invalid, +.was-validated .input-group .form-select:invalid, .input-group .form-select.is-invalid { - z-index: 2; } - .was-validated .input-group .form-control:invalid:focus, .input-group .form-control.is-invalid:focus, .was-validated - .input-group .form-select:invalid:focus, - .input-group .form-select.is-invalid:focus { - z-index: 3; } + z-index: 2; +} +.was-validated .input-group .form-control:invalid:focus, .input-group .form-control.is-invalid:focus, +.was-validated .input-group .form-select:invalid:focus, +.input-group .form-select.is-invalid:focus { + z-index: 3; +} .navbar { position: relative; @@ -1519,42 +1830,48 @@ textarea.form-control-lg { align-items: center; justify-content: space-between; padding-top: 0; - padding-bottom: 0; } - .navbar > .container, - .navbar > .container-fluid, .navbar > .container-sm, .navbar > .container-md, .navbar > .container-lg, .navbar > .container-xl, .navbar > .container-xxl { - display: flex; - flex-wrap: inherit; - align-items: center; - justify-content: space-between; } - + padding-bottom: 0; +} +.navbar > .container-xxl, .navbar > .container-xl, .navbar > .container-lg, .navbar > .container-md, .navbar > .container-sm, .navbar > .container, +.navbar > .container-fluid { + display: flex; + flex-wrap: inherit; + align-items: center; + justify-content: space-between; +} .navbar-brand { padding-top: 0.3125rem; padding-bottom: 0.3125rem; margin-right: 1rem; font-size: 1.25rem; text-decoration: none; - white-space: nowrap; } - + white-space: nowrap; +} .navbar-nav { display: flex; flex-direction: column; padding-left: 0; margin-bottom: 0; - list-style: none; } - .navbar-nav .nav-link { - padding-right: 0; - padding-left: 0; } - .navbar-nav .dropdown-menu { - position: static; } + list-style: none; +} +.navbar-nav .nav-link { + padding-right: 0; + padding-left: 0; +} +.navbar-nav .dropdown-menu { + position: static; +} .navbar-text { padding-top: 0.5rem; - padding-bottom: 0.5rem; } + padding-bottom: 0.5rem; +} .navbar-collapse { flex-basis: 100%; flex-grow: 1; - align-items: center; } + align-items: center; +} .navbar-toggler { padding: 0.25rem 0.75rem; @@ -1563,16 +1880,21 @@ textarea.form-control-lg { background-color: transparent; border: 1px solid transparent; border-radius: 0.25rem; - transition: box-shadow 0.15s ease-in-out; } - @media (prefers-reduced-motion: reduce) { - .navbar-toggler { - transition: none; } } - .navbar-toggler:hover { - text-decoration: none; } - .navbar-toggler:focus { - text-decoration: none; - outline: 0; - box-shadow: 0 0 0 0.25rem; } + transition: box-shadow 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .navbar-toggler { + transition: none; + } +} +.navbar-toggler:hover { + text-decoration: none; +} +.navbar-toggler:focus { + text-decoration: none; + outline: 0; + box-shadow: 0 0 0 0.25rem; +} .navbar-toggler-icon { display: inline-block; @@ -1581,242 +1903,43 @@ textarea.form-control-lg { vertical-align: middle; background-repeat: no-repeat; background-position: center; - background-size: 100%; } + background-size: 100%; +} .navbar-nav-scroll { max-height: var(--bs-scroll-height, 75vh); - overflow-y: auto; } + overflow-y: auto; +} @media (min-width: 576px) { .navbar-expand-sm { flex-wrap: nowrap; - justify-content: flex-start; } - .navbar-expand-sm .navbar-nav { - flex-direction: row; } - .navbar-expand-sm .navbar-nav .dropdown-menu { - position: absolute; } - .navbar-expand-sm .navbar-nav .nav-link { - padding-right: 0.5rem; - padding-left: 0.5rem; } - .navbar-expand-sm .navbar-nav-scroll { - overflow: visible; } - .navbar-expand-sm .navbar-collapse { - display: flex !important; - flex-basis: auto; } - .navbar-expand-sm .navbar-toggler { - display: none; } - .navbar-expand-sm .offcanvas-header { - display: none; } - .navbar-expand-sm .offcanvas { - position: inherit; - bottom: 0; - z-index: 1000; - flex-grow: 1; - visibility: visible !important; - background-color: transparent; - border-right: 0; - border-left: 0; - transition: none; - transform: none; } - .navbar-expand-sm .offcanvas-top, - .navbar-expand-sm .offcanvas-bottom { - height: auto; - border-top: 0; - border-bottom: 0; } - .navbar-expand-sm .offcanvas-body { - display: flex; - flex-grow: 0; - padding: 0; - overflow-y: visible; } } - -@media (min-width: 768px) { - .navbar-expand-md { - flex-wrap: nowrap; - justify-content: flex-start; } - .navbar-expand-md .navbar-nav { - flex-direction: row; } - .navbar-expand-md .navbar-nav .dropdown-menu { - position: absolute; } - .navbar-expand-md .navbar-nav .nav-link { - padding-right: 0.5rem; - padding-left: 0.5rem; } - .navbar-expand-md .navbar-nav-scroll { - overflow: visible; } - .navbar-expand-md .navbar-collapse { - display: flex !important; - flex-basis: auto; } - .navbar-expand-md .navbar-toggler { - display: none; } - .navbar-expand-md .offcanvas-header { - display: none; } - .navbar-expand-md .offcanvas { - position: inherit; - bottom: 0; - z-index: 1000; - flex-grow: 1; - visibility: visible !important; - background-color: transparent; - border-right: 0; - border-left: 0; - transition: none; - transform: none; } - .navbar-expand-md .offcanvas-top, - .navbar-expand-md .offcanvas-bottom { - height: auto; - border-top: 0; - border-bottom: 0; } - .navbar-expand-md .offcanvas-body { - display: flex; - flex-grow: 0; - padding: 0; - overflow-y: visible; } } - -@media (min-width: 992px) { - .navbar-expand-lg { - flex-wrap: nowrap; - justify-content: flex-start; } - .navbar-expand-lg .navbar-nav { - flex-direction: row; } - .navbar-expand-lg .navbar-nav .dropdown-menu { - position: absolute; } - .navbar-expand-lg .navbar-nav .nav-link { - padding-right: 0.5rem; - padding-left: 0.5rem; } - .navbar-expand-lg .navbar-nav-scroll { - overflow: visible; } - .navbar-expand-lg .navbar-collapse { - display: flex !important; - flex-basis: auto; } - .navbar-expand-lg .navbar-toggler { - display: none; } - .navbar-expand-lg .offcanvas-header { - display: none; } - .navbar-expand-lg .offcanvas { - position: inherit; - bottom: 0; - z-index: 1000; - flex-grow: 1; - visibility: visible !important; - background-color: transparent; - border-right: 0; - border-left: 0; - transition: none; - transform: none; } - .navbar-expand-lg .offcanvas-top, - .navbar-expand-lg .offcanvas-bottom { - height: auto; - border-top: 0; - border-bottom: 0; } - .navbar-expand-lg .offcanvas-body { - display: flex; - flex-grow: 0; - padding: 0; - overflow-y: visible; } } - -@media (min-width: 1200px) { - .navbar-expand-xl { - flex-wrap: nowrap; - justify-content: flex-start; } - .navbar-expand-xl .navbar-nav { - flex-direction: row; } - .navbar-expand-xl .navbar-nav .dropdown-menu { - position: absolute; } - .navbar-expand-xl .navbar-nav .nav-link { - padding-right: 0.5rem; - padding-left: 0.5rem; } - .navbar-expand-xl .navbar-nav-scroll { - overflow: visible; } - .navbar-expand-xl .navbar-collapse { - display: flex !important; - flex-basis: auto; } - .navbar-expand-xl .navbar-toggler { - display: none; } - .navbar-expand-xl .offcanvas-header { - display: none; } - .navbar-expand-xl .offcanvas { - position: inherit; - bottom: 0; - z-index: 1000; - flex-grow: 1; - visibility: visible !important; - background-color: transparent; - border-right: 0; - border-left: 0; - transition: none; - transform: none; } - .navbar-expand-xl .offcanvas-top, - .navbar-expand-xl .offcanvas-bottom { - height: auto; - border-top: 0; - border-bottom: 0; } - .navbar-expand-xl .offcanvas-body { - display: flex; - flex-grow: 0; - padding: 0; - overflow-y: visible; } } - -@media (min-width: 1400px) { - .navbar-expand-xxl { - flex-wrap: nowrap; - justify-content: flex-start; } - .navbar-expand-xxl .navbar-nav { - flex-direction: row; } - .navbar-expand-xxl .navbar-nav .dropdown-menu { - position: absolute; } - .navbar-expand-xxl .navbar-nav .nav-link { - padding-right: 0.5rem; - padding-left: 0.5rem; } - .navbar-expand-xxl .navbar-nav-scroll { - overflow: visible; } - .navbar-expand-xxl .navbar-collapse { - display: flex !important; - flex-basis: auto; } - .navbar-expand-xxl .navbar-toggler { - display: none; } - .navbar-expand-xxl .offcanvas-header { - display: none; } - .navbar-expand-xxl .offcanvas { - position: inherit; - bottom: 0; - z-index: 1000; - flex-grow: 1; - visibility: visible !important; - background-color: transparent; - border-right: 0; - border-left: 0; - transition: none; - transform: none; } - .navbar-expand-xxl .offcanvas-top, - .navbar-expand-xxl .offcanvas-bottom { - height: auto; - border-top: 0; - border-bottom: 0; } - .navbar-expand-xxl .offcanvas-body { - display: flex; - flex-grow: 0; - padding: 0; - overflow-y: visible; } } - -.navbar-expand { - flex-wrap: nowrap; - justify-content: flex-start; } - .navbar-expand .navbar-nav { - flex-direction: row; } - .navbar-expand .navbar-nav .dropdown-menu { - position: absolute; } - .navbar-expand .navbar-nav .nav-link { - padding-right: 0.5rem; - padding-left: 0.5rem; } - .navbar-expand .navbar-nav-scroll { - overflow: visible; } - .navbar-expand .navbar-collapse { + justify-content: flex-start; + } + .navbar-expand-sm .navbar-nav { + flex-direction: row; + } + .navbar-expand-sm .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-sm .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; + } + .navbar-expand-sm .navbar-nav-scroll { + overflow: visible; + } + .navbar-expand-sm .navbar-collapse { display: flex !important; - flex-basis: auto; } - .navbar-expand .navbar-toggler { - display: none; } - .navbar-expand .offcanvas-header { - display: none; } - .navbar-expand .offcanvas { + flex-basis: auto; + } + .navbar-expand-sm .navbar-toggler { + display: none; + } + .navbar-expand-sm .offcanvas-header { + display: none; + } + .navbar-expand-sm .offcanvas { position: inherit; bottom: 0; z-index: 1000; @@ -1826,74 +1949,351 @@ textarea.form-control-lg { border-right: 0; border-left: 0; transition: none; - transform: none; } - .navbar-expand .offcanvas-top, - .navbar-expand .offcanvas-bottom { + transform: none; + } + .navbar-expand-sm .offcanvas-top, +.navbar-expand-sm .offcanvas-bottom { height: auto; border-top: 0; - border-bottom: 0; } - .navbar-expand .offcanvas-body { + border-bottom: 0; + } + .navbar-expand-sm .offcanvas-body { display: flex; flex-grow: 0; padding: 0; - overflow-y: visible; } + overflow-y: visible; + } +} +@media (min-width: 768px) { + .navbar-expand-md { + flex-wrap: nowrap; + justify-content: flex-start; + } + .navbar-expand-md .navbar-nav { + flex-direction: row; + } + .navbar-expand-md .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-md .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; + } + .navbar-expand-md .navbar-nav-scroll { + overflow: visible; + } + .navbar-expand-md .navbar-collapse { + display: flex !important; + flex-basis: auto; + } + .navbar-expand-md .navbar-toggler { + display: none; + } + .navbar-expand-md .offcanvas-header { + display: none; + } + .navbar-expand-md .offcanvas { + position: inherit; + bottom: 0; + z-index: 1000; + flex-grow: 1; + visibility: visible !important; + background-color: transparent; + border-right: 0; + border-left: 0; + transition: none; + transform: none; + } + .navbar-expand-md .offcanvas-top, +.navbar-expand-md .offcanvas-bottom { + height: auto; + border-top: 0; + border-bottom: 0; + } + .navbar-expand-md .offcanvas-body { + display: flex; + flex-grow: 0; + padding: 0; + overflow-y: visible; + } +} +@media (min-width: 992px) { + .navbar-expand-lg { + flex-wrap: nowrap; + justify-content: flex-start; + } + .navbar-expand-lg .navbar-nav { + flex-direction: row; + } + .navbar-expand-lg .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-lg .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; + } + .navbar-expand-lg .navbar-nav-scroll { + overflow: visible; + } + .navbar-expand-lg .navbar-collapse { + display: flex !important; + flex-basis: auto; + } + .navbar-expand-lg .navbar-toggler { + display: none; + } + .navbar-expand-lg .offcanvas-header { + display: none; + } + .navbar-expand-lg .offcanvas { + position: inherit; + bottom: 0; + z-index: 1000; + flex-grow: 1; + visibility: visible !important; + background-color: transparent; + border-right: 0; + border-left: 0; + transition: none; + transform: none; + } + .navbar-expand-lg .offcanvas-top, +.navbar-expand-lg .offcanvas-bottom { + height: auto; + border-top: 0; + border-bottom: 0; + } + .navbar-expand-lg .offcanvas-body { + display: flex; + flex-grow: 0; + padding: 0; + overflow-y: visible; + } +} +@media (min-width: 1200px) { + .navbar-expand-xl { + flex-wrap: nowrap; + justify-content: flex-start; + } + .navbar-expand-xl .navbar-nav { + flex-direction: row; + } + .navbar-expand-xl .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-xl .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; + } + .navbar-expand-xl .navbar-nav-scroll { + overflow: visible; + } + .navbar-expand-xl .navbar-collapse { + display: flex !important; + flex-basis: auto; + } + .navbar-expand-xl .navbar-toggler { + display: none; + } + .navbar-expand-xl .offcanvas-header { + display: none; + } + .navbar-expand-xl .offcanvas { + position: inherit; + bottom: 0; + z-index: 1000; + flex-grow: 1; + visibility: visible !important; + background-color: transparent; + border-right: 0; + border-left: 0; + transition: none; + transform: none; + } + .navbar-expand-xl .offcanvas-top, +.navbar-expand-xl .offcanvas-bottom { + height: auto; + border-top: 0; + border-bottom: 0; + } + .navbar-expand-xl .offcanvas-body { + display: flex; + flex-grow: 0; + padding: 0; + overflow-y: visible; + } +} +@media (min-width: 1400px) { + .navbar-expand-xxl { + flex-wrap: nowrap; + justify-content: flex-start; + } + .navbar-expand-xxl .navbar-nav { + flex-direction: row; + } + .navbar-expand-xxl .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-xxl .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; + } + .navbar-expand-xxl .navbar-nav-scroll { + overflow: visible; + } + .navbar-expand-xxl .navbar-collapse { + display: flex !important; + flex-basis: auto; + } + .navbar-expand-xxl .navbar-toggler { + display: none; + } + .navbar-expand-xxl .offcanvas-header { + display: none; + } + .navbar-expand-xxl .offcanvas { + position: inherit; + bottom: 0; + z-index: 1000; + flex-grow: 1; + visibility: visible !important; + background-color: transparent; + border-right: 0; + border-left: 0; + transition: none; + transform: none; + } + .navbar-expand-xxl .offcanvas-top, +.navbar-expand-xxl .offcanvas-bottom { + height: auto; + border-top: 0; + border-bottom: 0; + } + .navbar-expand-xxl .offcanvas-body { + display: flex; + flex-grow: 0; + padding: 0; + overflow-y: visible; + } +} +.navbar-expand { + flex-wrap: nowrap; + justify-content: flex-start; +} +.navbar-expand .navbar-nav { + flex-direction: row; +} +.navbar-expand .navbar-nav .dropdown-menu { + position: absolute; +} +.navbar-expand .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; +} +.navbar-expand .navbar-nav-scroll { + overflow: visible; +} +.navbar-expand .navbar-collapse { + display: flex !important; + flex-basis: auto; +} +.navbar-expand .navbar-toggler { + display: none; +} +.navbar-expand .offcanvas-header { + display: none; +} +.navbar-expand .offcanvas { + position: inherit; + bottom: 0; + z-index: 1000; + flex-grow: 1; + visibility: visible !important; + background-color: transparent; + border-right: 0; + border-left: 0; + transition: none; + transform: none; +} +.navbar-expand .offcanvas-top, +.navbar-expand .offcanvas-bottom { + height: auto; + border-top: 0; + border-bottom: 0; +} +.navbar-expand .offcanvas-body { + display: flex; + flex-grow: 0; + padding: 0; + overflow-y: visible; +} .navbar-light .navbar-brand { - color: #3a3f44; } - .navbar-light .navbar-brand:hover, .navbar-light .navbar-brand:focus { - color: #3a3f44; } - + color: #3a3f44; +} +.navbar-light .navbar-brand:hover, .navbar-light .navbar-brand:focus { + color: #3a3f44; +} .navbar-light .navbar-nav .nav-link { - color: rgba(0, 0, 0, 0.55); } - .navbar-light .navbar-nav .nav-link:hover, .navbar-light .navbar-nav .nav-link:focus { - color: #3a3f44; } - .navbar-light .navbar-nav .nav-link.disabled { - color: rgba(0, 0, 0, 0.3); } - + color: rgba(0, 0, 0, 0.55); +} +.navbar-light .navbar-nav .nav-link:hover, .navbar-light .navbar-nav .nav-link:focus { + color: #3a3f44; +} +.navbar-light .navbar-nav .nav-link.disabled { + color: rgba(0, 0, 0, 0.3); +} .navbar-light .navbar-nav .show > .nav-link, .navbar-light .navbar-nav .nav-link.active { - color: #3a3f44; } - + color: #3a3f44; +} .navbar-light .navbar-toggler { color: rgba(0, 0, 0, 0.55); - border-color: rgba(0, 0, 0, 0.1); } - + border-color: rgba(0, 0, 0, 0.1); +} .navbar-light .navbar-toggler-icon { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); } - + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); +} .navbar-light .navbar-text { - color: rgba(0, 0, 0, 0.55); } - .navbar-light .navbar-text a, - .navbar-light .navbar-text a:hover, - .navbar-light .navbar-text a:focus { - color: #3a3f44; } + color: rgba(0, 0, 0, 0.55); +} +.navbar-light .navbar-text a, +.navbar-light .navbar-text a:hover, +.navbar-light .navbar-text a:focus { + color: #3a3f44; +} .navbar-dark .navbar-brand { - color: #fff; } - .navbar-dark .navbar-brand:hover, .navbar-dark .navbar-brand:focus { - color: #fff; } - + color: #fff; +} +.navbar-dark .navbar-brand:hover, .navbar-dark .navbar-brand:focus { + color: #fff; +} .navbar-dark .navbar-nav .nav-link { - color: rgba(255, 255, 255, 0.55); } - .navbar-dark .navbar-nav .nav-link:hover, .navbar-dark .navbar-nav .nav-link:focus { - color: #fff; } - .navbar-dark .navbar-nav .nav-link.disabled { - color: rgba(255, 255, 255, 0.25); } - + color: rgba(255, 255, 255, 0.55); +} +.navbar-dark .navbar-nav .nav-link:hover, .navbar-dark .navbar-nav .nav-link:focus { + color: #fff; +} +.navbar-dark .navbar-nav .nav-link.disabled { + color: rgba(255, 255, 255, 0.25); +} .navbar-dark .navbar-nav .show > .nav-link, .navbar-dark .navbar-nav .nav-link.active { - color: #fff; } - + color: #fff; +} .navbar-dark .navbar-toggler { color: rgba(255, 255, 255, 0.55); - border-color: rgba(255, 255, 255, 0.1); } - + border-color: rgba(255, 255, 255, 0.1); +} .navbar-dark .navbar-toggler-icon { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); } - + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); +} .navbar-dark .navbar-text { - color: rgba(255, 255, 255, 0.55); } - .navbar-dark .navbar-text a, - .navbar-dark .navbar-text a:hover, - .navbar-dark .navbar-text a:focus { - color: #fff; } + color: rgba(255, 255, 255, 0.55); +} +.navbar-dark .navbar-text a, +.navbar-dark .navbar-text a:hover, +.navbar-dark .navbar-text a:focus { + color: #fff; +} diff --git a/webroot/css/themes/theme-vapor.css b/webroot/css/themes/theme-vapor.css index c8f9498..f96c897 100644 --- a/webroot/css/themes/theme-vapor.css +++ b/webroot/css/themes/theme-vapor.css @@ -1,323 +1,408 @@ /* Callout */ .callout { border: 1px solid #e9ecef; - border-radius: .25rem; + border-radius: 0.25rem; background-color: #363636; - box-shadow: none; } + box-shadow: none; +} .callout-primary { border-left-color: #6f42c1; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-secondary { border-left-color: #ea39b8; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-success { border-left-color: #3cf281; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-info { border-left-color: #1ba2f6; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-warning { border-left-color: #ffc107; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-danger { border-left-color: #e44c55; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-light { border-left-color: #44d9e8; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} .callout-dark { border-left-color: #170229; - border-left-width: .25rem; - border-left-style: solid; } + border-left-width: 0.25rem; + border-left-style: solid; +} /* Toasts */ .toast { - min-width: 250px; } + min-width: 250px; +} .toast-primary { color: #21143a; background-color: #d4c6ec; - border-color: #c5b3e6; } - .toast-primary strong { - border-top-color: #b6a0e0; } + border-color: #c5b3e6; +} +.toast-primary strong { + border-top-color: #b6a0e0; +} .toast-secondary { color: #461137; background-color: #f9c4ea; - border-color: #f7b0e3; } - .toast-secondary strong { - border-top-color: #f599db; } + border-color: #f7b0e3; +} +.toast-secondary strong { + border-top-color: #f599db; +} .toast-success { color: #124927; background-color: #c5fbd9; - border-color: #b1facd; } - .toast-success strong { - border-top-color: #99f8be; } + border-color: #b1facd; +} +.toast-success strong { + border-top-color: #99f8be; +} .toast-info { color: #08314a; background-color: #bbe3fc; - border-color: #a4dafb; } - .toast-info strong { - border-top-color: #8cd0fa; } + border-color: #a4dafb; +} +.toast-info strong { + border-top-color: #8cd0fa; +} .toast-warning { color: #4d3a02; background-color: #ffecb5; - border-color: #ffe69c; } - .toast-warning strong { - border-top-color: #ffe083; } + border-color: #ffe69c; +} +.toast-warning strong { + border-top-color: #ffe083; +} .toast-danger { color: #44171a; background-color: #f7c9cc; - border-color: #f4b7bb; } - .toast-danger strong { - border-top-color: #f1a1a6; } + border-color: #f4b7bb; +} +.toast-danger strong { + border-top-color: #f1a1a6; +} .toast-light { color: #144146; background-color: #c7f4f8; - border-color: #b4f0f6; } - .toast-light strong { - border-top-color: #9debf3; } + border-color: #b4f0f6; +} +.toast-light strong { + border-top-color: #9debf3; +} .toast-dark { color: #07010c; background-color: #b9b3bf; - border-color: #a29aa9; } - .toast-dark strong { - border-top-color: #958c9d; } + border-color: #a29aa9; +} +.toast-dark strong { + border-top-color: #958c9d; +} /* Dropdown-item */ .dropdown-item.dropdown-item-primary { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #6f42c1; } .dropdown-item.dropdown-item-primary:hover { color: #fff; background-color: #5e38a4; } +======= + background-color: #6f42c1; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-primary:hover { color: #fff; - background-color: #6f42c1; } - + background-color: #6f42c1; +} .dropdown-item.dropdown-item-secondary { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #ea39b8; } .dropdown-item.dropdown-item-secondary:hover { color: #fff; background-color: #c7309c; } +======= + background-color: #ea39b8; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-secondary:hover { color: #fff; - background-color: #ea39b8; } - + background-color: #ea39b8; +} .dropdown-item.dropdown-item-success { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #3cf281; } .dropdown-item.dropdown-item-success:hover { color: #fff; background-color: #33ce6e; } +======= + background-color: #3cf281; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-success:hover { color: #fff; - background-color: #3cf281; } - + background-color: #3cf281; +} .dropdown-item.dropdown-item-info { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #1ba2f6; } .dropdown-item.dropdown-item-info:hover { color: #fff; background-color: #178ad1; } +======= + background-color: #1ba2f6; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-info:hover { color: #fff; - background-color: #1ba2f6; } - + background-color: #1ba2f6; +} .dropdown-item.dropdown-item-warning { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #ffc107; } .dropdown-item.dropdown-item-warning:hover { color: #fff; background-color: #d9a406; } +======= + background-color: #ffc107; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-warning:hover { color: #fff; - background-color: #ffc107; } - + background-color: #ffc107; +} .dropdown-item.dropdown-item-danger { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #e44c55; } .dropdown-item.dropdown-item-danger:hover { color: #fff; background-color: #c24148; } +======= + background-color: #e44c55; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-danger:hover { color: #fff; - background-color: #e44c55; } - + background-color: #e44c55; +} .dropdown-item.dropdown-item-light { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #44d9e8; } .dropdown-item.dropdown-item-light:hover { color: #fff; background-color: #3ab8c5; } +======= + background-color: #44d9e8; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-light:hover { color: #fff; - background-color: #44d9e8; } - + background-color: #44d9e8; +} .dropdown-item.dropdown-item-dark { color: #fff; text-decoration: none; +<<<<<<< HEAD background-color: #170229; } .dropdown-item.dropdown-item-dark:hover { color: #fff; background-color: #140223; } +======= + background-color: #170229; +} +>>>>>>> 80cd93da404036ac08b645e105c750c6348e7c3f .dropdown-item.dropdown-item-outline-dark:hover { color: #fff; - background-color: #170229; } + background-color: #170229; +} /* Progress Timeline */ .progress-timeline { - padding: 0.2em 0.2em 0.5em 0.2em; } - .progress-timeline ul { - position: relative; - padding: 0; } - .progress-timeline li { - list-style-type: none; - position: relative; } - .progress-timeline li.progress-inactive { - opacity: 0.5; } - .progress-timeline .progress-line { - height: 2px; } - .progress-timeline .progress-line.progress-inactive { - opacity: 0.5; } + padding: 0.2em 0.2em 0.5em 0.2em; +} +.progress-timeline ul { + position: relative; + padding: 0; +} +.progress-timeline li { + list-style-type: none; + position: relative; +} +.progress-timeline li.progress-inactive { + opacity: 0.5; +} +.progress-timeline .progress-line { + height: 2px; +} +.progress-timeline .progress-line.progress-inactive { + opacity: 0.5; +} /* Forms severity */ .form-control.is-invalid.info { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%231ba2f6' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%231ba2f6' stroke='none'/%3e%3c/svg%3e"); } - .form-control.is-invalid.info:focus { - border-color: #1ba2f6; - box-shadow: 0 0 0 0.25rem rgba(27, 162, 246, 0.25); } - + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%231ba2f6' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%231ba2f6' stroke='none'/%3e%3c/svg%3e"); +} +.form-control.is-invalid.info:focus { + border-color: #1ba2f6; + box-shadow: 0 0 0 0.25rem rgba(27, 162, 246, 0.25); +} .form-control.is-invalid.warning { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23ffc107' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23ffc107' stroke='none'/%3e%3c/svg%3e"); } - .form-control.is-invalid.warning:focus { - border-color: #ffc107; - box-shadow: 0 0 0 0.25rem rgba(255, 193, 7, 0.25); } + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23ffc107' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23ffc107' stroke='none'/%3e%3c/svg%3e"); +} +.form-control.is-invalid.warning:focus { + border-color: #ffc107; + box-shadow: 0 0 0 0.25rem rgba(255, 193, 7, 0.25); +} .form-select.is-invalid:not([multiple]):not([size]).info, -.form-select.is-invalid:not([multiple])[size="1"] -.form-select.is-invalid.info { +.form-select.is-invalid:not([multiple])[size="1"] .form-select.is-invalid.info { background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%231ba2f6'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%231ba2f6' stroke='none'/%3e%3c/svg%3e"); - background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); } - .form-select.is-invalid:not([multiple]):not([size]).info:focus, - .form-select.is-invalid:not([multiple])[size="1"] -.form-select.is-invalid.info:focus { - box-shadow: 0 0 0 0.25rem rgba(27, 162, 246, 0.25); } - + background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} +.form-select.is-invalid:not([multiple]):not([size]).info:focus, +.form-select.is-invalid:not([multiple])[size="1"] .form-select.is-invalid.info:focus { + box-shadow: 0 0 0 0.25rem rgba(27, 162, 246, 0.25); +} .form-select.is-invalid:not([multiple]):not([size]).warning, -.form-select.is-invalid:not([multiple])[size="1"] -.form-select.is-invalid.warning { +.form-select.is-invalid:not([multiple])[size="1"] .form-select.is-invalid.warning { background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23ffc107'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23ffc107' stroke='none'/%3e%3c/svg%3e"); - background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); } - .form-select.is-invalid:not([multiple]):not([size]).warning:focus, - .form-select.is-invalid:not([multiple])[size="1"] -.form-select.is-invalid.warning:focus { - box-shadow: 0 0 0 0.25rem rgba(255, 193, 7, 0.25); } + background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} +.form-select.is-invalid:not([multiple]):not([size]).warning:focus, +.form-select.is-invalid:not([multiple])[size="1"] .form-select.is-invalid.warning:focus { + box-shadow: 0 0 0 0.25rem rgba(255, 193, 7, 0.25); +} .form-check-input.is-invalid.info { - border-color: #1ba2f6; } - + border-color: #1ba2f6; +} .form-check-input.is-invalid.info:checked { - background-color: #1ba2f6; } - + background-color: #1ba2f6; +} .form-check-input.is-invalid.info ~ .form-check-label { - color: unset; } - + color: unset; +} .form-check-input.is-invalid.info:focus { - box-shadow: 0 0 0 0.2rem rgba(27, 162, 246, 0.25); } - + box-shadow: 0 0 0 0.2rem rgba(27, 162, 246, 0.25); +} .form-check-input.is-invalid.warning { - border-color: #ffc107; } - + border-color: #ffc107; +} .form-check-input.is-invalid.warning:checked { - background-color: #ffc107; } - + background-color: #ffc107; +} .form-check-input.is-invalid.warning ~ .form-check-label { - color: unset; } - + color: unset; +} .form-check-input.is-invalid.warning:focus { - box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.25); } + box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.25); +} /* Utilities */ .mw-75 { - max-width: 75% !important; } + max-width: 75% !important; +} .mw-66 { max-width: 66% !important; } .mw-50 { - max-width: 50% !important; } + max-width: 50% !important; +} .mw-33 { max-width: 33% !important; } .mw-25 { - max-width: 25% !important; } + max-width: 25% !important; +} .mh-75 { - max-height: 75% !important; } + max-height: 75% !important; +} .mh-66 { max-height: 66% !important; } .mh-50 { - max-height: 50% !important; } + max-height: 50% !important; +} .mh-33 { max-height: 33% !important; } .mh-25 { - max-height: 25% !important; } + max-height: 25% !important; +} .p-abs-center-y { top: 50%; - transform: translateY(-50%); } + transform: translateY(-50%); +} .p-abs-center-x { left: 50%; - transform: translateX(-50%); } + transform: translateX(-50%); +} .p-abs-center-both { top: 50%; left: 50%; - transform: translateX(-50%) translateY(-50%); } + transform: translateX(-50%) translateY(-50%); +} .fs-7 { font-size: .875rem !important; } @@ -377,85 +462,108 @@ .panel { background-color: #363636; border: 1px solid #454545; - box-shadow: none; } + box-shadow: none; +} .loading-overlay { background-color: #170229; - opacity: 0.65; } + opacity: 0.65; +} /* Top navbar */ .top-navbar { - background-color: #6f42c1; } + background-color: #6f42c1; +} .center-navbar nav.header-breadcrumb { - color: #fff; } + color: #fff; +} header.top-navbar .header-menu > a:hover, header.top-navbar .header-breadcrumb .header-breadcrumb-item > a:hover { - color: #d6d6d6 !important; } + color: #d6d6d6 !important; +} .top-navbar .center-navbar nav.header-breadcrumb li.header-breadcrumb-item a { - color: #fff; } + color: #fff; +} .top-navbar .right-navbar .header-menu a.nav-link { - color: #fff; } + color: #fff; +} .top-navbar .left-navbar .navbar-brand img { - filter: invert(1); } + filter: invert(1); +} .top-navbar .left-navbar .navbar-brand:hover img { - filter: invert(1) drop-shadow(0px 0px 3px #fff); } + filter: invert(1) drop-shadow(0px 0px 3px #fff); +} .top-navbar .composed-app-icon-container > .app-icon { - background-color: #fff; } + background-color: #fff; +} .breadcrumb-link-container { box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 2px 6px 0 rgba(0, 0, 0, 0.12); - background-color: #44d9e8; } + background-color: #44d9e8; +} /* Sidebar */ .sidebar { - transition: width .08s linear; + transition: width 0.08s linear; box-shadow: none; - background-color: #170229; } + background-color: #170229; +} .sidebar ~ main.content:after { - background: #000; } + background: #000; +} .sidebar .sidebar-wrapper { - border-right: 1px solid none; } + border-right: 1px solid none; +} .sidebar .sidebar-wrapper { - border-right: 1px solid rgba(0, 0, 0, 0.125); } + border-right: 1px solid rgba(0, 0, 0, 0.125); +} .sidebar ul.sidebar-elements li > a.sidebar-link { - color: #fff; } + color: #fff; +} .sidebar ul.sidebar-elements li > a.sidebar-link.active { background-color: #343a40; - color: #3cf281; } + color: #3cf281; +} .sidebar ul.sidebar-elements li > a.sidebar-link.have-active-child { background-color: #343a40; - color: #3cf281; } + color: #3cf281; +} .sidebar ul.sidebar-elements li > a.sidebar-link.have-active-child::after { background-color: #6f42c1; } .sidebar ul.sidebar-elements li > a.sidebar-link:hover { background-color: #495057; - color: #3cf281; } + color: #3cf281; +} .sidebar.expanded ul.sidebar-elements li > a.sidebar-link.have-active-child, .sidebar:hover ul.sidebar-elements li > a.sidebar-link.have-active-child { - background-color: unset; } + background-color: unset; +} .sidebar.expanded ul.sidebar-elements li > a.sidebar-link.have-active-child:hover, .sidebar:hover ul.sidebar-elements li > a.sidebar-link.have-active-child:hover { - background-color: #495057; } + background-color: #495057; +} ul.sidebar-elements li > a.sidebar-link.active::after { - background-color: #6f42c1; } + background-color: #6f42c1; +} .lock-sidebar > a.btn { - background-color: unset; } + background-color: unset; +} diff --git a/webroot/docs/openapi.yaml b/webroot/docs/openapi.yaml new file mode 100644 index 0000000..8077ce7 --- /dev/null +++ b/webroot/docs/openapi.yaml @@ -0,0 +1,2055 @@ +openapi: 3.0.0 +info: + version: 1.3.0 + title: Cerebrate Project API + description: | + Cerebrate is an open-source platform meant to act as a trusted contact information provider and interconnection orchestrator for other security tools. + +servers: + - url: https://cerebrate.local + +tags: + - name: Individuals + description: "Individuals are natural persons. They are meant to describe the basic information about an individual that may or may not be a user of this community. Users in genral require an individual object to identify the person behind them - however, no user account is required to store information about an individual. Individuals can have affiliations to organisations and broods as well as cryptographic keys, using which their messages can be verified and which can be used to securely contact them." + - name: Users + description: "Users enrolled in this Cerebrate instance." + - name: Organisations + description: "Organisations can be equivalent to legal entities or specific individual teams within such entities. Their purpose is to relate individuals to their affiliations and for release control of information using the Trust Circles." + - name: Tags + description: "Tags can be attached to entity to quickly classify them, allowing further filtering and searches." + - name: Inbox + description: "Inbox messages represent A list of requests to be manually processed." + - name: SharingGroups + description: "Sharing groups are distribution lists usable by tools that can exchange information with a list of trusted partners. Create recurring or ad hoc sharing groups and share them with the members of the sharing group." + - name: Broods + description: "Cerebrate can connect to other Cerebrate instances to exchange trust information and to instrument interconnectivity between connected local tools. Each such Cerebrate instance with its connected tools is considered to be a brood." + - name: EncryptionKeys + description: "Assign encryption keys to the user, used to securely communicate or validate messages coming from the user." + - name: AuthKeys + description: "Authkeys are used for API access. A user can have more than one authkey, so if you would like to use separate keys per tool that queries Cerebrate, add additional keys. Use the comment field to make identifying your keys easier." + +paths: + /individuals/index: + get: + summary: "Get individuals list" + operationId: getIndividuals + tags: + - Individuals + parameters: + - $ref: "#/components/parameters/quickFilter" + responses: + "200": + $ref: "#/components/responses/IndividualListResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + /individuals/view/{individualId}: + get: + summary: "Get individual by ID" + operationId: getIndividualById + tags: + - Individuals + parameters: + - $ref: "#/components/parameters/individualId" + responses: + "200": + $ref: "#/components/responses/IndividualResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + /individuals/add: + post: + summary: "Add individual" + operationId: addIndividual + tags: + - Individuals + requestBody: + $ref: "#/components/requestBodies/CreateIndividualRequest" + responses: + "200": + $ref: "#/components/responses/IndividualResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + /individuals/edit/{individualId}: + put: + summary: "Edit individual" + operationId: editIndividual + tags: + - Individuals + parameters: + - $ref: "#/components/parameters/individualId" + requestBody: + $ref: "#/components/requestBodies/EditIndividualRequest" + responses: + "200": + $ref: "#/components/responses/IndividualResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + /individuals/delete/{individualId}: + delete: + summary: "Delete individual by ID" + operationId: deleteIndividualById + tags: + - Individuals + parameters: + - $ref: "#/components/parameters/individualId" + responses: + "200": + $ref: "#/components/responses/IndividualResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + /users/index: + get: + summary: "Get users list" + operationId: getUsers + tags: + - Users + parameters: + - $ref: "#/components/parameters/quickFilter" + responses: + "200": + $ref: "#/components/responses/UserListResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + /users/view: + get: + summary: "Get information about the current user" + operationId: viewUserMe + tags: + - Users + responses: + "200": + $ref: "#/components/responses/UserResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + /users/view/{userId}: + get: + summary: "Get information of a user by ID" + operationId: viewUserById + tags: + - Users + parameters: + - $ref: "#/components/parameters/userId" + responses: + "200": + $ref: "#/components/responses/UserResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + /users/add: + post: + summary: "Add user" + operationId: addUser + tags: + - Users + requestBody: + $ref: "#/components/requestBodies/CreateUserRequest" + responses: + "200": + $ref: "#/components/responses/UserResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + /users/edit: + put: + summary: "Edit current user" + operationId: editUser + tags: + - Users + requestBody: + $ref: "#/components/requestBodies/EditUserRequest" + responses: + "200": + $ref: "#/components/responses/UserResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + /users/edit/{userId}: + put: + summary: "Edit current user" + operationId: editUserById + tags: + - Users + parameters: + - $ref: "#/components/parameters/userId" + requestBody: + $ref: "#/components/requestBodies/EditUserRequest" + responses: + "200": + $ref: "#/components/responses/UserResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + /users/delete/{userId}: + delete: + summary: "Delete user by ID" + operationId: deleteUserById + tags: + - Users + parameters: + - $ref: "#/components/parameters/userId" + responses: + "200": + $ref: "#/components/responses/UserResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + /organisations/add: + post: + summary: "Add organisation" + operationId: addOrganisation + tags: + - Organisations + requestBody: + $ref: "#/components/requestBodies/CreateOrganisationRequest" + responses: + "200": + $ref: "#/components/responses/OrganisationResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + /organisations/edit/{organisationId}: + put: + summary: "Edit organisation" + operationId: editOrganisation + tags: + - Organisations + parameters: + - $ref: "#/components/parameters/organisationId" + requestBody: + $ref: "#/components/requestBodies/EditOrganisationRequest" + responses: + "200": + $ref: "#/components/responses/OrganisationResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + /organisations/index: + get: + summary: "Get organisations" + operationId: getOrganisations + tags: + - Organisations + parameters: + - $ref: "#/components/parameters/quickFilter" + responses: + "200": + $ref: "#/components/responses/OrganisationListResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + /organisations/view/{organisationId}: + get: + summary: "View organisation by ID" + operationId: getOrganisationById + tags: + - Organisations + parameters: + - $ref: "#/components/parameters/organisationId" + responses: + "200": + $ref: "#/components/responses/OrganisationResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + /organisations/delete/{organisationId}: + delete: + summary: "Delete organisation by ID" + operationId: deleteOrganisationById + tags: + - Organisations + parameters: + - $ref: "#/components/parameters/organisationId" + responses: + "200": + $ref: "#/components/responses/OrganisationResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + /organisations/tag/{organisationId}: + post: + summary: "Tag organisation by ID" + operationId: tagOrganisationById + tags: + - Organisations + parameters: + - $ref: "#/components/parameters/organisationId" + requestBody: + $ref: "#/components/requestBodies/TagOrganisationRequest" + responses: + "200": + $ref: "#/components/responses/OrganisationResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + /organisations/untag/{organisationId}: + post: + summary: "Remove organisation tag by ID" + operationId: untagOrganisationById + tags: + - Organisations + parameters: + - $ref: "#/components/parameters/organisationId" + requestBody: + $ref: "#/components/requestBodies/UntagOrganisationRequest" + responses: + "200": + $ref: "#/components/responses/OrganisationResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + /tags/index: + get: + summary: "Get tags list" + operationId: getTags + tags: + - Tags + parameters: + - $ref: "#/components/parameters/quickFilter" + responses: + "200": + $ref: "#/components/responses/TagListResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + /inbox/index: + get: + summary: "Get inbox list" + operationId: getinbox + tags: + - Inbox + parameters: + - $ref: "#/components/parameters/quickFilter" + responses: + "200": + $ref: "#/components/responses/InboxListResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + /inbox/createEntry/User/Registration: + post: + summary: "Create user registration inbox entry" + operationId: createInboxEntry + tags: + - Inbox + requestBody: + $ref: "#/components/requestBodies/CreateUserRegistrationInboxEntryRequest" + responses: + "200": + $ref: "#/components/responses/CreateUserRegistrationInboxEntryResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + /sharingGroups/index: + get: + summary: "Get a sharing groups list" + operationId: getSharingGroups + tags: + - SharingGroups + parameters: + - $ref: "#/components/parameters/quickFilter" + responses: + "200": + $ref: "#/components/responses/SharingGroupListResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + /sharingGroups/add: + post: + summary: "Add sharing group" + operationId: addSharingGroup + tags: + - SharingGroups + requestBody: + $ref: "#/components/requestBodies/CreateSharingGroupRequest" + responses: + "200": + $ref: "#/components/responses/IndividualResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + /sharingGroups/view/{sharingGroupId}: + get: + summary: "Get sharing group by ID" + operationId: getSharingGroupById + tags: + - SharingGroups + parameters: + - $ref: "#/components/parameters/sharingGroupId" + responses: + "200": + $ref: "#/components/responses/SharingGroupResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + /sharingGroups/delete/{sharingGroupId}: + delete: + summary: "Delete sharing group by ID" + operationId: deleteSharingGroupById + tags: + - SharingGroups + parameters: + - $ref: "#/components/parameters/sharingGroupId" + responses: + "200": + $ref: "#/components/responses/SharingGroupResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + /sharingGroups/edit/{sharingGroupId}: + put: + summary: "Edit sharing group" + operationId: editSharingGroup + tags: + - SharingGroups + parameters: + - $ref: "#/components/parameters/sharingGroupId" + requestBody: + $ref: "#/components/requestBodies/EditSharingGroupRequest" + responses: + "200": + $ref: "#/components/responses/SharingGroupResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + /broods/index: + get: + summary: "Get broods list" + operationId: getBroods + tags: + - Broods + parameters: + - $ref: "#/components/parameters/quickFilter" + responses: + "200": + $ref: "#/components/responses/BroodListResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + /broods/view/{broodId}: + get: + summary: "Get brood by ID" + operationId: getBroodById + tags: + - Broods + parameters: + - $ref: "#/components/parameters/broodId" + responses: + "200": + $ref: "#/components/responses/BroodResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + /broods/add: + post: + summary: "Add brood" + operationId: addBrood + tags: + - Broods + requestBody: + $ref: "#/components/requestBodies/CreateBroodRequest" + responses: + "200": + $ref: "#/components/responses/BroodResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + /broods/edit/{broodId}: + put: + summary: "Edit brood" + operationId: editBrood + tags: + - Broods + parameters: + - $ref: "#/components/parameters/broodId" + requestBody: + $ref: "#/components/requestBodies/EditBroodRequest" + responses: + "200": + $ref: "#/components/responses/BroodResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + /broods/delete/{broodId}: + delete: + summary: "Delete brood by ID" + operationId: deleteBroodById + tags: + - Broods + parameters: + - $ref: "#/components/parameters/broodId" + responses: + "200": + $ref: "#/components/responses/BroodResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + /broods/testConnection/{broodId}: + get: + summary: "Test brood connection by ID" + operationId: testBroodConnectionById + tags: + - Broods + parameters: + - $ref: "#/components/parameters/broodId" + responses: + "200": + $ref: "#/components/responses/TestBroodConnectionResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + # EncryptionKeys + /encryptionKeys/index: + get: + summary: "Get encryption keys list" + operationId: getEncryptionKeys + tags: + - EncryptionKeys + parameters: + - $ref: "#/components/parameters/quickFilter" + responses: + "200": + $ref: "#/components/responses/EncryptionKeyListResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + /encryptionKeys/view/{encryptionKeyId}: + get: + summary: "Get encryption key by ID" + operationId: getEncryptionKeyId + tags: + - EncryptionKeys + parameters: + - $ref: "#/components/parameters/encryptionKeyId" + responses: + "200": + $ref: "#/components/responses/EncryptionKeyResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + /encryptionKeys/add: + post: + summary: "Add encryption key" + operationId: addEncryptionKey + tags: + - EncryptionKeys + requestBody: + $ref: "#/components/requestBodies/CreateEncryptionKeyRequest" + responses: + "200": + $ref: "#/components/responses/EncryptionKeyResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + /encryptionKeys/edit/{encryptionKeyId}: + put: + summary: "Edit encryption key" + operationId: editEncryptionKey + tags: + - EncryptionKeys + parameters: + - $ref: "#/components/parameters/encryptionKeyId" + requestBody: + $ref: "#/components/requestBodies/EditEncryptionKeyRequest" + responses: + "200": + $ref: "#/components/responses/EncryptionKeyResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + /encryptionKeys/delete/{encryptionKeyId}: + delete: + summary: "Delete encryption key by ID" + operationId: deleteEncryptionKeyById + tags: + - EncryptionKeys + parameters: + - $ref: "#/components/parameters/encryptionKeyId" + responses: + "200": + $ref: "#/components/responses/EncryptionKeyResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + # AuthKeys + /authKeys/index: + get: + summary: "Get auth keys list" + operationId: getAuthKeys + tags: + - AuthKeys + parameters: + - $ref: "#/components/parameters/quickFilter" + responses: + "200": + $ref: "#/components/responses/AuthKeyListResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + /authKeys/add: + post: + summary: "Add auth keys" + operationId: addAuthKey + tags: + - AuthKeys + requestBody: + $ref: "#/components/requestBodies/CreateAuthKeyRequest" + responses: + "200": + $ref: "#/components/responses/AuthKeyResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + /authKeys/delete/{authKeyId}: + delete: + summary: "Delete auth key by ID" + operationId: deleteAuthKeyById + tags: + - AuthKeys + parameters: + - $ref: "#/components/parameters/authKeyId" + responses: + "200": + $ref: "#/components/responses/AuthKeyResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + +components: + schemas: + # General + UUID: + type: string + format: uuid + maxLength: 36 + example: "c99506a6-1255-4b71-afa5-7b8ba48c3b1b" + + ID: + type: integer + format: int32 + example: 1 + + DateTime: + type: string + format: datetime + example: "2022-01-05T11:19:26+00:00" + + Email: + type: string + format: email + example: "user@example.com" + + ModelName: + type: string + enum: + - "Organisation" + - "User" + - "Individual" + - "EncryptionKey" + - "Role" + - "Tag" + - "SharingGroup" + - "Brood" + + # Individuals + IndividualFirstName: + type: string + example: "John" + + IndividualLastName: + type: string + example: "Doe" + + IndividualFullName: + type: string + example: "John Doe" + + IndividualPosition: + type: string + example: "Security Analyst" + + Individual: + type: object + properties: + id: + $ref: "#/components/schemas/ID" + uuid: + $ref: "#/components/schemas/UUID" + email: + $ref: "#/components/schemas/Email" + first_name: + $ref: "#/components/schemas/IndividualFirstName" + last_name: + $ref: "#/components/schemas/IndividualLastName" + full_name: + $ref: "#/components/schemas/IndividualFullName" + position: + $ref: "#/components/schemas/IndividualPosition" + tags: + $ref: "#/components/schemas/TagList" + aligments: + $ref: "#/components/schemas/AligmentList" + created: + $ref: "#/components/schemas/DateTime" + modified: + $ref: "#/components/schemas/DateTime" + + # Users + Username: + type: string + example: "admin" + + IndividualList: + type: array + items: + $ref: "#/components/schemas/Individual" + + User: + type: object + properties: + id: + $ref: "#/components/schemas/ID" + uuid: + $ref: "#/components/schemas/UUID" + username: + $ref: "#/components/schemas/Username" + role_id: + $ref: "#/components/schemas/ID" + individual_id: + $ref: "#/components/schemas/ID" + disabled: + type: boolean + created: + $ref: "#/components/schemas/DateTime" + modified: + $ref: "#/components/schemas/DateTime" + organisation_id: + $ref: "#/components/schemas/ID" + organisation: + $ref: "#/components/schemas/Organisation" + individual: + $ref: "#/components/schemas/Individual" + role: + $ref: "#/components/schemas/Role" + # user_settings: TODO + # user_settings_by_name: TODO + # user_settings_by_name_with_fallback: TODO + + UserList: + type: array + items: + $ref: "#/components/schemas/User" + + # Organisations + OrganisationName: + type: string + + OrganisationUrl: + type: string + + OrganisationSector: + type: string + nullable: true + + OrganisationType: + type: string + nullable: true + + OrganisationContacts: + type: string + nullable: true + + OrganisationNationality: + type: string + nullable: true + + Organisation: + type: object + properties: + id: + $ref: "#/components/schemas/ID" + uuid: + $ref: "#/components/schemas/UUID" + name: + $ref: "#/components/schemas/OrganisationName" + url: + $ref: "#/components/schemas/OrganisationUrl" + nationality: + $ref: "#/components/schemas/OrganisationNationality" + sector: + $ref: "#/components/schemas/OrganisationSector" + type: + $ref: "#/components/schemas/OrganisationType" + contacts: + $ref: "#/components/schemas/OrganisationContacts" + created: + $ref: "#/components/schemas/DateTime" + modified: + $ref: "#/components/schemas/DateTime" + tags: + $ref: "#/components/schemas/TagList" + aligments: + $ref: "#/components/schemas/AligmentList" + + OrganisationList: + type: array + items: + $ref: "#/components/schemas/Organisation" + + # Tags + TagName: + type: string + example: "white" + + TagNamespace: + type: string + nullable: true + example: "tlp" + + TagPredicate: + type: string + nullable: true + + TagValue: + type: string + nullable: true + + TagColour: + type: string + example: "FFFFFF" + + TagTextColour: + type: string + example: "white" + + Tag: + type: object + properties: + id: + $ref: "#/components/schemas/ID" + name: + $ref: "#/components/schemas/TagName" + namespace: + $ref: "#/components/schemas/TagNamespace" + predicate: + $ref: "#/components/schemas/TagPredicate" + value: + $ref: "#/components/schemas/TagValue" + colour: + $ref: "#/components/schemas/TagColour" + text_colour: + $ref: "#/components/schemas/TagTextColour" + counter: + type: integer + created: + $ref: "#/components/schemas/DateTime" + modified: + $ref: "#/components/schemas/DateTime" + + TagList: + type: array + items: + $ref: "#/components/schemas/Tag" + + # Alignments + Alignment: + type: object + + AligmentList: + type: array + items: + $ref: "#/components/schemas/Alignment" + + # Roles + RoleName: + type: string + maxLength: 255 + example: "admin" + + Role: + type: object + properties: + id: + $ref: "#/components/schemas/ID" + name: + $ref: "#/components/schemas/RoleName" + is_default: + type: boolean + perm_admin: + type: boolean + perm_sync: + type: boolean + perm_org_admin: + type: boolean + + # Inbox + InboxScope: + type: string + enum: + - "User" + - "LocalTool" + - "Brood" + - "Proposal" + - "Synchronisation" + + InboxAction: + type: string + enum: + - "Registration" + - "IncomingConnectionRequest" + - "AcceptedRequest" + - "DeclinedRequest" + - "Synchronisation" + - "OneWaySynchronization" + - "ProposalEdit" + - "DataExchange" + + InboxTitle: + type: string + + InboxOrigin: + type: string + + InboxComment: + type: string + nullable: true + + InboxDescription: + type: string + nullable: true + + Inbox: + type: object + properties: + id: + $ref: "#/components/schemas/ID" + uuid: + $ref: "#/components/schemas/UUID" + scope: + $ref: "#/components/schemas/InboxScope" + action: + $ref: "#/components/schemas/InboxAction" + title: + $ref: "#/components/schemas/InboxTitle" + origin: + $ref: "#/components/schemas/InboxOrigin" + comment: + $ref: "#/components/schemas/InboxComment" + description: + $ref: "#/components/schemas/InboxDescription" + user_id: + $ref: "#/components/schemas/ID" + created: + $ref: "#/components/schemas/DateTime" + modified: + $ref: "#/components/schemas/DateTime" + + UserRegistrationInbox: + type: object + allOf: + - $ref: "#/components/schemas/Inbox" + - type: object + properties: + data: + type: object + properties: + email: + type: string + format: email + password: + type: string + user: + $ref: "#/components/schemas/User" + local_tool_connector_name: + type: string + nullable: true + + IncomingConnectionRequestInbox: + type: object + allOf: + - $ref: "#/components/schemas/Inbox" + - type: object + properties: + data: + type: object + properties: + connectorName: + type: string + enum: + - "MispConnector" + cerebrateURL: + type: string + example: "http://192.168.0.1" + local_tool_id: + type: integer + remote_tool_id: + type: integer + + InboxList: + type: array + items: + anyOf: + - $ref: "#/components/schemas/UserRegistrationInbox" + - $ref: "#/components/schemas/IncomingConnectionRequestInbox" + + # SharingGroups + SharingGroupName: + type: string + + SharingGroupReleasability: + type: string + + SharingGroupDescription: + type: string + + SharingGroup: + type: object + properties: + id: + $ref: "#/components/schemas/ID" + uuid: + $ref: "#/components/schemas/UUID" + name: + $ref: "#/components/schemas/SharingGroupName" + releasability: + $ref: "#/components/schemas/SharingGroupReleasability" + description: + $ref: "#/components/schemas/SharingGroupDescription" + organisation_id: + $ref: "#/components/schemas/ID" + user_id: + $ref: "#/components/schemas/ID" + active: + type: boolean + local: + type: boolean + sharing_group_orgs: + type: array + items: + $ref: "#/components/schemas/Organisation" + user: + $ref: "#/components/schemas/User" + organisation: + $ref: "#/components/schemas/Organisation" + created: + $ref: "#/components/schemas/DateTime" + modified: + $ref: "#/components/schemas/DateTime" + + SharingGroupList: + type: array + items: + $ref: "#/components/schemas/SharingGroup" + + # Broods + BroodName: + type: string + + BroodDescription: + type: string + + BroodUrl: + type: string + + BroodIsTrusted: + type: boolean + description: "Trusted upstream source" + + BroodIsPull: + type: boolean + description: "Enable pulling of trust information" + + Brood: + type: object + properties: + id: + $ref: "#/components/schemas/ID" + uuid: + $ref: "#/components/schemas/UUID" + name: + $ref: "#/components/schemas/BroodName" + url: + $ref: "#/components/schemas/BroodUrl" + description: + $ref: "#/components/schemas/BroodDescription" + organisation_id: + $ref: "#/components/schemas/ID" + trusted: + $ref: "#/components/schemas/BroodIsTrusted" + pull: + $ref: "#/components/schemas/BroodIsPull" + skip_proxy: + type: boolean + authkey: + $ref: "#/components/schemas/AuthKeyRaw" + organisation: + $ref: "#/components/schemas/Organisation" + created: + $ref: "#/components/schemas/DateTime" + modified: + $ref: "#/components/schemas/DateTime" + + BroodList: + type: array + items: + $ref: "#/components/schemas/Brood" + + # EncryptionKeys + EncryptionKeyType: + type: string + enum: + - "pgp" + - "smime" + + EncryptionKeyValue: + type: string + example: | + -----BEGIN PGP PUBLIC KEY BLOCK----- + ... + -----END PGP PUBLIC KEY BLOCK----- + + EncryptionKeyExpiration: + type: integer + description: "UNIX timestamp or null of there is no expiration" + nullable: true + + EncryptionKey: + type: object + properties: + id: + $ref: "#/components/schemas/ID" + uuid: + $ref: "#/components/schemas/UUID" + type: + $ref: "#/components/schemas/EncryptionKeyType" + encryption_key: + $ref: "#/components/schemas/EncryptionKeyValue" + revoked: + type: boolean + expires: + $ref: "#/components/schemas/EncryptionKeyExpiration" + owner_id: + $ref: "#/components/schemas/ID" + owner_model: + $ref: "#/components/schemas/ModelName" + created: + $ref: "#/components/schemas/DateTime" + modified: + $ref: "#/components/schemas/DateTime" + + EncryptionKeyList: + type: array + items: + $ref: "#/components/schemas/EncryptionKey" + + # AuthKeys + AuthKeyRaw: + type: string + + AuthKeyHashed: + type: string + + AuthKeyExpiration: + type: integer + description: "0 or UNIX timestamp" + example: 0 + + AuthKeyCreatedAt: + type: integer + description: "UNIX timestamp" + + AuthKeyComment: + type: string + + AuthKey: + type: object + properties: + id: + $ref: "#/components/schemas/ID" + uuid: + $ref: "#/components/schemas/UUID" + authkey: + $ref: "#/components/schemas/AuthKeyHashed" + authkey_start: + type: string + example: abcd + authkey_end: + type: string + example: abcd + created: + $ref: "#/components/schemas/AuthKeyCreatedAt" + expiration: + $ref: "#/components/schemas/AuthKeyExpiration" + type: integer + description: "0 or UNIX timestamp" + user_id: + $ref: "#/components/schemas/ID" + comment: + $ref: "#/components/schemas/AuthKeyComment" + user: + $ref: "#/components/schemas/User" + + AuthKeyList: + type: array + items: + $ref: "#/components/schemas/AuthKey" + + # Errors + ApiError: + type: object + required: + - message + - url + - code + properties: + message: + type: string + url: + type: string + example: "/users" + code: + type: integer + example: 500 + + UnauthorizedApiError: + type: object + required: + - message + - url + - code + properties: + message: + type: string + example: "Authentication failed. Please make sure you pass the API key of an API enabled user along in the Authorization header." + url: + type: string + example: "/users" + code: + type: integer + example: 403 + + MethodNotAllowedApiError: + type: object + required: + - message + - url + - code + properties: + message: + type: string + example: "You do not have permission to use this functionality." + url: + type: string + example: "/users/index" + code: + type: integer + example: 405 + + NotFoundApiError: + type: object + required: + - message + - url + - code + properties: + message: + type: string + example: "Invalid user" + url: + type: string + example: "/users/users/view/1234" + code: + type: integer + example: 404 + + parameters: + individualId: + name: individualId + in: path + description: "Numeric ID of the User" + required: true + schema: + $ref: "#/components/schemas/ID" + + userId: + name: userId + in: path + description: "Numeric ID of the User" + required: true + schema: + $ref: "#/components/schemas/ID" + + organisationId: + name: organisationId + in: path + description: "Numeric ID of the Organisation" + required: true + schema: + $ref: "#/components/schemas/ID" + + sharingGroupId: + name: sharingGroupId + in: path + description: "Numeric ID of the Sharing Group" + required: true + schema: + $ref: "#/components/schemas/ID" + + broodId: + name: broodId + in: path + description: "Numeric ID of the Brood" + required: true + schema: + $ref: "#/components/schemas/ID" + + encryptionKeyId: + name: encryptionKeyId + in: path + description: "Numeric ID of the EncryptionKey" + required: true + schema: + $ref: "#/components/schemas/ID" + + authKeyId: + name: authKeyId + in: path + description: "Numeric ID of the AuthKey" + required: true + schema: + $ref: "#/components/schemas/ID" + + quickFilter: + name: quickFilter + in: query + description: "Quick filter used to match multiple attributes such as name, description, emails, etc." + schema: + type: string + example: "user@example.com" + + securitySchemes: + ApiKeyAuth: + type: apiKey + in: header + name: Authorization + description: | + The authorization is performed by using the following header in the HTTP requests: + + Authorization: YOUR_API_KEY + + requestBodies: + # Individuals + CreateIndividualRequest: + required: true + content: + application/json: + schema: + type: object + properties: + uuid: + $ref: "#/components/schemas/UUID" + email: + $ref: "#/components/schemas/Email" + first_name: + $ref: "#/components/schemas/IndividualFirstName" + last_name: + $ref: "#/components/schemas/IndividualLastName" + position: + $ref: "#/components/schemas/IndividualPosition" + + EditIndividualRequest: + required: true + content: + application/json: + schema: + type: object + properties: + uuid: + $ref: "#/components/schemas/UUID" + email: + $ref: "#/components/schemas/IndividualLastName" + first_name: + $ref: "#/components/schemas/IndividualFirstName" + last_name: + type: boolean + position: + $ref: "#/components/schemas/IndividualPosition" + + # Users + CreateUserRequest: + required: true + content: + application/json: + schema: + type: object + properties: + individual_id: + $ref: "#/components/schemas/ID" + organisation_id: + $ref: "#/components/schemas/ID" + role_id: + $ref: "#/components/schemas/ID" + disabled: + type: boolean + username: + $ref: "#/components/schemas/Username" + password: + type: string + + EditUserRequest: + required: true + content: + application/json: + schema: + type: object + properties: + id: + $ref: "#/components/schemas/ID" + individual_id: + $ref: "#/components/schemas/ID" + organisation_id: + $ref: "#/components/schemas/ID" + role_id: + $ref: "#/components/schemas/ID" + disabled: + type: boolean + username: + $ref: "#/components/schemas/Username" + password: + type: string + + # Organisations + CreateOrganisationRequest: + required: true + content: + application/json: + schema: + type: object + properties: + uuid: + $ref: "#/components/schemas/UUID" + name: + $ref: "#/components/schemas/OrganisationName" + url: + $ref: "#/components/schemas/OrganisationUrl" + nationality: + $ref: "#/components/schemas/OrganisationNationality" + sector: + $ref: "#/components/schemas/OrganisationSector" + type: + $ref: "#/components/schemas/OrganisationType" + contacts: + $ref: "#/components/schemas/OrganisationContacts" + + EditOrganisationRequest: + required: true + content: + application/json: + schema: + type: object + properties: + uuid: + $ref: "#/components/schemas/UUID" + name: + $ref: "#/components/schemas/OrganisationName" + url: + $ref: "#/components/schemas/OrganisationUrl" + nationality: + $ref: "#/components/schemas/OrganisationNationality" + sector: + $ref: "#/components/schemas/OrganisationSector" + type: + $ref: "#/components/schemas/OrganisationType" + contacts: + $ref: "#/components/schemas/OrganisationContacts" + + TagOrganisationRequest: + required: true + content: + application/json: + schema: + type: object + properties: + tag_list: + type: string + description: "Stringified JSON array of the tag names to add." + example: '["red"]' + + UntagOrganisationRequest: + required: true + content: + application/json: + schema: + type: object + properties: + tag_list: + type: string + description: "Stringified JSON array of the tag names to remove." + example: '["red"]' + + # Inbox + CreateUserRegistrationInboxEntryRequest: + description: "Create user registration inbox entry request" + content: + application/json: + schema: + type: object + properties: + email: + type: string + format: email + password: + type: string + + # SharingGroups + CreateSharingGroupRequest: + required: true + content: + application/json: + schema: + type: object + properties: + uuid: + $ref: "#/components/schemas/UUID" + name: + $ref: "#/components/schemas/SharingGroupName" + releasability: + $ref: "#/components/schemas/SharingGroupReleasability" + description: + $ref: "#/components/schemas/SharingGroupDescription" + organisation_id: + $ref: "#/components/schemas/ID" + user_id: + $ref: "#/components/schemas/ID" + active: + type: boolean + local: + type: boolean + + EditSharingGroupRequest: + required: true + content: + application/json: + schema: + type: object + properties: + uuid: + $ref: "#/components/schemas/UUID" + name: + $ref: "#/components/schemas/SharingGroupName" + releasability: + $ref: "#/components/schemas/SharingGroupReleasability" + description: + $ref: "#/components/schemas/SharingGroupDescription" + organisation_id: + $ref: "#/components/schemas/ID" + user_id: + $ref: "#/components/schemas/ID" + active: + type: boolean + local: + type: boolean + + # Broods + CreateBroodRequest: + required: true + content: + application/json: + schema: + type: object + properties: + uuid: + $ref: "#/components/schemas/UUID" + name: + $ref: "#/components/schemas/BroodName" + url: + $ref: "#/components/schemas/BroodUrl" + description: + $ref: "#/components/schemas/BroodDescription" + organisation_id: + $ref: "#/components/schemas/ID" + trusted: + $ref: "#/components/schemas/BroodIsTrusted" + pull: + $ref: "#/components/schemas/BroodIsPull" + skip_proxy: + type: boolean + authkey: + $ref: "#/components/schemas/AuthKeyRaw" + + EditBroodRequest: + required: true + content: + application/json: + schema: + type: object + properties: + uuid: + $ref: "#/components/schemas/UUID" + name: + $ref: "#/components/schemas/BroodName" + url: + $ref: "#/components/schemas/BroodUrl" + description: + $ref: "#/components/schemas/BroodDescription" + organisation_id: + $ref: "#/components/schemas/ID" + trusted: + $ref: "#/components/schemas/BroodIsTrusted" + pull: + $ref: "#/components/schemas/BroodIsPull" + skip_proxy: + type: boolean + authkey: + $ref: "#/components/schemas/AuthKeyRaw" + + CreateEncryptionKeyRequest: + required: true + content: + application/json: + schema: + type: object + properties: + uuid: + $ref: "#/components/schemas/UUID" + type: + $ref: "#/components/schemas/EncryptionKeyType" + encryption_key: + $ref: "#/components/schemas/EncryptionKeyValue" + revoked: + type: boolean + expires: + $ref: "#/components/schemas/EncryptionKeyExpiration" + owner_id: + $ref: "#/components/schemas/ID" + owner_model: + $ref: "#/components/schemas/ModelName" + + EditEncryptionKeyRequest: + required: true + content: + application/json: + schema: + type: object + properties: + uuid: + $ref: "#/components/schemas/UUID" + type: + $ref: "#/components/schemas/EncryptionKeyType" + encryption_key: + $ref: "#/components/schemas/EncryptionKeyValue" + revoked: + type: boolean + expires: + $ref: "#/components/schemas/EncryptionKeyExpiration" + owner_id: + $ref: "#/components/schemas/ID" + owner_model: + $ref: "#/components/schemas/ModelName" + + # AuthKeys + CreateAuthKeyRequest: + required: true + content: + application/json: + schema: + type: object + properties: + uuid: + $ref: "#/components/schemas/UUID" + authkey: + $ref: "#/components/schemas/AuthKeyRaw" + expiration: + $ref: "#/components/schemas/AuthKeyExpiration" + user_id: + $ref: "#/components/schemas/ID" + comment: + $ref: "#/components/schemas/AuthKeyComment" + + responses: + # Individuals + IndividualResponse: + description: "Individual response" + content: + application/json: + schema: + $ref: "#/components/schemas/Individual" + + IndividualListResponse: + description: "Individuals list response" + content: + application/json: + schema: + $ref: "#/components/schemas/IndividualList" + + # Users + UserResponse: + description: "User response" + content: + application/json: + schema: + $ref: "#/components/schemas/User" + + UserListResponse: + description: "Users list response" + content: + application/json: + schema: + $ref: "#/components/schemas/UserList" + + # Organisations + OrganisationResponse: + description: "Organisation response" + content: + application/json: + schema: + $ref: "#/components/schemas/Organisation" + + OrganisationListResponse: + description: "Organisations list response" + content: + application/json: + schema: + $ref: "#/components/schemas/OrganisationList" + + # Tags + TagResponse: + description: "Tag response" + content: + application/json: + schema: + $ref: "#/components/schemas/Tag" + + TagListResponse: + description: "Tags list response" + content: + application/json: + schema: + $ref: "#/components/schemas/TagList" + + # Inbox + UserRegistrationInboxResponse: + description: "User registration inbox response" + content: + application/json: + schema: + $ref: "#/components/schemas/UserRegistrationInbox" + + IncomingConnectionRequestInboxResponse: + description: "Incoming connection request inbox response" + content: + application/json: + schema: + $ref: "#/components/schemas/IncomingConnectionRequestInbox" + + InboxListResponse: + description: "Inbox list response" + content: + application/json: + schema: + $ref: "#/components/schemas/InboxList" + + CreateUserRegistrationInboxEntryResponse: + description: "Inbox response" + content: + application/json: + schema: + type: object + properties: + data: + allOf: + - $ref: "#/components/schemas/UserRegistrationInbox" + - properties: + local_tool_connector_name: + type: string + nullable: true + success: + type: boolean + message: + type: string + example: "User account creation requested. Please wait for an admin to approve your account." + errors: + type: array + items: + type: object + # TODO: describe + + # SharingGroups + SharingGroupResponse: + description: "Sharing group response" + content: + application/json: + schema: + $ref: "#/components/schemas/SharingGroup" + + SharingGroupListResponse: + description: "Sharing groups list response" + content: + application/json: + schema: + $ref: "#/components/schemas/SharingGroupList" + + # Broods + BroodResponse: + description: "Brood response" + content: + application/json: + schema: + $ref: "#/components/schemas/Brood" + + BroodListResponse: + description: "Brood list response" + content: + application/json: + schema: + $ref: "#/components/schemas/BroodList" + + TestBroodConnectionResponse: + description: "Brood list response" + content: + application/json: + schema: + type: object + properties: + code: + type: integer + description: "HTTP status code" + example: 200 + response: + type: object + properties: + version: + type: string + example: "0.1" + application: + type: string + example: "Cerebrate" + user: + type: string + example: "sync" + ping: + type: number + format: float + + # EncryptionKeys + EncryptionKeyResponse: + description: "Encryption key response" + content: + application/json: + schema: + $ref: "#/components/schemas/EncryptionKey" + + EncryptionKeyListResponse: + description: "Encryption key list response" + content: + application/json: + schema: + $ref: "#/components/schemas/EncryptionKeyList" + + # AuthKeys + AuthKeyResponse: + description: "Auth key response" + content: + application/json: + schema: + $ref: "#/components/schemas/AuthKey" + + AuthKeyListResponse: + description: "Auth key list response" + content: + application/json: + schema: + $ref: "#/components/schemas/AuthKeyList" + + # Errors + ApiErrorResponse: + description: "Unexpected API error" + content: + application/json: + schema: + $ref: "#/components/schemas/ApiError" + + UnauthorizedApiErrorResponse: + description: "Authentication failed. Please make sure you pass the API key of an API enabled user along in the Authorization header." + content: + application/json: + schema: + $ref: "#/components/schemas/UnauthorizedApiError" + + MethodNotAllowedApiErrorResponse: + description: "Method not allowed. Your User Role is not allowed to access this resource." + content: + application/json: + schema: + $ref: "#/components/schemas/MethodNotAllowedApiError" + +security: + - ApiKeyAuth: [] diff --git a/webroot/js/CodeMirror/addon/display/placeholder.js b/webroot/js/CodeMirror/addon/display/placeholder.js new file mode 100644 index 0000000..d8e2dbd --- /dev/null +++ b/webroot/js/CodeMirror/addon/display/placeholder.js @@ -0,0 +1,78 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function (mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function (CodeMirror) { + CodeMirror.defineOption("placeholder", "", function (cm, val, old) { + var prev = old && old != CodeMirror.Init; + if (val && !prev) { + cm.on("blur", onBlur); + cm.on("change", onChange); + cm.on("swapDoc", onChange); + CodeMirror.on(cm.getInputField(), "compositionupdate", cm.state.placeholderCompose = function () { onComposition(cm) }) + onChange(cm); + } else if (!val && prev) { + cm.off("blur", onBlur); + cm.off("change", onChange); + cm.off("swapDoc", onChange); + CodeMirror.off(cm.getInputField(), "compositionupdate", cm.state.placeholderCompose) + clearPlaceholder(cm); + var wrapper = cm.getWrapperElement(); + wrapper.className = wrapper.className.replace(" CodeMirror-empty", ""); + } + + if (val && !cm.hasFocus()) onBlur(cm); + }); + + function clearPlaceholder(cm) { + if (cm.state.placeholder) { + cm.state.placeholder.parentNode.removeChild(cm.state.placeholder); + cm.state.placeholder = null; + } + } + function setPlaceholder(cm) { + clearPlaceholder(cm); + var elt = cm.state.placeholder = document.createElement("pre"); + elt.style.cssText = "height: 0; overflow: visible"; + elt.style.direction = cm.getOption("direction"); + elt.className = "CodeMirror-placeholder CodeMirror-line-like"; + var placeHolder = cm.getOption("placeholder") + if (typeof placeHolder == "string") placeHolder = document.createTextNode(placeHolder) + elt.appendChild(placeHolder) + cm.display.lineSpace.insertBefore(elt, cm.display.lineSpace.firstChild); + } + + function onComposition(cm) { + setTimeout(function () { + var empty = false + if (cm.lineCount() == 1) { + var input = cm.getInputField() + empty = input.nodeName == "TEXTAREA" ? !cm.getLine(0).length + : !/[^\u200b]/.test(input.querySelector(".CodeMirror-line").textContent) + } + if (empty) setPlaceholder(cm) + else clearPlaceholder(cm) + }, 20) + } + + function onBlur(cm) { + if (isEmpty(cm)) setPlaceholder(cm); + } + function onChange(cm) { + var wrapper = cm.getWrapperElement(), empty = isEmpty(cm); + wrapper.className = wrapper.className.replace(" CodeMirror-empty", "") + (empty ? " CodeMirror-empty" : ""); + + if (empty) setPlaceholder(cm); + else clearPlaceholder(cm); + } + + function isEmpty(cm) { + return (cm.lineCount() === 1) && (cm.getLine(0) === ""); + } +}); diff --git a/webroot/js/main.js b/webroot/js/main.js index caf5d24..c4383ff 100644 --- a/webroot/js/main.js +++ b/webroot/js/main.js @@ -279,10 +279,12 @@ $(document).ready(() => { overloadBSDropdown(); addSupportOfNestedDropdown(); - const debouncedGlobalSearch = debounce(performGlobalSearch, 400) - $('#globalSearch') - .keydown(debouncedGlobalSearch) - .keydown(focusSearchResults); + if (window.debounce) { + const debouncedGlobalSearch = debounce(performGlobalSearch, 400) + $('#globalSearch') + .keydown(debouncedGlobalSearch) + .keydown(focusSearchResults); + } $('.lock-sidebar a.btn-lock-sidebar').click(() => { const $sidebar = $('.sidebar') diff --git a/webroot/theme/package-lock.json b/webroot/theme/package-lock.json index c19ec32..70d2d66 100644 --- a/webroot/theme/package-lock.json +++ b/webroot/theme/package-lock.json @@ -1,1555 +1,378 @@ { "name": "theme", "version": "1.0.0", - "lockfileVersion": 1, + "lockfileVersion": 2, "requires": true, - "dependencies": { - "@babel/code-frame": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", - "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", - "requires": { - "@babel/highlight": "^7.14.5" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.15.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", - "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==" - }, - "@babel/highlight": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", - "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", - "requires": { - "@babel/helper-validator-identifier": "^7.14.5", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "@types/minimist": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz", - "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==" - }, - "@types/normalize-package-data": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", - "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==" - }, - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" - }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "amdefine": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=" - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" - }, - "are-we-there-yet": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz", - "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==", - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=" - }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "assert-plus": { + "packages": { + "": { + "name": "theme", "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - }, - "async-foreach": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", - "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=" - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" - }, - "aws4": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", - "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "requires": { - "tweetnacl": "^0.14.3" + "license": "ISC", + "dependencies": { + "bootstrap": "^5.1.1", + "sass": "^1.45.0" } }, + "node_modules/@popperjs/core": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.0.tgz", + "integrity": "sha512-zrsUxjLOKAzdewIDRWy9nsV1GQsKBCWaGwsZQlCgr6/q+vjyZhFgqedLfFBuI9anTPEUT4APq9Mu0SZBTzIcGQ==", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/bootstrap": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.1.3.tgz", + "integrity": "sha512-fcQztozJ8jToQWXxVuEyXWW+dSo8AiXWKwiSSrKWsRB/Qt+Ewwza+JWoLKiTuQLaEPhdNAJ7+Dosc9DOIqNy7Q==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + }, + "peerDependencies": { + "@popperjs/core": "^2.10.2" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", + "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/immutable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0.tgz", + "integrity": "sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/picomatch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/sass": { + "version": "1.45.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.45.0.tgz", + "integrity": "sha512-ONy5bjppoohtNkFJRqdz1gscXamMzN3wQy1YH9qO2FiNpgjLhpz/IPRGg0PpCjyz/pWfCOaNEaiEGCcjOFAjqw==", + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/source-map-js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.1.tgz", + "integrity": "sha512-4+TN2b3tqOCd/kaGRJ/sTYA0tR0mdXx26ipdolxcwtJVqEnqNYvlCAt1q3ypy4QMlYus+Zh34RNtYLoq2oQ4IA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + } + }, + "dependencies": { + "@popperjs/core": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.0.tgz", + "integrity": "sha512-zrsUxjLOKAzdewIDRWy9nsV1GQsKBCWaGwsZQlCgr6/q+vjyZhFgqedLfFBuI9anTPEUT4APq9Mu0SZBTzIcGQ==", + "peer": true + }, + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" + }, "bootstrap": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.1.1.tgz", - "integrity": "sha512-/jUa4sSuDZWlDLQ1gwQQR8uoYSvLJzDd8m5o6bPKh3asLAMYVZKdRCjb1joUd5WXf0WwCNzd2EjwQQhupou0dA==" + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.1.3.tgz", + "integrity": "sha512-fcQztozJ8jToQWXxVuEyXWW+dSo8AiXWKwiSSrKWsRB/Qt+Ewwza+JWoLKiTuQLaEPhdNAJ7+Dosc9DOIqNy7Q==", + "requires": {} }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" - }, - "camelcase-keys": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", - "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", - "requires": { - "camelcase": "^5.3.1", - "map-obj": "^4.0.0", - "quick-lru": "^4.0.1" - } - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" - }, - "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", - "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "requires": { - "ansi-regex": "^4.1.0" - } - } - } - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" - }, - "core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" - }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "requires": { - "assert-plus": "^1.0.0" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" - }, - "decamelize-keys": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz", - "integrity": "sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=", - "requires": { - "decamelize": "^1.1.0", - "map-obj": "^1.0.0" - }, - "dependencies": { - "map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=" - } - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" - }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" - }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" - }, - "env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==" - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" - }, - "extend": { + "braces": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" + "fill-range": "^7.0.1" } }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" - }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "chokidar": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", + "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" } }, - "fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "requires": { - "minipass": "^3.0.0" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "gaze": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", - "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", - "requires": { - "globule": "^1.0.0" - } - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" - }, - "get-stdin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=" - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "requires": { - "assert-plus": "^1.0.0" - } - }, - "glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "globule": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/globule/-/globule-1.3.3.tgz", - "integrity": "sha512-mb1aYtDbIjTu4ShMB85m3UzjX9BVKe9WCzsnfMSZk+K5GpIbBOexgg4PPCt5eHDEG5/ZQAUX2Kct02zfiPLsKg==", - "requires": { - "glob": "~7.1.1", - "lodash": "~4.17.10", - "minimatch": "~3.0.2" - }, - "dependencies": { - "glob": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", - "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } - } - }, - "graceful-fs": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", - "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==" - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" - }, - "har-validator": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", - "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", - "requires": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" - } - }, - "hard-rejection": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", - "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==" - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" - }, - "has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" - }, - "hosted-git-info": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.0.2.tgz", - "integrity": "sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg==", - "requires": { - "lru-cache": "^6.0.0" - } - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==" - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" - }, - "is-core-module": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.7.0.tgz", - "integrity": "sha512-ByY+tjCciCr+9nLryBYcSD50EOGWt95c7tIsKTG1J2ixKKXPvF7Ej3AVd+UfDydAJom3biBGDBALaO79ktwgEQ==", - "requires": { - "has": "^1.0.3" - } - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=" - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, - "js-base64": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz", - "integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==" - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" - }, - "json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" - }, - "lines-and-columns": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", - "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=" - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "requires": { - "p-locate": "^4.1.0" - } - }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "requires": { - "yallist": "^4.0.0" - } - }, - "map-obj": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", - "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==" - }, - "meow": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", - "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==", - "requires": { - "@types/minimist": "^1.2.0", - "camelcase-keys": "^6.2.2", - "decamelize": "^1.2.0", - "decamelize-keys": "^1.1.0", - "hard-rejection": "^2.1.0", - "minimist-options": "4.1.0", - "normalize-package-data": "^3.0.0", - "read-pkg-up": "^7.0.1", - "redent": "^3.0.0", - "trim-newlines": "^3.0.0", - "type-fest": "^0.18.0", - "yargs-parser": "^20.2.3" - } - }, - "mime-db": { - "version": "1.50.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.50.0.tgz", - "integrity": "sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A==" - }, - "mime-types": { - "version": "2.1.33", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.33.tgz", - "integrity": "sha512-plLElXp7pRDd0bNZHw+nMd52vRYjLwQjygaNg7ddJ2uJtTlmnTCjWuPKxVu6//AdaRuME84SvLW91sIkBqGT0g==", - "requires": { - "mime-db": "1.50.0" - } - }, - "min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==" - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist-options": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", - "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", - "requires": { - "arrify": "^1.0.1", - "is-plain-obj": "^1.1.0", - "kind-of": "^6.0.3" - } - }, - "minipass": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.5.tgz", - "integrity": "sha512-+8NzxD82XQoNKNrl1d/FSi+X8wAEWR+sbYAfIvub4Nz0d22plFG72CEVVaufV8PNf4qSslFTD8VMOxNVhHCjTw==", - "requires": { - "yallist": "^4.0.0" - } - }, - "minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "requires": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - } - }, - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" - }, - "nan": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz", - "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==" - }, - "node-gyp": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-7.1.2.tgz", - "integrity": "sha512-CbpcIo7C3eMu3dL1c3d0xw449fHIGALIJsRP4DDPHpyiW8vcriNY7ubh9TE4zEKfSxscY7PjeFnshE7h75ynjQ==", - "requires": { - "env-paths": "^2.2.0", - "glob": "^7.1.4", - "graceful-fs": "^4.2.3", - "nopt": "^5.0.0", - "npmlog": "^4.1.2", - "request": "^2.88.2", - "rimraf": "^3.0.2", - "semver": "^7.3.2", - "tar": "^6.0.2", - "which": "^2.0.2" - } - }, - "node-sass": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-6.0.1.tgz", - "integrity": "sha512-f+Rbqt92Ful9gX0cGtdYwjTrWAaGURgaK5rZCWOgCNyGWusFYHhbqCCBoFBeat+HKETOU02AyTxNhJV0YZf2jQ==", - "requires": { - "async-foreach": "^0.1.3", - "chalk": "^1.1.1", - "cross-spawn": "^7.0.3", - "gaze": "^1.0.0", - "get-stdin": "^4.0.1", - "glob": "^7.0.3", - "lodash": "^4.17.15", - "meow": "^9.0.0", - "nan": "^2.13.2", - "node-gyp": "^7.1.0", - "npmlog": "^4.0.0", - "request": "^2.88.0", - "sass-graph": "2.2.5", - "stdout-stream": "^1.4.0", - "true-case-path": "^1.0.2" - } - }, - "nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "requires": { - "abbrev": "1" - } - }, - "normalize-package-data": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", - "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", - "requires": { - "hosted-git-info": "^4.0.1", - "is-core-module": "^2.5.0", - "semver": "^7.3.4", - "validate-npm-package-license": "^3.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "requires": { - "p-limit": "^2.2.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" - }, - "parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" - }, - "path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" - }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "psl": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" - }, - "quick-lru": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", - "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==" - }, - "read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "requires": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "dependencies": { - "hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" - }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" - }, - "type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==" - } - } - }, - "read-pkg-up": { + "fill-range": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", "requires": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - }, - "dependencies": { - "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==" - } + "to-regex-range": "^5.0.1" } }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "optional": true }, - "redent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "requires": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - } - }, - "request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" - }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" - }, - "resolve": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", - "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", - "requires": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" - } - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { + "glob-parent": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "sass-graph": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.5.tgz", - "integrity": "sha512-VFWDAHOe6mRuT4mZRd4eKE+d8Uedrk6Xnh7Sh9b4NGufQLQjOrvf/MQoOdx+0s92L89FeyUUNfU597j/3uNpag==", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "requires": { - "glob": "^7.0.0", - "lodash": "^4.0.0", - "scss-tokenizer": "^0.2.3", - "yargs": "^13.3.2" + "is-glob": "^4.0.1" } }, - "scss-tokenizer": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", - "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=", - "requires": { - "js-base64": "^2.1.8", - "source-map": "^0.4.2" - } - }, - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "requires": { - "lru-cache": "^6.0.0" - } - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" - }, - "signal-exit": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.5.tgz", - "integrity": "sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==" - }, - "source-map": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", - "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", - "requires": { - "amdefine": ">=0.0.4" - } - }, - "spdx-correct": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", - "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==" - }, - "spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.10.tgz", - "integrity": "sha512-oie3/+gKf7QtpitB0LYLETe+k8SifzsX4KixvpOsbI6S0kRiRQ5MKOio8eMSAKQ17N06+wdEOXRiId+zOxo0hA==" - }, - "sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "stdout-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz", - "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==", - "requires": { - "readable-stream": "^2.0.1" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "requires": { - "min-indent": "^1.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - }, - "tar": { - "version": "6.1.11", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", - "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", - "requires": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^3.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - } - }, - "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - }, - "trim-newlines": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", - "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==" - }, - "true-case-path": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz", - "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==", - "requires": { - "glob": "^7.1.2" - } - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" - }, - "type-fest": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", - "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==" - }, - "uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "requires": { - "punycode": "^2.1.0" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" - }, - "validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - }, - "dependencies": { - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - } - } - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "requires": { - "isexe": "^2.0.0" - } - }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" - }, - "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", - "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "requires": { - "ansi-regex": "^4.1.0" - } - } - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" - }, - "yallist": { + "immutable": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0.tgz", + "integrity": "sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==" }, - "yargs": { - "version": "13.3.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", - "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "requires": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.2" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "requires": { - "locate-path": "^3.0.0" - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "requires": { - "p-limit": "^2.0.0" - } - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "requires": { - "ansi-regex": "^4.1.0" - } - }, - "yargs-parser": { - "version": "13.1.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", - "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } + "binary-extensions": "^2.0.0" } }, - "yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==" + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + }, + "picomatch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==" + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "requires": { + "picomatch": "^2.2.1" + } + }, + "sass": { + "version": "1.45.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.45.0.tgz", + "integrity": "sha512-ONy5bjppoohtNkFJRqdz1gscXamMzN3wQy1YH9qO2FiNpgjLhpz/IPRGg0PpCjyz/pWfCOaNEaiEGCcjOFAjqw==", + "requires": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + } + }, + "source-map-js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.1.tgz", + "integrity": "sha512-4+TN2b3tqOCd/kaGRJ/sTYA0tR0mdXx26ipdolxcwtJVqEnqNYvlCAt1q3ypy4QMlYus+Zh34RNtYLoq2oQ4IA==" + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "requires": { + "is-number": "^7.0.0" + } } } } diff --git a/webroot/theme/package.json b/webroot/theme/package.json index 476def3..69bfdc1 100644 --- a/webroot/theme/package.json +++ b/webroot/theme/package.json @@ -5,13 +5,14 @@ "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "autobuild": "node-sass --watch scss -o ../css/themes", - "build": "node-sass scss -o ../css/themes" + "autobuild": "sass --no-source-map --watch scss:../css/themes", + "build": "sass --no-source-map scss:../css/themes", + "build-with-map": "sass scss:../css/themes" }, "author": "", "license": "ISC", "dependencies": { "bootstrap": "^5.1.1", - "node-sass": "^6.0.1" + "sass": "^1.45.0" } }