diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index b6d56de..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "typescript.tsdk": "./y-web/node_modules/typescript/lib" -} diff --git a/.vscode/y.code-workspace b/.vscode/y.code-workspace new file mode 100644 index 0000000..a5e00a3 --- /dev/null +++ b/.vscode/y.code-workspace @@ -0,0 +1,12 @@ +{ + "folders": [ + { + "path": ".." + } + ], + "settings": { + "typescript.tsdk": "./y-web/node_modules/typescript/lib", + "rust-analyzer.linkedProjects": ["./y-server/Cargo.toml"], + "rust-analyzer.showUnlinkedFileNotification": false + } +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..892f328 --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright 2024 Maksym (sk-m) Kurysh + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d47e268 --- /dev/null +++ b/README.md @@ -0,0 +1,99 @@ +# y + +## Installing y (production environment) + +y comes in two parts: a _server_ and _web distribution_. + +You can download the latest version of y on the [releases](https://github.com/sk-m/y/releases) page. + +### 1. Prerequisites + +In order to set up y on your server, you first will need: + +1. A linux machine. +2. PostgreSQL. +3. A web server (nginx/apache/etc.). + +### 2. Setting up PostgreSQL + +After installing postgresql and initializing your database cluster (see postgresql documentation), do the following: + +1. Launch `psql` (you might need to log in as the `postgres` user first - `sudo su - postgres`) +2. Create a database for y: `CREATE DATABASE y;` +3. Create a user: `CREATE USER y_user WITH PASSWORD 'passw0rd!';` +4. Grant required privileges to that user: `GRANT ALL PRIVILEGES ON DATABASE "y" to y_user;` +5. Set that user as the owner of the database: `ALTER DATABASE y OWNER TO y_user;` + +### 3. Installing y server + +_y server_ is, for the most part, just one binary file. You can either run it from a cli, if you want to test it out first, or set up a service for it, which is a more long-term soultion. + +Before starting the server, edit the `.env` file and update it according to your setup. You mainly want to make sure that the `DATABASE_URL` variable is correct, everything else should be fine by default. Then, + +- to start the server from a cli, just run `./y-server`, or +- to set up y-server as a service, run the `./install.sh` script. + +### 4. Serving web files + +_y server_ does **not** serve y's web files, so you will have to use a web server for that. + +The next section is written for **nginx**, but you can use any web server you like. + +1. Install **nginx**. +2. Copy y's `www` folder anywhere you like, those files will be hosted by **nginx**. +3. Add the following configuration to your `/etc/nginx/nginx.conf`: + +```text +server { + # Example server config: + server_name localhost; + listen 80; + + # Replace with the path to your www folder: + root /usr/share/nginx/y/www; + + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + location /api/ { + proxy_pass http://localhost:8080; + proxy_set_header Host $host; + } +} +``` + +Don't forget to restart **nginx** after updating `ninx.conf`. + +### 5. Logging in + +If you've done everything correctly, you should be able to open y in your browser and log into a default admin user: + +- username: `admin` +- password: `admin` + +## Trying out y (development environment) + +Setting up a development environment should be very straightforward. + +1. Install PostgreSQL and set it up using a tutorial in the "**Installing y (production environment)**" section. +2. Install **NodeJS**, **pnpm** and **Rust**. +3. Clone the repo: `git clone https://github.com/sk-m/y.git` +4. Start the server: + + 1. `cd y/y-server` + 2. `cp .env.example .env` + 3. update `.env` according to your setup + 4. `cargo run` + +5. Start up the frontend: + + 1. `cd y/y-web` + 2. `pnpm install` + 3. `pnpm run dev` + +## Footnotes + +[Support Ukraine](https://u24.gov.ua/) diff --git a/y-server/.env.example b/y-server/.env.example new file mode 100644 index 0000000..5fdb899 --- /dev/null +++ b/y-server/.env.example @@ -0,0 +1,4 @@ +SERVER_ADDRESS=127.0.0.1 +SERVER_PORT=8080 + +DATABASE_URL=postgres://y_user:passw0rd!@127.0.0.1:5432/y diff --git a/y-server/.gitignore b/y-server/.gitignore new file mode 100644 index 0000000..796603f --- /dev/null +++ b/y-server/.gitignore @@ -0,0 +1,2 @@ +target +.env \ No newline at end of file diff --git a/y-server/Cargo.lock b/y-server/Cargo.lock new file mode 100644 index 0000000..de4f3ff --- /dev/null +++ b/y-server/Cargo.lock @@ -0,0 +1,2648 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "actix-codec" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617a8268e3537fe1d8c9ead925fca49ef6400927ee7bc26750e90ecee14ce4b8" +dependencies = [ + "bitflags 1.3.2", + "bytes", + "futures-core", + "futures-sink", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "actix-http" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2079246596c18b4a33e274ae10c0e50613f4d32a4198e09c7b93771013fed74" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "ahash 0.8.3", + "base64", + "bitflags 1.3.2", + "brotli", + "bytes", + "bytestring", + "derive_more", + "encoding_rs", + "flate2", + "futures-core", + "h2", + "http", + "httparse", + "httpdate", + "itoa", + "language-tags", + "local-channel", + "mime", + "percent-encoding", + "pin-project-lite", + "rand", + "sha1", + "smallvec", + "tokio", + "tokio-util", + "tracing", + "zstd", +] + +[[package]] +name = "actix-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" +dependencies = [ + "quote", + "syn 2.0.28", +] + +[[package]] +name = "actix-router" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66ff4d247d2b160861fa2866457e85706833527840e4133f8f49aa423a38799" +dependencies = [ + "bytestring", + "http", + "regex", + "serde", + "tracing", +] + +[[package]] +name = "actix-rt" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15265b6b8e2347670eb363c47fc8c75208b4a4994b27192f345fcbe707804f3e" +dependencies = [ + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e8613a75dd50cc45f473cee3c34d59ed677c0f7b44480ce3b8247d7dc519327" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio", + "num_cpus", + "socket2", + "tokio", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" +dependencies = [ + "futures-core", + "paste", + "pin-project-lite", +] + +[[package]] +name = "actix-utils" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd3cb42f9566ab176e1ef0b8b3a896529062b4efc6be0123046095914c4c1c96" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-utils", + "actix-web-codegen", + "ahash 0.7.6", + "bytes", + "bytestring", + "cfg-if", + "cookie", + "derive_more", + "encoding_rs", + "futures-core", + "futures-util", + "http", + "itoa", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2", + "time 0.3.25", + "url", +] + +[[package]] +name = "actix-web-codegen" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2262160a7ae29e3415554a3f1fc04c764b1540c116aa524683208078b7a75bc9" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "addr2line" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-executor" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c1da3ae8dabd9c00f453a329dfe1fb28da3c0a72e2478cdcd93171740c20499" +dependencies = [ + "async-lock", + "async-task", + "concurrent-queue", + "fastrand 2.0.1", + "futures-lite", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1b6f5d7df27bd294849f8eec66ecfc63d11814df7a4f5d74168a2394467b776" +dependencies = [ + "async-channel", + "async-executor", + "async-io", + "async-lock", + "blocking", + "futures-lite", + "once_cell", +] + +[[package]] +name = "async-io" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +dependencies = [ + "async-lock", + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-lite", + "log", + "parking", + "polling", + "rustix 0.37.25", + "slab", + "socket2", + "waker-fn", +] + +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-std" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" +dependencies = [ + "async-channel", + "async-global-executor", + "async-io", + "async-lock", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-task" +version = "4.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9441c6b2fe128a7c2bf680a44c34d0df31ce09e5b7e401fcca3faa483dbc921" + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" +dependencies = [ + "serde", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blocking" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c36a4d0d48574b3dd360b4b7d95cc651d2b6557b6402848a27d4b228a473e2a" +dependencies = [ + "async-channel", + "async-lock", + "async-task", + "fastrand 2.0.1", + "futures-io", + "futures-lite", + "piper", + "tracing", +] + +[[package]] +name = "brotli" +version = "3.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b6561fd3f895a11e8f72af2cb7d22e08366bebc2b6b57f7744c4bda27034744" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" + +[[package]] +name = "bytestring" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "238e4886760d98c4f899360c834fa93e62cf7f721ac3c2da375cbdf4b8679aae" +dependencies = [ + "bytes", +] + +[[package]] +name = "cc" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c6b2562119bf28c3439f7f02db99faf0aa1a8cdfe5772a2ee155d32227239f0" +dependencies = [ + "jobserver", + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "time 0.1.45", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "concurrent-queue" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f057a694a54f12365049b0958a1685bb52d567f5593b355fbf685838e873d400" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "percent-encoding", + "time 0.3.25", + "version_check", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "cpufeatures" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484" + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.28", +] + +[[package]] +name = "darling_macro" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "der" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7684a49fb1af197853ef7b2ee694bc1f5b4179556f1e5710e1760c5db6f5e929" +dependencies = [ + "serde", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 1.0.109", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +dependencies = [ + "serde", +] + +[[package]] +name = "encoding_rs" +version = "0.8.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[package]] +name = "flate2" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +dependencies = [ + "futures-core", + "futures-sink", + "spin 0.9.8", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" + +[[package]] +name = "futures-executor" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-macro" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "futures-sink" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" + +[[package]] +name = "futures-task" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" + +[[package]] +name = "futures-util" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "gimli" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" + +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "h2" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 1.9.3", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +dependencies = [ + "ahash 0.8.3", + "allocator-api2", +] + +[[package]] +name = "hashlink" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +dependencies = [ + "hashbrown 0.14.0", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "http" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "iana-time-zone" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown 0.14.0", + "serde", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys", +] + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "jobserver" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin 0.5.2", +] + +[[package]] +name = "libc" +version = "0.2.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" + +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + +[[package]] +name = "libsqlite3-sys" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "linux-raw-sys" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" + +[[package]] +name = "local-channel" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f303ec0e94c6c54447f84f3b0ef7af769858a9c4ef56ef2a986d3dcd4c3fc9c" +dependencies = [ + "futures-core", + "futures-sink", + "futures-util", + "local-waker", +] + +[[package]] +name = "local-waker" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34f76eb3611940e0e7d53a9aaa4e6a3151f69541a282fd0dad5571420c53ff1" + +[[package]] +name = "lock_api" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" +dependencies = [ + "value-bag", +] + +[[package]] +name = "md-5" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" +dependencies = [ + "digest", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "parking" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e52c774a4c39359c1d1c52e43f73dd91a75a614652c825408eec30c95a9b2067" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", + "password-hash", + "sha2", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "pin-project-lite" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c516611246607d0c04186886dbb3a754368ef82c79e9827a802c6d836dd111c" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" +dependencies = [ + "atomic-waker", + "fastrand 2.0.1", + "futures-io", +] + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "polling" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "concurrent-queue", + "libc", + "log", + "pin-project-lite", + "windows-sys", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "regex" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" + +[[package]] +name = "rsa" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab43bb47d23c1a631b4b680199a45255dce26fa9ab2fa902581f624ff13e6a8" +dependencies = [ + "byteorder", + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-iter", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.37.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4eb579851244c2c03e7c24f501c3432bed80b8f720af1d6e5b0e0f01555a035" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys", +] + +[[package]] +name = "rustix" +version = "0.38.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f" +dependencies = [ + "bitflags 2.3.3", + "errno", + "libc", + "linux-raw-sys 0.4.10", + "windows-sys", +] + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" + +[[package]] +name = "serde" +version = "1.0.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32ac8da02677876d532745a130fc9d8e6edfa81a269b107c5b00829b91d8eb3c" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aafe972d60b0b9bee71a91b92fee2d4fb3c9d7e8f6b179aa99f27203d99a4816" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "serde_json" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ca3b16a3d82c4088f343b7480a93550b3eabe1a358569c2dfe38bbcead07237" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.0.0", + "serde", + "serde_json", + "serde_with_macros", + "time 0.3.25", +] + +[[package]] +name = "serde_with_macros" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e6be15c453eb305019bfa438b1593c731f36a289a7853f7707ee29e870b3b3c" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "slab" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" + +[[package]] +name = "socket2" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlformat" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b7b278788e7be4d0d29c0f39497a0eef3fba6bbc8e70d8bf7fde46edeaa9e85" +dependencies = [ + "itertools", + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e50c216e3624ec8e7ecd14c6a6a6370aad6ee5d8cfc3ab30b5162eeeef2ed33" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d6753e460c998bbd4cd8c6f0ed9a64346fcca0723d6e75e52fdc351c5d2169d" +dependencies = [ + "ahash 0.8.3", + "async-io", + "async-std", + "atoi", + "byteorder", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "dotenvy", + "either", + "event-listener", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashlink", + "hex", + "indexmap 2.0.0", + "log", + "memchr", + "once_cell", + "paste", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlformat", + "thiserror", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "sqlx-macros" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a793bb3ba331ec8359c1853bd39eed32cdd7baaf22c35ccf5c92a7e8d1189ec" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 1.0.109", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a4ee1e104e00dedb6aa5ffdd1343107b0a4702e862a84320ee7cc74782d96fc" +dependencies = [ + "async-std", + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 1.0.109", + "tempfile", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "864b869fdf56263f4c95c45483191ea0af340f9f3e3e7b4d57a61c7c87a970db" +dependencies = [ + "atoi", + "base64", + "bitflags 2.3.3", + "byteorder", + "bytes", + "chrono", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb7ae0e6a97fb3ba33b23ac2671a5ce6e3cabe003f451abd5a56e7951d975624" +dependencies = [ + "atoi", + "base64", + "bitflags 2.3.3", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand", + "serde", + "serde_json", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59dc83cf45d89c555a577694534fcd1b55c545a816c816ce51f20bbe56a4f3f" +dependencies = [ + "atoi", + "chrono", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "stringprep" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3737bde7edce97102e0e2b15365bf7a20bfdb5f60f4f9e8d7004258a51a8da" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" +dependencies = [ + "cfg-if", + "fastrand 2.0.1", + "redox_syscall", + "rustix 0.38.8", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "1.0.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "time" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fdd63d58b18d663fbdf70e049f00a22c8e42be082203be7f26589213cd75ea" +dependencies = [ + "deranged", + "itoa", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" + +[[package]] +name = "time-macros" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb71511c991639bb078fd5bf97757e03914361c48100d52878b8e52b46fb92cd" +dependencies = [ + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" +dependencies = [ + "autocfg", + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "windows-sys", +] + +[[package]] +name = "tokio-util" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "tracing-core" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + +[[package]] +name = "url" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "uuid" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" +dependencies = [ + "getrandom", +] + +[[package]] +name = "value-bag" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d92ccd67fb88503048c01b59152a04effd0782d035a83a6d256ce6085f08f4a3" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "waker-fn" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.28", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" + +[[package]] +name = "web-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "whoami" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "y-server" +version = "0.0.0" +dependencies = [ + "actix-web", + "chrono", + "dotenvy", + "futures", + "pbkdf2", + "rand", + "rand_core", + "serde", + "serde_json", + "serde_with", + "sqlx", + "uuid", +] + +[[package]] +name = "zeroize" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" + +[[package]] +name = "zstd" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a27595e173641171fc74a1232b7b1c7a7cb6e18222c11e9dfb9888fa424c53c" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "6.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee98ffd0b48ee95e6c5168188e44a54550b1564d9d530ee21d5f0eaed1069581" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.8+zstd.1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c" +dependencies = [ + "cc", + "libc", + "pkg-config", +] diff --git a/y-server/Cargo.toml b/y-server/Cargo.toml new file mode 100644 index 0000000..842ed4c --- /dev/null +++ b/y-server/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "y-server" +version = "0.0.0" +edition = "2021" +authors = ["sk-m "] +description = "y API server" +repository = "https://github.com/sk-m/y" +publish = false + +[dependencies] +actix-web = "4" +dotenvy = "0.15" +pbkdf2 = { version = "0.12", features = ["simple"] } +rand_core = { version = "0.6", features = ["std"] } +serde = { version = "1.0.183", features = ["derive"] } +uuid = { version = "1.4.1", features = ["v4"] } +chrono = "0.4.26" +rand = "0.8.5" +serde_with = { version = "3.3.0", features = ["chrono"] } +sqlx = { version = "0.7", features = ["runtime-async-std", "postgres", "migrate", "chrono", "uuid", "json"] } +futures = "0.3.28" +serde_json = "1.0.107" diff --git a/y-server/install.sh b/y-server/install.sh new file mode 100755 index 0000000..dab8087 --- /dev/null +++ b/y-server/install.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +echo "This script will set up a service for y-server." +echo +echo "This directory ($(pwd)) will be set as the working directory for y." +echo "Please make sure to move y-server's files to a desired location before continuing." + +if [ "$(id -u)" -ne 0 ]; then + echo + echo "Warning: This script should be run as root!" +fi + +echo +read -p "Press enter to start: " + +service_definition="[Unit] +Description=y server +Before=postgresql.service + +[Install] +WantedBy=default.target + +[Service] +WorkingDirectory=$(pwd) +ExecStart=$(pwd)/y-server +" + +echo "$service_definition" > /etc/systemd/system/y-server.service +systemctl daemon-reload + +echo +echo "Service written to /etc/systemd/system/y-server.service" +echo +echo "To start y-server, run 'systemctl start y-server'" +echo "You can also make y-server start on boot with 'systemctl enable y-server'" \ No newline at end of file diff --git a/y-server/migrations/20231015231212_create_users_table.down.sql b/y-server/migrations/20231015231212_create_users_table.down.sql new file mode 100644 index 0000000..cf5a705 --- /dev/null +++ b/y-server/migrations/20231015231212_create_users_table.down.sql @@ -0,0 +1 @@ +DROP TABLE public.users diff --git a/y-server/migrations/20231015231212_create_users_table.up.sql b/y-server/migrations/20231015231212_create_users_table.up.sql new file mode 100644 index 0000000..def0588 --- /dev/null +++ b/y-server/migrations/20231015231212_create_users_table.up.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS public.users +( + id serial NOT NULL, + username character varying(128) NOT NULL, + password character varying(512), + PRIMARY KEY (id), + UNIQUE (username) +); diff --git a/y-server/migrations/20231015231257_create_user_sessions_table.down.sql b/y-server/migrations/20231015231257_create_user_sessions_table.down.sql new file mode 100644 index 0000000..177aa33 --- /dev/null +++ b/y-server/migrations/20231015231257_create_user_sessions_table.down.sql @@ -0,0 +1 @@ +DROP TABLE public.user_sessions diff --git a/y-server/migrations/20231015231257_create_user_sessions_table.up.sql b/y-server/migrations/20231015231257_create_user_sessions_table.up.sql new file mode 100644 index 0000000..f06cf2e --- /dev/null +++ b/y-server/migrations/20231015231257_create_user_sessions_table.up.sql @@ -0,0 +1,16 @@ +CREATE TABLE IF NOT EXISTS public.user_sessions +( + id serial NOT NULL, + session_id uuid NOT NULL, + session_key character varying(256) NOT NULL, + user_id integer NOT NULL, + created_at timestamp without time zone NOT NULL, + expires_on timestamp without time zone, + PRIMARY KEY (id), + UNIQUE (session_id), + FOREIGN KEY (user_id) + REFERENCES public.users (id) MATCH SIMPLE + ON UPDATE CASCADE + ON DELETE CASCADE + NOT VALID +); diff --git a/y-server/migrations/20231015231325_users_add_created_at.down.sql b/y-server/migrations/20231015231325_users_add_created_at.down.sql new file mode 100644 index 0000000..5822826 --- /dev/null +++ b/y-server/migrations/20231015231325_users_add_created_at.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE IF EXISTS public.users DROP COLUMN IF EXISTS created_at; +-- Add down migration script here diff --git a/y-server/migrations/20231015231325_users_add_created_at.up.sql b/y-server/migrations/20231015231325_users_add_created_at.up.sql new file mode 100644 index 0000000..64e0079 --- /dev/null +++ b/y-server/migrations/20231015231325_users_add_created_at.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE IF EXISTS public.users + ADD COLUMN created_at timestamp without time zone NOT NULL DEFAULT CURRENT_TIMESTAMP; \ No newline at end of file diff --git a/y-server/migrations/20231017212853_create_group_tables.down.sql b/y-server/migrations/20231017212853_create_group_tables.down.sql new file mode 100644 index 0000000..1f16c8b --- /dev/null +++ b/y-server/migrations/20231017212853_create_group_tables.down.sql @@ -0,0 +1,3 @@ +DROP TABLE public.user_group_rights; +DROP TABLE public.user_group_membership; +DROP TABLE public.user_groups; diff --git a/y-server/migrations/20231017212853_create_group_tables.up.sql b/y-server/migrations/20231017212853_create_group_tables.up.sql new file mode 100644 index 0000000..f40dace --- /dev/null +++ b/y-server/migrations/20231017212853_create_group_tables.up.sql @@ -0,0 +1,36 @@ +CREATE TABLE IF NOT EXISTS public.user_groups +( + id serial NOT NULL, + name character varying(256) NOT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS public.user_group_membership +( + user_id integer NOT NULL, + group_id integer NOT NULL, + PRIMARY KEY (user_id, group_id), + FOREIGN KEY (user_id) + REFERENCES public.users (id) MATCH SIMPLE + ON UPDATE CASCADE + ON DELETE CASCADE + NOT VALID, + FOREIGN KEY (group_id) + REFERENCES public.user_groups (id) MATCH SIMPLE + ON UPDATE CASCADE + ON DELETE CASCADE + NOT VALID +); + +CREATE TABLE IF NOT EXISTS public.user_group_rights +( + group_id integer NOT NULL, + right_name character varying(256) NOT NULL, + right_options jsonb NOT NULL, + PRIMARY KEY (group_id, right_name), + FOREIGN KEY (group_id) + REFERENCES public.user_groups (id) MATCH SIMPLE + ON UPDATE CASCADE + ON DELETE CASCADE + NOT VALID +); diff --git a/y-server/migrations/20231210231237_add_system_user_groups.down.sql b/y-server/migrations/20231210231237_add_system_user_groups.down.sql new file mode 100644 index 0000000..5bdb77f --- /dev/null +++ b/y-server/migrations/20231210231237_add_system_user_groups.down.sql @@ -0,0 +1 @@ +ALTER TABLE IF EXISTS public.user_groups DROP COLUMN IF EXISTS group_type; diff --git a/y-server/migrations/20231210231237_add_system_user_groups.up.sql b/y-server/migrations/20231210231237_add_system_user_groups.up.sql new file mode 100644 index 0000000..f95de7f --- /dev/null +++ b/y-server/migrations/20231210231237_add_system_user_groups.up.sql @@ -0,0 +1,4 @@ +ALTER TABLE IF EXISTS public.user_groups + ADD COLUMN group_type character varying(128); + +INSERT INTO user_groups (name, group_type) VALUES ('everyone', 'everyone'), ('user', 'user'); diff --git a/y-server/migrations/20240106053017_create_default_user_and_groups.down.sql b/y-server/migrations/20240106053017_create_default_user_and_groups.down.sql new file mode 100644 index 0000000..09fb253 --- /dev/null +++ b/y-server/migrations/20240106053017_create_default_user_and_groups.down.sql @@ -0,0 +1,2 @@ +DELETE FROM users WHERE username = 'admin'; +DELETE FROM user_groups WHERE name = 'admin'; diff --git a/y-server/migrations/20240106053017_create_default_user_and_groups.up.sql b/y-server/migrations/20240106053017_create_default_user_and_groups.up.sql new file mode 100644 index 0000000..3c6fdc9 --- /dev/null +++ b/y-server/migrations/20240106053017_create_default_user_and_groups.up.sql @@ -0,0 +1,12 @@ +DO $$ + DECLARE admin_group_id integer; + DECLARE admin_user_id integer; +BEGIN + -- Password is 'admin' + INSERT INTO users (username, password) VALUES ('admin', '$pbkdf2-sha256$i=600000,l=32$Lt/elXAzTITlKrCFLUvBcQ$w67zGVHY24ZeGQtB+9Pu8bp0EaQvncwk8yB8BinkNlU') RETURNING id INTO admin_user_id; + INSERT INTO user_groups (name) VALUES ('admin') RETURNING id INTO admin_group_id; + + INSERT INTO user_group_rights (group_id, right_name, right_options) VALUES (admin_group_id, 'manage_user_groups', '{"allow_creating_user_groups": true, "allow_deleting_user_groups": true}'), (admin_group_id, 'assign_user_groups', '{"allow_assigning_any_group": true}'); + + INSERT INTO user_group_membership (user_id, group_id) VALUES (admin_user_id, admin_group_id); +END $$; diff --git a/y-server/src/api/admin/create_user.rs b/y-server/src/api/admin/create_user.rs new file mode 100644 index 0000000..609c009 --- /dev/null +++ b/y-server/src/api/admin/create_user.rs @@ -0,0 +1,77 @@ +use actix_web::{post, web, HttpResponse, Responder}; +use serde::{Deserialize, Serialize}; + +use crate::request::error; + +use crate::user::get_client_rights; +use crate::util::RequestPool; + +use pbkdf2::{ + password_hash::{rand_core::OsRng, PasswordHasher, SaltString}, + Pbkdf2, +}; + +#[derive(Deserialize)] +struct CreateUserInput { + username: String, + password: String, +} + +#[derive(Serialize)] +struct CreateUserOutput { + id: i32, +} + +#[post("/users")] +async fn create_user( + pool: web::Data, + form: web::Json, + req: actix_web::HttpRequest, +) -> impl Responder { + let client_rights = get_client_rights(&pool, &req).await; + + let action_allowed = client_rights + .iter() + .any(|right| right.right_name == "create_account"); + + if !action_allowed { + return error("create_user.unauthorized"); + } + + let username = form.username.as_str(); + + let existing_user: Result = + sqlx::query_scalar("SELECT id FROM users WHERE username = $1") + .bind(username) + .fetch_one(&**pool) + .await; + + if existing_user.is_ok() { + return error("create_user.username_taken"); + } + + let password_salt = SaltString::generate(&mut OsRng); + let password_hash = Pbkdf2.hash_password(form.password.as_bytes(), &password_salt); + + match password_hash { + Ok(password_hash) => { + let password_hash = password_hash.to_string(); + + let result: Result = sqlx::query_scalar( + "INSERT INTO users (username, password) VALUES ($1, $2) RETURNING id", + ) + .bind(username) + .bind(password_hash.as_str()) + .fetch_one(&**pool) + .await; + + match result { + Ok(result) => { + return HttpResponse::Ok().json(web::Json(CreateUserOutput { id: result })); + } + Err(_) => return error("create_user.other"), + } + } + Err(_) => return error("create_user.other"), + }; +} diff --git a/y-server/src/api/admin/create_user_group.rs b/y-server/src/api/admin/create_user_group.rs new file mode 100644 index 0000000..efac30a --- /dev/null +++ b/y-server/src/api/admin/create_user_group.rs @@ -0,0 +1,54 @@ +use crate::request::error; +use crate::user::get_client_rights; +use crate::util::RequestPool; +use actix_web::{post, web, HttpResponse, Responder}; +use serde::Deserialize; + +#[derive(serde::Serialize)] +struct CreateUserGroupOutput { + id: i32, +} + +#[derive(Deserialize)] +struct CreateUserGroupInput { + name: String, +} + +#[post("/user-groups")] +async fn create_user_group( + pool: web::Data, + form: web::Json, + req: actix_web::HttpRequest, +) -> impl Responder { + let client_rights = get_client_rights(&pool, &req).await; + + let action_allowed = client_rights + .iter() + .find(|right| { + right.right_name.eq("manage_user_groups") + && right + .right_options + .get("allow_creating_user_groups") + .and_then(|value| value.as_bool()) + .unwrap_or(false) + }) + .is_some(); + + if !action_allowed { + return error("create_user_group.unauthorized"); + } + + let name = form.name.clone(); + + let result = sqlx::query_scalar("INSERT INTO user_groups (name) VALUES ($1) RETURNING id") + .bind(name) + .fetch_one(&**pool) + .await; + + return match result { + Ok(new_group_id) => { + HttpResponse::Ok().json(web::Json(CreateUserGroupOutput { id: new_group_id })) + } + Err(_) => error("create_user_group.other"), + }; +} diff --git a/y-server/src/api/admin/delete_user.rs b/y-server/src/api/admin/delete_user.rs new file mode 100644 index 0000000..0d34013 --- /dev/null +++ b/y-server/src/api/admin/delete_user.rs @@ -0,0 +1,48 @@ +use crate::request::error; +use crate::user::get_client_rights; +use crate::util::RequestPool; +use actix_web::{delete, web, HttpResponse, Responder}; +use serde::Deserialize; + +#[derive(Deserialize)] +struct DeleteUserInput { + user_ids: Vec, +} + +#[delete("/users")] +async fn delete_user( + pool: web::Data, + form: web::Json, + req: actix_web::HttpRequest, +) -> impl Responder { + let client_rights = get_client_rights(&pool, &req).await; + + let action_allowed = client_rights + .iter() + .find(|right| right.right_name.eq("delete_user")) + .is_some(); + + if !action_allowed { + return error("delete_user.unauthorized"); + } + + let user_ids = form.into_inner().user_ids; + + // ! TODO refactor. I don't like this one bit + let user_ids_param = user_ids + .iter() + .map(|id| id.to_string()) + .collect::>() + .join(","); + + let delete_users_result = + sqlx::query(format!("DELETE FROM users WHERE id IN ({})", user_ids_param).as_str()) + .execute(&**pool) + .await; + + if delete_users_result.is_err() { + return error("delete_user.other"); + } else { + return HttpResponse::Ok().body("{}"); + } +} diff --git a/y-server/src/api/admin/delete_user_group.rs b/y-server/src/api/admin/delete_user_group.rs new file mode 100644 index 0000000..2b4b02c --- /dev/null +++ b/y-server/src/api/admin/delete_user_group.rs @@ -0,0 +1,44 @@ +use crate::request::error; +use crate::user::get_client_rights; +use crate::util::RequestPool; +use actix_web::{delete, web, HttpResponse, Responder}; + +#[delete("/user-groups/{user_group_id}")] +async fn delete_user_group( + pool: web::Data, + path: web::Path, + req: actix_web::HttpRequest, +) -> impl Responder { + let client_rights = get_client_rights(&pool, &req).await; + + let action_allowed = client_rights + .iter() + .find(|right| { + right.right_name.eq("manage_user_groups") + && right + .right_options + .get("allow_deleting_user_groups") + .and_then(|value| value.as_bool()) + .unwrap_or(false) + }) + .is_some(); + + if !action_allowed { + return error("delete_user_group.unauthorized"); + } + + let user_group_id = path.into_inner(); + + let delete_group_result = sqlx::query( + "DELETE FROM user_groups WHERE id = $1 AND (group_type NOT IN ('everyone', 'user') OR group_type IS NULL)", + ) + .bind(user_group_id) + .execute(&**pool) + .await; + + if delete_group_result.is_err() { + return error("delete_user_group.other"); + } else { + return HttpResponse::Ok().body("{}"); + } +} diff --git a/y-server/src/api/admin/mod.rs b/y-server/src/api/admin/mod.rs new file mode 100644 index 0000000..0898876 --- /dev/null +++ b/y-server/src/api/admin/mod.rs @@ -0,0 +1,11 @@ +pub mod create_user; +pub mod create_user_group; +pub mod delete_user; +pub mod delete_user_group; +pub mod update_password; +pub mod update_user_group; +pub mod update_user_group_membership; +pub mod user; +pub mod user_group; +pub mod user_groups; +pub mod users; diff --git a/y-server/src/api/admin/update_password.rs b/y-server/src/api/admin/update_password.rs new file mode 100644 index 0000000..7306abc --- /dev/null +++ b/y-server/src/api/admin/update_password.rs @@ -0,0 +1,91 @@ +use crate::user::get_client_rights; +use crate::util::RequestPool; +use crate::{request::error, user::get_user_groups}; +use actix_web::{put, web, HttpResponse, Responder}; +use serde::Deserialize; + +use pbkdf2::{ + password_hash::{rand_core::OsRng, PasswordHasher, SaltString}, + Pbkdf2, +}; + +#[derive(Deserialize)] +struct UpdatePasswordInput { + password: String, +} + +#[put("/users/{user_id}/password")] +async fn update_password( + pool: web::Data, + form: web::Json, + path: web::Path, + req: actix_web::HttpRequest, +) -> impl Responder { + let target_user_id = path.into_inner(); + + let client_rights = get_client_rights(&pool, &req).await; + let target_user_groups = get_user_groups(&pool, target_user_id).await; + + let mut change_user_password_right_present = false; + let mut client_right_groups_blacklist: Vec = vec![]; + + for right in client_rights { + if right.right_name.eq("change_user_password") { + change_user_password_right_present = true; + + if let Some(blacklist) = right.right_options.get("user_groups_blacklist") { + if blacklist.is_array() { + for group_id in blacklist.as_array().unwrap() { + if group_id.is_i64() { + client_right_groups_blacklist.push(group_id.as_i64().unwrap() as i32); + } + } + } + } + } + } + + let mut action_allowed = false; + + if change_user_password_right_present { + action_allowed = true; + + for group in target_user_groups { + if client_right_groups_blacklist.contains(&group.id) { + action_allowed = false; + break; + } + } + } + + if !action_allowed { + return error("update_password.unauthorized"); + } + + let password_salt = SaltString::generate(&mut OsRng); + let password_hash = Pbkdf2.hash_password(form.password.as_bytes(), &password_salt); + + match password_hash { + Ok(password_hash) => { + let password_hash = password_hash.to_string(); + + let result = sqlx::query("UPDATE users SET password = $1 WHERE id = $2") + .bind(password_hash.as_str()) + .bind(target_user_id) + .execute(&**pool) + .await; + + match result { + Ok(result) => { + if result.rows_affected() == 1 { + return HttpResponse::Ok().body("{}"); + } else { + return error("update_password.user_not_found"); + } + } + Err(_) => return error("update_password.other"), + } + } + Err(_) => return error("update_password.other"), + }; +} diff --git a/y-server/src/api/admin/update_user_group.rs b/y-server/src/api/admin/update_user_group.rs new file mode 100644 index 0000000..f82ab88 --- /dev/null +++ b/y-server/src/api/admin/update_user_group.rs @@ -0,0 +1,114 @@ +use std::collections::HashMap; + +use crate::request::error; +use crate::user::get_client_rights; +use crate::util::RequestPool; +use actix_web::{patch, web, HttpResponse, Responder}; +use serde::Deserialize; +use sqlx::QueryBuilder; + +#[derive(Deserialize)] +struct Right { + granted: bool, + options: HashMap, +} + +#[derive(Deserialize)] +struct UpdateUserGroupInput { + name: Option, + rights: Option>, +} + +#[patch("/user-groups/{user_group_id}")] +async fn update_user_group( + pool: web::Data, + form: web::Json, + path: web::Path, + req: actix_web::HttpRequest, +) -> impl Responder { + let client_rights = get_client_rights(&pool, &req).await; + + let action_allowed = client_rights + .iter() + .find(|right| right.right_name.eq("manage_user_groups")) + .is_some(); + + if !action_allowed { + return error("update_user_group.unauthorized"); + } + + let user_group_id = path.into_inner(); + + let form = form.into_inner(); + + let updated_name = &form.name.is_some(); + let updated_rights = &form.rights.is_some().clone(); + + if *updated_name { + let name = &form.name.unwrap(); + + let result = sqlx::query("UPDATE user_groups SET name = $1 WHERE id = $2") + .bind(name) + .bind(user_group_id) + .execute(&**pool) + .await; + + if result.is_err() { + return error("update_user_group.other"); + } + } + + if *updated_rights { + let transaction = pool.begin().await; + + if let Ok(mut transaction) = transaction { + let delete_rights_result = + sqlx::query("DELETE FROM user_group_rights WHERE group_id = $1") + .bind(user_group_id) + .execute(&mut *transaction) + .await; + + if delete_rights_result.is_err() { + return error("update_user_group.other"); + } + + let mut rights_query_builder = QueryBuilder::new( + "INSERT INTO user_group_rights(group_id, right_name, right_options) ", + ); + + let assigned_rigths = form + .rights + .unwrap() + .into_iter() + .filter(|right| right.1.granted) + .collect::>(); + + if assigned_rigths.len() > 0 { + rights_query_builder.push_values(assigned_rigths, |mut b, (right_name, right)| { + b.push_bind(user_group_id) + .push_bind(right_name) + .push_bind(serde_json::to_value(right.options).unwrap()); + }); + + let assign_rights_result = rights_query_builder + .build() + .execute(&mut *transaction) + .await; + + if assign_rights_result.is_err() { + return error("update_user_group.other"); + } + } + + let result = transaction.commit().await; + + if result.is_err() { + return error("update_user_group.other"); + } + } else { + return error("update_user_group.other"); + } + } + + return HttpResponse::Ok().body("{}"); +} diff --git a/y-server/src/api/admin/update_user_group_membership.rs b/y-server/src/api/admin/update_user_group_membership.rs new file mode 100644 index 0000000..bfcc5de --- /dev/null +++ b/y-server/src/api/admin/update_user_group_membership.rs @@ -0,0 +1,144 @@ +use serde::Deserialize; +use sqlx::QueryBuilder; + +use crate::{ + request::error, + user::{get_client_rights, get_user_groups}, +}; +use actix_web::{patch, web, HttpResponse, Responder}; + +use crate::util::RequestPool; + +#[derive(Deserialize)] +struct UpdateUserGroupMembershipInput { + user_groups: Vec, +} + +#[patch("/users/{user_id}/groups")] +async fn update_user_group_membership( + pool: web::Data, + form: web::Json, + path: web::Path, + req: actix_web::HttpRequest, +) -> impl Responder { + // ! TODO refactor the rights check + let target_user_id = path.into_inner(); + let target_groups = get_user_groups(&pool, target_user_id).await; + + let client_rights = get_client_rights(&pool, &req).await; + + let input_groups = form + .into_inner() + .user_groups + .into_iter() + .collect::>(); + + let mut mutated_groups: Vec = vec![]; + + // Added groups + for group_id in &input_groups { + if !target_groups + .iter() + .find(|group| group.id == *group_id) + .is_some() + { + mutated_groups.push(group_id.clone()); + } + } + + // Removed groups + for group in &target_groups { + if !input_groups + .iter() + .find(|group_id| **group_id == group.id) + .is_some() + { + dbg!("removed group {}!", group.id); + mutated_groups.push(group.id.clone()); + } + } + + let mut client_assignable_groups: Vec = vec![]; + let mut all_groups_allowed = false; + + for right in client_rights { + if right.right_name.eq("assign_user_groups") { + if let Some(all_allowed) = right.right_options.get("allow_assigning_any_group") { + if all_allowed.as_bool().unwrap_or(false) { + all_groups_allowed = true; + break; + } + } + + if let Some(groups) = right.right_options.get("assignable_user_groups") { + if groups.is_array() { + for group_id in groups.as_array().unwrap() { + if group_id.is_i64() { + client_assignable_groups.push(group_id.as_i64().unwrap() as i32); + } + } + } + } + } + } + + let mut action_allowed = true; + + dbg!(&mutated_groups); + + if !all_groups_allowed { + for group_id in &mutated_groups { + if !client_assignable_groups.contains(group_id) { + action_allowed = false; + break; + } + } + } + + if !action_allowed { + return error("update_user_group_membership.unauthorized"); + } + + let transaction = pool.begin().await; + + if let Ok(mut transaction) = transaction { + let unassign_groups_result = + sqlx::query("DELETE FROM user_group_membership WHERE user_id = $1") + .bind(target_user_id) + .execute(&mut *transaction) + .await; + + if unassign_groups_result.is_err() { + return error("update_user_group_membership.other"); + } + + let mut groups_query_builder = + QueryBuilder::new("INSERT INTO user_group_membership(user_id, group_id) "); + + if input_groups.len() > 0 { + groups_query_builder.push_values(input_groups, |mut b, group_id| { + b.push_bind(target_user_id).push_bind(group_id); + }); + + let assign_groups_result = groups_query_builder + .build() + .execute(&mut *transaction) + .await; + + if assign_groups_result.is_err() { + return error("update_user_group_membership.other"); + } + } + + let result = transaction.commit().await; + + match result { + Ok(_) => { + return HttpResponse::Ok().body("{}"); + } + Err(_) => return error("update_user_group_membership.other"), + } + } else { + return error("update_user_group_membership.other"); + } +} diff --git a/y-server/src/api/admin/user.rs b/y-server/src/api/admin/user.rs new file mode 100644 index 0000000..28fa600 --- /dev/null +++ b/y-server/src/api/admin/user.rs @@ -0,0 +1,64 @@ +use actix_web::{get, web, HttpResponse, Responder}; +use chrono::NaiveDateTime as Timestamp; +use serde::Serialize; + +use crate::request::error; + +use crate::util::RequestPool; + +#[derive(Serialize)] +struct UserOutput { + id: i32, + username: String, + created_at: String, + + user_groups: Vec, +} + +#[derive(sqlx::FromRow)] +struct UserRow { + group_id: Option, + username: String, + user_created_at: Timestamp, +} + +#[get("/users/{user_id}")] +async fn user(pool: web::Data, path: web::Path) -> impl Responder { + let user_id = path.into_inner(); + + let user_rows = sqlx::query_as::<_, UserRow>("SELECT user_groups.id as group_id, users.username, users.created_at as user_created_at FROM user_groups RIGHT OUTER JOIN user_group_membership ON user_groups.id = user_group_membership.group_id RIGHT OUTER JOIN users ON user_group_membership.user_id = users.id + WHERE users.id = $1") + .bind(user_id) + .fetch_all(&**pool).await; + + match user_rows { + Ok(user_rows) => { + if user_rows.len() == 0 { + return error("users.not_found"); + } + + let username = user_rows[0].username.clone(); + let created_at = user_rows[0].user_created_at.clone(); + + // TODO refactor this mess please + let group_ids = user_rows + .iter() + .map(|entry| entry.group_id) + .filter(|id| id.is_some()) + .map(|id| id.unwrap()) + .collect::>(); + + return HttpResponse::Ok().json(web::Json(UserOutput { + id: user_id, + username, + created_at: created_at.format("%Y-%m-%dT%H:%M:%SZ").to_string(), + + user_groups: group_ids, + })); + } + Err(err) => { + dbg!(err); + return error("users.internal"); + } + } +} diff --git a/y-server/src/api/admin/user_group.rs b/y-server/src/api/admin/user_group.rs new file mode 100644 index 0000000..88a258b --- /dev/null +++ b/y-server/src/api/admin/user_group.rs @@ -0,0 +1,34 @@ +use crate::user_group::get_user_group; +use crate::util::RequestPool; +use crate::{request::error, user_group::UserGroupRight}; +use actix_web::{get, web, HttpResponse, Responder}; +use serde::Serialize; + +#[derive(Serialize)] +struct UserGroupOutput { + pub id: i32, + pub name: String, + pub group_type: Option, + pub rights: Vec, +} + +#[get("/user-groups/{user_group_id}")] +async fn user_group(pool: web::Data, path: web::Path) -> impl Responder { + let user_group_id = path.into_inner(); + + let user_group = get_user_group(&pool, user_group_id).await; + + match user_group { + Ok(user_group) => { + return HttpResponse::Ok().json(web::Json(UserGroupOutput { + id: user_group.id, + name: user_group.name, + group_type: user_group.group_type, + rights: user_group.rights, + })); + } + Err(_) => { + return error("user_group.not_found"); + } + } +} diff --git a/y-server/src/api/admin/user_groups.rs b/y-server/src/api/admin/user_groups.rs new file mode 100644 index 0000000..d84c276 --- /dev/null +++ b/y-server/src/api/admin/user_groups.rs @@ -0,0 +1,69 @@ +use actix_web::{get, web, HttpResponse, Responder}; +use serde::Serialize; + +use crate::request::{error, TableInput, DEFAULT_LIMIT}; +use futures::join; + +use crate::user_group::UserGroup; +use crate::util::RequestPool; + +#[derive(Serialize)] +struct UserGroupOutput { + id: i32, + name: String, + group_type: Option, +} + +#[derive(Serialize)] +struct UserGroupsOutput { + user_groups: Vec, + total_count: i64, +} + +#[get("/user-groups")] +async fn user_groups( + pool: web::Data, + query: web::Query, +) -> impl Responder { + let search = query.search.clone().unwrap_or("".to_string()); + + let order_by = match query.order_by.as_ref() { + Some(order_by) => match order_by.as_str() { + "name" => "name", + _ => "name", + }, + None => "name", + }; + + let user_groups = sqlx::query_as::<_, UserGroup>( + "SELECT * FROM user_groups WHERE name LIKE '%' || $1 || '%' ORDER BY $2 LIMIT $3 OFFSET $4", + ) + .bind(search) + .bind(order_by) + .bind(query.limit.unwrap_or(DEFAULT_LIMIT)) + .bind(query.skip.unwrap_or(0)) + .fetch_all(&**pool); + + let user_groups_count = + sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM user_groups").fetch_one(&**pool); + + let (user_groups, user_groups_count) = join!(user_groups, user_groups_count); + + if let Ok(groups) = user_groups { + let groups_json = groups + .iter() + .map(|group| UserGroupOutput { + id: group.id, + name: group.name.clone(), + group_type: group.group_type.clone(), + }) + .collect::>(); + + HttpResponse::Ok().json(web::Json(UserGroupsOutput { + user_groups: groups_json, + total_count: user_groups_count.unwrap_or(0), + })) + } else { + error("user_groups.internal") + } +} diff --git a/y-server/src/api/admin/users.rs b/y-server/src/api/admin/users.rs new file mode 100644 index 0000000..1d004eb --- /dev/null +++ b/y-server/src/api/admin/users.rs @@ -0,0 +1,69 @@ +use actix_web::{get, web, HttpResponse, Responder}; +use serde::Serialize; + +use crate::request::{error, TableInput, DEFAULT_LIMIT}; +use futures::join; + +use crate::user::User; +use crate::util::RequestPool; + +#[derive(Serialize)] +struct UserOutput { + id: i32, + username: String, + created_at: String, +} + +#[derive(Serialize)] +struct UsersOutput { + users: Vec, + total_count: i64, +} + +#[get("/users")] +async fn users(pool: web::Data, query: web::Query) -> impl Responder { + let search = query.search.clone().unwrap_or("".to_string()); + + let order_by = match query.order_by.as_ref() { + Some(order_by) => match order_by.as_str() { + "username" => "username", + _ => "created_at", + }, + None => "created_at", + }; + + let users = sqlx::query_as::<_, User>( + "SELECT * FROM users WHERE username LIKE '%' || $1 || '%' ORDER BY $2 LIMIT $3 OFFSET $4", + ) + .bind(search) + .bind(order_by) + .bind(query.limit.unwrap_or(DEFAULT_LIMIT)) + .bind(query.skip.unwrap_or(0)) + .fetch_all(&**pool); + + let users_count = sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM users").fetch_one(&**pool); + + let (users, users_count) = join!(users, users_count); + + match users { + Ok(users) => { + let users_json = users + .iter() + .map(|user| UserOutput { + id: user.id, + username: user.username.clone(), + created_at: user.created_at.format("%Y-%m-%dT%H:%M:%SZ").to_string(), + }) + .collect::>(); + + return HttpResponse::Ok().json(web::Json(UsersOutput { + users: users_json, + total_count: users_count.unwrap_or(0), + })); + } + Err(err) => { + dbg!(err); + return error("users.internal"); + } + } +} diff --git a/y-server/src/api/auth/login.rs b/y-server/src/api/auth/login.rs new file mode 100644 index 0000000..e5626dc --- /dev/null +++ b/y-server/src/api/auth/login.rs @@ -0,0 +1,76 @@ +use actix_web::{cookie::Cookie, post, web, HttpResponse, Responder}; +use pbkdf2::{ + password_hash::{PasswordHash, PasswordVerifier}, + Pbkdf2, +}; +use serde::Deserialize; + +use crate::{ + request::error, + user::{create_user_session, User}, +}; + +use crate::util::RequestPool; + +#[derive(Deserialize)] +struct LoginInput { + username: String, + password: String, +} + +#[post("/login")] +async fn login(pool: web::Data, form: web::Json) -> impl Responder { + let provided_password = form.password.clone(); + + let user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE username = $1") + .bind(&form.username) + .fetch_one(&**pool) + .await; + + if let Ok(user) = user { + match &user.password { + Some(password) => { + let parsed_password_hash = PasswordHash::new(&password); + + if let Ok(parsed_password_hash) = parsed_password_hash { + let passwords_match = + Pbkdf2.verify_password(provided_password.as_bytes(), &parsed_password_hash); + + match passwords_match { + Ok(_) => { + let new_session = create_user_session(&pool, user.id).await; + + if let Ok(new_session) = new_session { + let session_cookie_value = format!( + "{}:{}", + new_session.session_id, new_session.session_key + ); + + let session_cookie = + Cookie::build("y-session", session_cookie_value) + .secure(false) + .http_only(true) + .path("/") + .finish(); + + return HttpResponse::Ok().cookie(session_cookie).body("{}"); + } + } + Err(_) => { + return error("auth.passwords_do_not_match"); + } + } + } else { + return error("auth.other"); + } + } + None => { + return error("auth.authentication_forbidden"); + } + } + } else { + return error("auth.user_does_not_exist"); + } + + error("auth.other") +} diff --git a/y-server/src/api/auth/logout.rs b/y-server/src/api/auth/logout.rs new file mode 100644 index 0000000..215105e --- /dev/null +++ b/y-server/src/api/auth/logout.rs @@ -0,0 +1,32 @@ +use actix_web::{cookie::Cookie, post, web, HttpRequest, HttpResponse, Responder}; + +use crate::{ + request::error, + user::{destroy_user_session, get_user_from_request}, + util::RequestPool, +}; + +#[post("/logout")] +async fn logout(pool: web::Data, req: HttpRequest) -> impl Responder { + let session_info = get_user_from_request(&pool, &req).await; + + if let Some((_, session)) = session_info { + let result = destroy_user_session(&pool, session.session_id).await; + + if result { + return HttpResponse::Ok() + .cookie( + Cookie::build("y-session", "") + .secure(false) + .http_only(true) + .path("/") + .finish(), + ) + .body("{}"); + } else { + return error("auth.invalid_session"); + } + } else { + return error("auth.invalid_session"); + } +} diff --git a/y-server/src/api/auth/me.rs b/y-server/src/api/auth/me.rs new file mode 100644 index 0000000..dbe87c6 --- /dev/null +++ b/y-server/src/api/auth/me.rs @@ -0,0 +1,35 @@ +use actix_web::{get, web, HttpRequest, HttpResponse, Responder}; +use serde::Serialize; + +use crate::{ + request::{Error, Response}, + user::{get_client_rights, get_user_from_request, UserRight}, + util::RequestPool, +}; + +#[derive(Serialize)] +struct MeOutput { + id: i32, + username: String, + user_rights: Vec, +} + +#[get("/me")] +async fn me(pool: web::Data, req: HttpRequest) -> impl Responder { + let session_info = get_user_from_request(&pool, &req).await; + let user_rights = get_client_rights(&pool, &req).await; + + return if let Some((user, _)) = session_info { + HttpResponse::Ok().json(web::Json(MeOutput { + id: user.id, + username: user.username, + user_rights, + })) + } else { + HttpResponse::Unauthorized().json(Response { + error: Error { + code: "auth.unauthorized".to_string(), + }, + }) + }; +} diff --git a/y-server/src/api/auth/mod.rs b/y-server/src/api/auth/mod.rs new file mode 100644 index 0000000..8e4f49c --- /dev/null +++ b/y-server/src/api/auth/mod.rs @@ -0,0 +1,3 @@ +pub mod login; +pub mod logout; +pub mod me; diff --git a/y-server/src/api/mod.rs b/y-server/src/api/mod.rs new file mode 100644 index 0000000..6abeee4 --- /dev/null +++ b/y-server/src/api/mod.rs @@ -0,0 +1,3 @@ +pub mod admin; +pub mod auth; +pub mod user_rights; diff --git a/y-server/src/api/user_rights.rs b/y-server/src/api/user_rights.rs new file mode 100644 index 0000000..19909b4 --- /dev/null +++ b/y-server/src/api/user_rights.rs @@ -0,0 +1,16 @@ +use actix_web::{get, web, HttpResponse, Responder}; +use serde::Serialize; + +use crate::right::{get_right_categories, RightCategory}; + +#[derive(Serialize)] +struct RightsOutput { + categories: Vec, +} + +#[get("/user-rights")] +async fn user_rights() -> impl Responder { + let categories = get_right_categories(); + + HttpResponse::Ok().json(web::Json(RightsOutput { categories })) +} diff --git a/y-server/src/db.rs b/y-server/src/db.rs new file mode 100644 index 0000000..97dda41 --- /dev/null +++ b/y-server/src/db.rs @@ -0,0 +1,14 @@ +use sqlx::{postgres::PgPoolOptions, Postgres}; +use std::env; + +pub async fn connect_to_database() -> sqlx::Pool { + let database_url = env::var("DATABASE_URL").expect("DATABASE_URL env variable must be set"); + + let pool = PgPoolOptions::new() + .max_connections(5) + .connect(database_url.as_str()) + .await + .expect("Could not connect to the database"); + + return pool; +} diff --git a/y-server/src/main.rs b/y-server/src/main.rs new file mode 100644 index 0000000..2816721 --- /dev/null +++ b/y-server/src/main.rs @@ -0,0 +1,116 @@ +mod api; +mod db; +mod request; +mod right; +mod user; +mod user_group; +mod util; + +use actix_web::{web, App, HttpServer}; +use dotenvy::dotenv; +use std::env; +use std::process::exit; +use util::RequestPool; + +async fn process_cli_arguments(pool: &RequestPool) { + let cli_arguments: Vec = env::args().collect(); + + for (index, argument) in cli_arguments.iter().enumerate() { + if argument == "--create-user" { + let username = &cli_arguments.get(index + 1); + let password = &cli_arguments.get(index + 2); + let group_name = &cli_arguments.get(index + 3); + + if let (Some(username), Some(password)) = (username, password) { + println!("Creating a new user and exiting..."); + + let create_user = util::cli_create_user( + pool, + username, + password, + group_name.and_then(|name| Some(name.as_str())), + ) + .await; + + match create_user { + Ok(_) => { + println!("No errors reported."); + exit(0); + } + Err(error) => { + println!("Error reported: {}", error); + exit(1); + } + } + } else { + println!("Usage: --create-user [group]"); + println!(" group: optional, name of the group that will be assigned to the user."); + exit(1); + } + } + } +} + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + dotenv().ok(); + + let pool = db::connect_to_database().await; + + process_cli_arguments(&pool).await; + + let server_address = + env::var("SERVER_ADDRESS").expect("SERVER_ADDRESS env variable must be set"); + + let server_port: u16 = env::var("SERVER_PORT") + .expect("SERVER_PORT env variable must be set") + .parse() + .expect("SERVER_PORT is not a valid port number"); + + let migration_result = sqlx::migrate!().run(&pool).await; + + match migration_result { + Ok(_) => { + println!("Migrations ran successfully.") + } + Err(error) => { + println!("Error running migrations: {}", error); + exit(1); + } + } + + println!( + "Starting the server on {}:{}...", + server_address, server_port + ); + + HttpServer::new(move || { + App::new() + .app_data(web::Data::new(pool.clone())) + .service( + web::scope("/api/auth") + .service(crate::api::auth::login::login) + .service(crate::api::auth::me::me) + .service(crate::api::auth::logout::logout), + ) + .service( + web::scope("/api/admin") + .service(crate::api::admin::user::user) + .service(crate::api::admin::create_user::create_user) + .service(crate::api::admin::delete_user::delete_user) + .service(crate::api::admin::users::users) + .service(crate::api::admin::update_password::update_password) + .service(crate::api::admin::user_groups::user_groups) + .service(crate::api::admin::user_group::user_group) + .service(crate::api::admin::update_user_group::update_user_group) + .service(crate::api::admin::create_user_group::create_user_group) + .service(crate::api::admin::delete_user_group::delete_user_group) + .service(crate::api::admin::update_user_group_membership::update_user_group_membership) + , + ) + .service(web::scope("/api").service(crate::api::user_rights::user_rights)) + }) + .bind((server_address, server_port))? + .run() + .await +} diff --git a/y-server/src/request.rs b/y-server/src/request.rs new file mode 100644 index 0000000..53a949b --- /dev/null +++ b/y-server/src/request.rs @@ -0,0 +1,33 @@ +use actix_web::{web, HttpResponse}; +use serde::{Deserialize, Serialize}; + +pub const DEFAULT_LIMIT: i64 = 25; + +#[derive(Deserialize)] +pub struct TableInput { + pub order_by: Option, + pub direction: Option, + + pub limit: Option, + pub skip: Option, + + pub search: Option, +} + +#[derive(Serialize)] +pub struct Error { + pub code: String, +} + +#[derive(Serialize)] +pub struct Response { + pub error: Error, +} + +pub fn error(code: &str) -> HttpResponse { + HttpResponse::BadRequest().json(web::Json(Response { + error: Error { + code: code.to_string(), + }, + })) +} diff --git a/y-server/src/right.rs b/y-server/src/right.rs new file mode 100644 index 0000000..daf86f2 --- /dev/null +++ b/y-server/src/right.rs @@ -0,0 +1,115 @@ +use serde::Serialize; + +#[derive(Serialize)] +pub enum RightTag { + #[serde(rename = "dangerous")] + Dangerous, + #[serde(rename = "administrative")] + Administrative, +} + +#[derive(Serialize)] +pub enum RightValueType { + #[serde(rename = "boolean")] + Boolean, + #[serde(rename = "number")] + Number, + #[serde(rename = "string")] + String, + #[serde(rename = "string_array")] + StringArray, +} + +#[derive(Serialize)] +pub struct RightOption { + name: &'static str, + value_type: RightValueType, + value_source: Option<&'static str>, +} + +#[derive(Serialize)] +pub struct Right { + name: &'static str, + options: Vec, + tags: Vec, +} + +#[derive(Serialize)] +pub struct RightCategory { + name: &'static str, + rights: Vec, +} + +pub fn get_right_categories() -> Vec { + vec![ + RightCategory { + name: "basic", + rights: vec![Right { + name: "create_account", + options: vec![], + tags: vec![], + }], + }, + RightCategory { + name: "user_administration", + rights: vec![ + Right { + name: "change_user_password", + options: vec![RightOption { + name: "user_groups_blacklist", + value_type: RightValueType::StringArray, + value_source: Some("user_groups"), + }], + tags: vec![RightTag::Administrative], + }, + Right { + name: "delete_user", + options: vec![], + tags: vec![RightTag::Administrative], + }, + ], + }, + RightCategory { + name: "user_rights", + rights: vec![ + Right { + name: "manage_user_groups", + options: vec![ + RightOption { + name: "allow_creating_user_groups", + value_type: RightValueType::Boolean, + value_source: None, + }, + RightOption { + name: "allow_deleting_user_groups", + value_type: RightValueType::Boolean, + value_source: None, + }, + RightOption { + name: "mutable_user_rights", + value_type: RightValueType::StringArray, + value_source: Some("user_rights"), + }, + ], + tags: vec![RightTag::Administrative], + }, + Right { + name: "assign_user_groups", + options: vec![ + RightOption { + name: "allow_assigning_any_group", + value_type: RightValueType::Boolean, + value_source: None, + }, + RightOption { + name: "assignable_user_groups", + value_type: RightValueType::StringArray, + value_source: Some("user_groups"), + }, + ], + tags: vec![RightTag::Administrative], + }, + ], + }, + ] +} diff --git a/y-server/src/user.rs b/y-server/src/user.rs new file mode 100644 index 0000000..6b8bc54 --- /dev/null +++ b/y-server/src/user.rs @@ -0,0 +1,208 @@ +use actix_web::HttpRequest; +use chrono::NaiveDateTime as Timestamp; +use chrono::Utc; +use serde_json::Value; +use uuid::Uuid; + +use crate::user_group::UserGroup; +use crate::util::RequestPool; + +#[derive(sqlx::FromRow)] +pub struct User { + pub id: i32, + + pub username: String, + pub password: Option, + + pub created_at: Timestamp, +} + +#[derive(sqlx::FromRow)] +pub struct UserSession { + pub id: i32, + + pub session_id: Uuid, + pub session_key: String, + + pub user_id: i32, + + pub created_at: Timestamp, + pub expires_on: Option, +} + +fn generate_session_secrets() -> (Uuid, String) { + use rand::distributions::Alphanumeric; + use rand::{thread_rng, Rng}; + + let session_key: String = thread_rng() + .sample_iter(&Alphanumeric) + .take(256) + .map(char::from) + .collect(); + + let session_id = Uuid::new_v4(); + + (session_id, session_key) +} + +pub async fn create_user_session( + pool: &RequestPool, + user_id: i32, +) -> Result { + use chrono::prelude::*; + + let (session_id, session_key) = generate_session_secrets(); + + let created_at = Utc::now(); + let expires_on = created_at + chrono::Duration::days(30); + + sqlx::query_as::<_, UserSession>( + "INSERT INTO user_sessions (session_id, session_key, user_id, created_at, expires_on) VALUES ($1, $2, $3, $4, $5) RETURNING *", + ) + .bind(session_id) + .bind(session_key) + .bind(user_id) + .bind(created_at.naive_utc()) + .bind(expires_on.naive_utc()) + .fetch_one(pool) + .await +} + +#[derive(sqlx::FromRow)] +struct UserWithSession { + pub id: i32, + + pub user_id: i32, + pub username: String, + pub user_created_at: Timestamp, + + pub session_id: Uuid, + pub session_key: String, + pub session_created_at: Timestamp, + pub session_expires_on: Option, +} + +pub async fn get_user_from_request( + pool: &RequestPool, + req: &HttpRequest, +) -> Option<(User, UserSession)> { + let session_cookie = req.cookie("y-session"); + + if let Some(session_cookie) = session_cookie { + let cookie_value = session_cookie.value(); + let cookie_parts: Vec<&str> = cookie_value.split(':').collect(); + + if cookie_parts.len() == 2 { + let (session_id, session_key) = (Uuid::parse_str(cookie_parts[0]), cookie_parts[1]); + + if let Ok(session_id) = session_id { + let user_with_session = sqlx::query_as::<_, UserWithSession>( + "SELECT user_sessions.id, user_sessions.session_id, user_sessions.session_key, user_sessions.user_id, user_sessions.created_at as session_created_at, user_sessions.expires_on as session_expires_on, users.username, users.created_at as user_created_at FROM user_sessions INNER JOIN users ON user_sessions.user_id = users.id WHERE user_sessions.session_id = $1 AND user_sessions.session_key = $2" + ) + .bind(session_id) + .bind(session_key) + .fetch_one(pool) + .await; + + if let Ok(user_with_session) = user_with_session { + if let Some(expires_on) = user_with_session.session_expires_on { + if Utc::now().naive_utc() > expires_on { + return None; + } + } + + return Some(( + User { + created_at: user_with_session.user_created_at, + id: user_with_session.user_id, + password: None, + username: user_with_session.username, + }, + UserSession { + id: user_with_session.id, + created_at: user_with_session.session_created_at, + expires_on: user_with_session.session_expires_on, + session_id: user_with_session.session_id, + session_key: user_with_session.session_key, + user_id: user_with_session.user_id, + }, + )); + } + } + } + } + + None +} + +pub async fn destroy_user_session(pool: &RequestPool, session_id: Uuid) -> bool { + let result = sqlx::query("DELETE FROM user_sessions WHERE session_id = $1") + .bind(session_id) + .execute(pool) + .await; + + result.is_ok() && result.unwrap().rows_affected() == 1 +} + +#[derive(sqlx::FromRow, serde::Serialize)] +pub struct UserRight { + pub right_name: String, + pub right_options: Value, +} + +pub async fn get_client_rights(pool: &RequestPool, req: &HttpRequest) -> Vec { + let client_session = get_user_from_request(&pool, &req).await; + + // ! TODO refactor the query + let right_rows = if let Some((user, _)) = client_session { + sqlx::query_as::<_, UserRight>("SELECT DISTINCT ON (user_group_rights.right_name, user_group_rights.right_options) user_group_rights.right_name, user_group_rights.right_options FROM user_groups + RIGHT JOIN user_group_rights ON user_group_rights.group_id = user_groups.id + WHERE user_groups.group_type IN ('user', 'everyone') + UNION ALL + SELECT DISTINCT ON (user_group_rights.right_name, user_group_rights.right_options) user_group_rights.right_name, user_group_rights.right_options FROM user_groups + RIGHT JOIN user_group_membership ON user_groups.id = user_group_membership.group_id + RIGHT JOIN users ON user_group_membership.user_id = users.id + RIGHT JOIN user_group_rights ON user_group_rights.group_id = user_group_membership.group_id + WHERE users.id = $1") + .bind(user.id) + .fetch_all(pool) + .await + } else { + sqlx::query_as::<_, UserRight>("SELECT DISTINCT ON (user_group_rights.right_name, user_group_rights.right_options) user_group_rights.right_name, user_group_rights.right_options FROM user_groups + RIGHT JOIN user_group_rights ON user_group_rights.group_id = user_groups.id + WHERE user_groups.group_type = 'everyone'") + .fetch_all(pool) + .await + }; + + match right_rows { + Ok(right_rows) => { + return right_rows; + } + Err(err) => { + dbg!(err); + return vec![]; + } + } +} + +pub async fn get_user_groups(pool: &RequestPool, user_id: i32) -> Vec { + let groups = sqlx::query_as::<_, UserGroup>( + "SELECT user_groups.id, user_groups.name, user_groups.group_type FROM user_groups + RIGHT JOIN user_group_membership ON user_group_membership.group_id = user_groups.id + WHERE user_group_membership.user_id = $1", + ) + .bind(user_id) + .fetch_all(pool) + .await; + + match groups { + Ok(groups) => { + return groups; + } + Err(err) => { + dbg!(err); + return vec![]; + } + } +} diff --git a/y-server/src/user_group.rs b/y-server/src/user_group.rs new file mode 100644 index 0000000..b2175d3 --- /dev/null +++ b/y-server/src/user_group.rs @@ -0,0 +1,77 @@ +use serde::Serialize; +use sqlx::FromRow; + +use crate::util::RequestPool; + +#[derive(FromRow, Serialize)] +pub struct UserGroup { + pub id: i32, + pub name: String, + pub group_type: Option, +} + +#[derive(Serialize)] +pub struct UserGroupRight { + pub group_id: i32, + pub right_name: String, + pub right_options: serde_json::Value, +} + +pub struct UserGroupWithRights { + pub id: i32, + pub name: String, + pub group_type: Option, + pub rights: Vec, +} + +#[derive(FromRow)] +struct UserGroupAndRights { + pub id: i32, + pub name: String, + pub group_type: Option, + pub right_name: Option, + pub right_options: Option, +} + +pub async fn get_user_group( + pool: &RequestPool, + user_group_id: i32, +) -> Result { + let group_and_rights = sqlx::query_as::<_, UserGroupAndRights>( + "SELECT user_groups.id, user_groups.name, user_groups.group_type, user_group_rights.right_name, user_group_rights.right_options FROM user_groups LEFT JOIN user_group_rights ON user_group_rights.group_id = user_groups.id WHERE user_groups.id = $1" + ) + .bind(user_group_id) + .fetch_all(pool).await; + + match group_and_rights { + Ok(group_and_rights) => { + if group_and_rights.len() == 0 { + return Err(sqlx::Error::RowNotFound); + } + + let group_id = group_and_rights[0].id.clone(); + let group_name = group_and_rights[0].name.clone(); + let group_type = group_and_rights[0].group_type.clone(); + + let mut rights: Vec = vec![]; + + for group_and_right in group_and_rights { + if group_and_right.right_name.is_some() { + rights.push(UserGroupRight { + group_id: group_and_right.id, + right_name: group_and_right.right_name.unwrap(), + right_options: group_and_right.right_options.unwrap(), + }); + } + } + + return Ok(UserGroupWithRights { + id: group_id, + name: group_name, + group_type, + rights, + }); + } + Err(err) => return Err(err), + }; +} diff --git a/y-server/src/util.rs b/y-server/src/util.rs new file mode 100644 index 0000000..3057e1b --- /dev/null +++ b/y-server/src/util.rs @@ -0,0 +1,77 @@ +pub type RequestPool = sqlx::Pool; + +pub async fn cli_create_user( + pool: &RequestPool, + username: &str, + password: &str, + group: Option<&str>, +) -> Result<(), &'static str> { + use pbkdf2::{ + password_hash::{rand_core::OsRng, PasswordHasher, SaltString}, + Pbkdf2, + }; + + let password_salt = SaltString::generate(&mut OsRng); + let password_hash = Pbkdf2.hash_password(password.as_bytes(), &password_salt); + + match password_hash { + Ok(password_hash) => { + let password_hash = password_hash.to_string(); + + let user_id = sqlx::query_scalar::<_, i32>( + "INSERT INTO users (username, password) VALUES ($1, $2) RETURNING id", + ) + .bind(username) + .bind(password_hash.as_str()) + .fetch_one(pool) + .await; + + match user_id { + Ok(user_id) => { + println!("User created successfully."); + + if let Some(group) = group { + println!("Adding to the group..."); + + let group_id = sqlx::query_scalar::<_, i32>( + "SELECT id FROM user_groups WHERE name = $1", + ) + .bind(group) + .fetch_one(pool) + .await; + + if !group_id.is_ok() { + return Err("Could not find the group"); + } + + let group_result = sqlx::query( + "INSERT INTO user_group_membership (user_id, group_id) VALUES ($1, $2)", + ) + .bind(user_id) + .bind(group_id.unwrap()) + .execute(pool) + .await; + + match group_result { + Ok(_) => { + println!("User added to the group successfully."); + return Ok(()); + } + Err(error) => { + dbg!(error); + return Err("Could not add the user to the group"); + } + } + } else { + Ok(()) + } + } + Err(error) => { + dbg!(error); + Err("Error creating a new user") + } + } + } + Err(_) => Err("Error hashing the password for a new user"), + } +} diff --git a/y-web/.eslintrc.cjs b/y-web/.eslintrc.cjs index 04580ca..6100120 100644 --- a/y-web/.eslintrc.cjs +++ b/y-web/.eslintrc.cjs @@ -1,172 +1,175 @@ -module.exports = { - extends: [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:@typescript-eslint/recommended-requiring-type-checking", - "plugin:unicorn/recommended", - "plugin:sonarjs/recommended", - "plugin:solid/typescript", - ], - parser: "@typescript-eslint/parser", - parserOptions: { - tsconfigRootDir: __dirname, - project: ["./tsconfig.json"], - }, - plugins: ["@typescript-eslint", "unicorn", "sonarjs", "solid"], - root: true, - rules: { - // ESLint - errors - "no-constant-binary-expression": "error", - "array-callback-return": "error", - "no-await-in-loop": "error", - "no-duplicate-imports": "error", - "no-promise-executor-return": "error", - "no-self-compare": "error", - "no-template-curly-in-string": "error", - "require-atomic-updates": "error", - eqeqeq: "error", - "func-style": ["error", "declaration", { allowArrowFunctions: true }], - "no-confusing-arrow": "error", - "no-eval": "error", - "no-floating-decimal": "error", - "no-implicit-coercion": "error", - "no-implied-eval": "error", - "no-mixed-operators": "error", - "no-multi-assign": "error", - "no-param-reassign": "error", - "no-return-assign": "error", - "no-sequences": "error", - "no-useless-computed-key": "error", - "no-useless-concat": "error", - "no-var": "error", - "prefer-promise-reject-errors": "error", - radix: "error", - "eol-last": "error", - semi: ["error", "never"], - "prefer-const": "error", - "semi-spacing": ["error", { before: false, after: true }], - "arrow-spacing": ["error", { before: true, after: true }], - "jsx-quotes": ["error", "prefer-double"], - "linebreak-style": ["error", "unix"], - "no-tabs": "error", - "space-before-blocks": ["error", "always"], - "space-infix-ops": "error", - "space-unary-ops": ["error", { words: true, nonwords: false }], - "wrap-iife": ["error", "inside"], - "no-trailing-spaces": "error", - "no-unneeded-ternary": "error", - "no-useless-return": "error", - "no-else-return": "error", - "no-unused-expressions": "error", - "prefer-arrow-callback": "error", - "prefer-rest-params": "error", - "prefer-spread": "error", - "no-undefined": "error", - "no-empty": "error", - "no-lone-blocks": "error", - - // ESLint - warns - "no-unmodified-loop-condition": "warn", - "no-unreachable-loop": "warn", - "no-negated-condition": "warn", - "prefer-template": "warn", - - // Typescript ESLint - errors - "no-unused-vars": "off", - "@typescript-eslint/no-unused-vars": "error", - "default-param-last": "off", - "@typescript-eslint/default-param-last": "error", - "no-empty-function": "off", - "@typescript-eslint/no-empty-function": "error", - "no-invalid-this": "off", - "@typescript-eslint/no-invalid-this": "error", - "no-loop-func": "off", - "@typescript-eslint/no-loop-func": "error", - "no-shadow": "off", - "@typescript-eslint/no-shadow": "error", - "no-use-before-define": "off", - "@typescript-eslint/no-use-before-define": ["error", { functions: false }], - "no-return-await": "off", - "@typescript-eslint/return-await": "error", - "@typescript-eslint/no-confusing-void-expression": [ - "error", - { - ignoreArrowShorthand: true, - ignoreVoidOperator: true, - }, - ], - "@typescript-eslint/no-non-null-asserted-nullish-coalescing": "error", - "@typescript-eslint/no-confusing-non-null-assertion": "error", - "require-await": "off", - "@typescript-eslint/require-await": "error", - "func-call-spacing": "off", - "@typescript-eslint/func-call-spacing": "error", - quotes: "off", - "@typescript-eslint/quotes": ["error", "double"], - "@typescript-eslint/array-type": [ - "error", - { default: "array-simple", readonly: "array-simple" }, - ], - "@typescript-eslint/consistent-type-assertions": [ - "error", - { assertionStyle: "as" }, - ], - "@typescript-eslint/consistent-type-definitions": ["error", "type"], - "@typescript-eslint/no-unnecessary-boolean-literal-compare": "error", - "@typescript-eslint/method-signature-style": ["error", "property"], - "@typescript-eslint/prefer-function-type": "error", - "@typescript-eslint/no-unnecessary-condition": "error", - "@typescript-eslint/naming-convention": [ - "error", - { - selector: "default", - format: ["camelCase"], - leadingUnderscore: "allow", - trailingUnderscore: "forbid", - }, - { - selector: "variable", - format: ["camelCase", "PascalCase", "UPPER_CASE"], - }, - { - selector: "typeLike", - format: ["PascalCase"], - }, - { - selector: "objectLiteralProperty", - format: null, - }, - ], - - // Typescript ESLint - warns - "no-magic-numbers": "off", - "@typescript-eslint/no-magic-numbers": ["warn", { ignore: [1, 0, -1] }], - "@typescript-eslint/promise-function-async": "warn", - - // eslint-plugin-unicorn - "unicorn/catch-error-name": "off", - "unicorn/prefer-query-selector": "off", - "unicorn/expiring-todo-comments": "off", - "unicorn/no-null": "off", - "unicorn/no-object-as-default-parameter": "off", - "unicorn/no-unused-properties": "warn", - "unicorn/filename-case": [ - "error", - { - case: "kebabCase", - }, - ], - "unicorn/prevent-abbreviations": [ - "error", - { ignore: ["^ref$", "Ref.*", "Props", "props", "args"] }, - ], - - // eslint-plugin-sonarjs - "sonarjs/elseif-without-else": "warn", - "sonarjs/cognitive-complexity": "warn", - "sonarjs/no-nested-template-literals": "warn", - - // eslint-plugin-solid - "solid/reactivity": "error", - }, -} +module.exports = { + extends: [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/recommended-requiring-type-checking", + "plugin:unicorn/recommended", + "plugin:sonarjs/recommended", + "plugin:solid/typescript", + ], + parser: "@typescript-eslint/parser", + parserOptions: { + tsconfigRootDir: __dirname, + project: ["./tsconfig.json"], + }, + plugins: ["@typescript-eslint", "unicorn", "sonarjs", "solid"], + root: true, + rules: { + // ESLint - errors + "no-constant-binary-expression": "error", + "array-callback-return": "error", + "no-await-in-loop": "error", + "no-duplicate-imports": "error", + "no-promise-executor-return": "error", + "no-self-compare": "error", + "no-template-curly-in-string": "error", + "require-atomic-updates": "error", + eqeqeq: "error", + "func-style": ["error", "declaration", { allowArrowFunctions: true }], + "no-confusing-arrow": "error", + "no-eval": "error", + "no-floating-decimal": "error", + "no-implicit-coercion": "error", + "no-implied-eval": "error", + "no-mixed-operators": "error", + "no-multi-assign": "error", + "no-param-reassign": "error", + "no-return-assign": "error", + "no-sequences": "error", + "no-useless-computed-key": "error", + "no-useless-concat": "error", + "no-var": "error", + "prefer-promise-reject-errors": "error", + radix: "error", + "eol-last": "error", + semi: ["error", "never"], + "prefer-const": "error", + "semi-spacing": ["error", { before: false, after: true }], + "arrow-spacing": ["error", { before: true, after: true }], + "jsx-quotes": ["error", "prefer-double"], + "linebreak-style": ["error", "unix"], + "no-tabs": "error", + "space-before-blocks": ["error", "always"], + "space-infix-ops": "error", + "space-unary-ops": ["error", { words: true, nonwords: false }], + "wrap-iife": ["error", "inside"], + "no-trailing-spaces": "error", + "no-unneeded-ternary": "error", + "no-useless-return": "error", + "no-else-return": "error", + "no-unused-expressions": "error", + "prefer-arrow-callback": "error", + "prefer-rest-params": "error", + "prefer-spread": "error", + "no-undefined": "error", + "no-empty": "error", + "no-lone-blocks": "error", + "no-warning-comments": ["error"], + + // ESLint - warns + "no-unmodified-loop-condition": "warn", + "no-unreachable-loop": "warn", + "no-negated-condition": "warn", + "prefer-template": "warn", + + // Typescript ESLint - errors + "@typescript-eslint/no-non-null-assertion": "off", + + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": "error", + "default-param-last": "off", + "@typescript-eslint/default-param-last": "error", + "no-empty-function": "off", + "@typescript-eslint/no-empty-function": "error", + "no-invalid-this": "off", + "@typescript-eslint/no-invalid-this": "error", + "no-loop-func": "off", + "@typescript-eslint/no-loop-func": "error", + "no-shadow": "off", + "@typescript-eslint/no-shadow": "error", + "no-use-before-define": "off", + "@typescript-eslint/no-use-before-define": ["error", { functions: false }], + "no-return-await": "off", + "@typescript-eslint/return-await": "error", + "@typescript-eslint/no-confusing-void-expression": [ + "error", + { + ignoreArrowShorthand: true, + ignoreVoidOperator: true, + }, + ], + "@typescript-eslint/no-non-null-asserted-nullish-coalescing": "error", + "@typescript-eslint/no-confusing-non-null-assertion": "error", + "require-await": "off", + "@typescript-eslint/require-await": "error", + "func-call-spacing": "off", + "@typescript-eslint/func-call-spacing": "error", + quotes: "off", + "@typescript-eslint/quotes": ["error", "double"], + "@typescript-eslint/array-type": [ + "error", + { default: "array-simple", readonly: "array-simple" }, + ], + "@typescript-eslint/consistent-type-assertions": [ + "error", + { assertionStyle: "as" }, + ], + "@typescript-eslint/consistent-type-definitions": ["error", "type"], + "@typescript-eslint/no-unnecessary-boolean-literal-compare": "error", + "@typescript-eslint/method-signature-style": ["error", "property"], + "@typescript-eslint/prefer-function-type": "error", + "@typescript-eslint/no-unnecessary-condition": "error", + "@typescript-eslint/naming-convention": [ + "error", + { + selector: "default", + format: ["camelCase"], + leadingUnderscore: "allow", + trailingUnderscore: "forbid", + }, + { + selector: "variable", + format: ["camelCase", "PascalCase", "UPPER_CASE"], + }, + { + selector: "typeLike", + format: ["PascalCase"], + }, + { + selector: "objectLiteralProperty", + format: null, + }, + ], + + // Typescript ESLint - warns + "no-magic-numbers": "off", + "@typescript-eslint/no-magic-numbers": ["warn", { ignore: [1, 0, -1] }], + "@typescript-eslint/promise-function-async": "warn", + + // eslint-plugin-unicorn + "unicorn/catch-error-name": "off", + "unicorn/prefer-query-selector": "off", + "unicorn/expiring-todo-comments": "off", + "unicorn/no-null": "off", + "unicorn/no-object-as-default-parameter": "off", + "unicorn/no-unused-properties": "warn", + "unicorn/filename-case": [ + "warn", + { + case: "kebabCase", + }, + ], + "unicorn/prevent-abbreviations": [ + "error", + { ignore: ["^ref$", "Ref.*", "Props", "props", "args", "params"] }, + ], + + // eslint-plugin-sonarjs + "sonarjs/elseif-without-else": "warn", + "sonarjs/cognitive-complexity": "warn", + "sonarjs/no-nested-template-literals": "warn", + + // eslint-plugin-solid + "solid/reactivity": "error", + }, +} diff --git a/y-web/i18n/en.json b/y-web/i18n/en.json new file mode 100644 index 0000000..94bfeec --- /dev/null +++ b/y-web/i18n/en.json @@ -0,0 +1,99 @@ +{ + "main": { + "userRightCategory": { + "basic": { + "name": "Basic", + "description": "Common rights most users should probably have." + }, + "user_administration": { + "name": "User administration", + "description": "Rights for administrating other user's accounts." + }, + "user_rights": { + "name": "Rights and groups", + "description": "User groups administration." + } + }, + "userRight": { + "create_account": { + "name": "Create user accounts", + "description": "Create new users. Already existing users will be able to create other accounts as well." + }, + "delete_user": { + "name": "Delete users", + "description": "Unlike blocking or hiding, deleting a user is an irreversible action. All data related to a deleted user is forever lost." + }, + "change_user_password": { + "name": "Change user's password", + "description": "Update user's password. Target user will not necessarily be notified of the change." + }, + "manage_user_groups": { + "name": "Manage user groups", + "description": "Allows control over user groups & it's rights." + }, + "assign_user_groups": { + "name": "Update user's groups", + "description": "Allows assigning and removing groups from a user." + } + }, + "userRightTagDescription": { + "inherited": "This right is inherited from \"user\" or \"everyone\" group.", + "dangerous": "", + "administrative": "This right is very dangerous and could allow complete access to the system. It may also give access to restricted or personal infomation and be highly desctructive. Only the most trusted individuals should have this assigned." + }, + "userRightOption": { + "change_user_password": { + "user_groups_blacklist": { + "label": "Blacklisted groups", + "description": "Prevent changing password for users of these groups." + } + }, + "manage_user_groups": { + "allow_creating_user_groups": { + "description": "Allow creating new user groups." + }, + "allow_deleting_user_groups": { + "description": "Allow deleting user groups." + }, + "mutable_user_rights": { + "label": "Mutable user rights", + "description": "Rights that are allowed to be added or removed from a group." + } + }, + "assign_user_groups": { + "allow_assigning_any_group": { + "description": "Allow assigning any group to a user." + }, + "assignable_user_groups": { + "label": "Assignable user groups", + "description": "List of user groups that are allowed to be assigned or removed from a user." + } + } + } + }, + "error": { + "code": { + "auth.passwords_do_not_match": "Incorrect password", + "auth.other": "Other", + "auth.authentication_forbidden": "Authentication forbidden", + "auth.user_does_not_exist": "User does not exist", + "create_user_group.unauthorized": "Permission denied", + "create_user_group.other": "Other", + "create_user.unauthorized": "Permission denied", + "create_user.username_taken": "Username is already taken", + "create_user.other": "Other", + "delete_user_group.unauthorized": "Permission denied", + "delete_user_group.other": "Other", + "delete_user.unauthorized": "Permission denied", + "update_password.unauthorized": "Permission denied", + "update_password.user_not_found": "User not found", + "update_password.other": "Other", + "update_user_group_membership.unauthorized": "Permission denied", + "update_user_group_membership.other": "Other", + "update_user_groups.unauthorized": "Permission denied", + "update_user_groups.other": "Other", + "user_group.not_found": "User group not found", + "users.not_found": "User not found" + } + } +} diff --git a/y-web/package.json b/y-web/package.json index 11973be..6807c01 100644 --- a/y-web/package.json +++ b/y-web/package.json @@ -20,16 +20,20 @@ "eslint-plugin-sonarjs": "^0.19.0", "eslint-plugin-unicorn": "^47.0.0", "husky": "^8.0.3", - "less": "^4.1.3", + "less": "^4.2.0", "prettier": "^2.8.8", "source-map-explorer": "^2.5.3", "typescript": "^5.1.6", - "vite": "^4.1.1", + "vite": "^4.5.1", "vite-plugin-solid": "^2.5.0" }, "dependencies": { - "@solidjs/router": "^0.8.2", + "@solid-primitives/i18n": "^2.0.0", + "@solidjs/router": "^0.8.3", + "@tanstack/solid-query": "^4.32.6", + "date-fns": "^2.30.0", "material-symbols": "^0.10.0", - "solid-js": "^1.7.8" + "solid-js": "^1.8.7", + "zod": "^3.21.4" } } diff --git a/y-web/pnpm-lock.yaml b/y-web/pnpm-lock.yaml index 836196b..15146a6 100644 --- a/y-web/pnpm-lock.yaml +++ b/y-web/pnpm-lock.yaml @@ -1,48 +1,79 @@ -lockfileVersion: 5.4 - -specifiers: - '@solidjs/router': ^0.8.2 - '@trivago/prettier-plugin-sort-imports': ^4.1.1 - '@typescript-eslint/eslint-plugin': ^5.59.9 - '@typescript-eslint/parser': ^5.62.0 - eslint: ^8.42.0 - eslint-plugin-solid: ^0.12.1 - eslint-plugin-sonarjs: ^0.19.0 - eslint-plugin-unicorn: ^47.0.0 - husky: ^8.0.3 - less: ^4.1.3 - material-symbols: ^0.10.0 - prettier: ^2.8.8 - solid-js: ^1.7.8 - source-map-explorer: ^2.5.3 - typescript: ^5.1.6 - vite: ^4.1.1 - vite-plugin-solid: ^2.5.0 +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false dependencies: - '@solidjs/router': 0.8.2_solid-js@1.7.8 - material-symbols: 0.10.0 - solid-js: 1.7.8 + '@solid-primitives/i18n': + specifier: ^2.0.0 + version: 2.0.0(solid-js@1.8.7) + '@solidjs/router': + specifier: ^0.8.3 + version: 0.8.3(solid-js@1.8.7) + '@tanstack/solid-query': + specifier: ^4.32.6 + version: 4.32.6(solid-js@1.8.7) + date-fns: + specifier: ^2.30.0 + version: 2.30.0 + material-symbols: + specifier: ^0.10.0 + version: 0.10.0 + solid-js: + specifier: ^1.8.7 + version: 1.8.7 + zod: + specifier: ^3.21.4 + version: 3.21.4 devDependencies: - '@trivago/prettier-plugin-sort-imports': 4.1.1_prettier@2.8.8 - '@typescript-eslint/eslint-plugin': 5.59.9_rb335mcpj44evzzhtrutinykce - '@typescript-eslint/parser': 5.62.0_2hgafj6nysxfrciqm6sw63gw3m - eslint: 8.42.0 - eslint-plugin-solid: 0.12.1_2hgafj6nysxfrciqm6sw63gw3m - eslint-plugin-sonarjs: 0.19.0_eslint@8.42.0 - eslint-plugin-unicorn: 47.0.0_eslint@8.42.0 - husky: 8.0.3 - less: 4.1.3 - prettier: 2.8.8 - source-map-explorer: 2.5.3 - typescript: 5.1.6 - vite: 4.1.1_less@4.1.3 - vite-plugin-solid: 2.5.0_solid-js@1.7.8+vite@4.1.1 + '@trivago/prettier-plugin-sort-imports': + specifier: ^4.1.1 + version: 4.1.1(prettier@2.8.8) + '@typescript-eslint/eslint-plugin': + specifier: ^5.59.9 + version: 5.59.9(@typescript-eslint/parser@5.62.0)(eslint@8.42.0)(typescript@5.1.6) + '@typescript-eslint/parser': + specifier: ^5.62.0 + version: 5.62.0(eslint@8.42.0)(typescript@5.1.6) + eslint: + specifier: ^8.42.0 + version: 8.42.0 + eslint-plugin-solid: + specifier: ^0.12.1 + version: 0.12.1(eslint@8.42.0)(typescript@5.1.6) + eslint-plugin-sonarjs: + specifier: ^0.19.0 + version: 0.19.0(eslint@8.42.0) + eslint-plugin-unicorn: + specifier: ^47.0.0 + version: 47.0.0(eslint@8.42.0) + husky: + specifier: ^8.0.3 + version: 8.0.3 + less: + specifier: ^4.2.0 + version: 4.2.0 + prettier: + specifier: ^2.8.8 + version: 2.8.8 + source-map-explorer: + specifier: ^2.5.3 + version: 2.5.3 + typescript: + specifier: ^5.1.6 + version: 5.1.6 + vite: + specifier: ^4.5.1 + version: 4.5.1(less@4.2.0) + vite-plugin-solid: + specifier: ^2.5.0 + version: 2.5.0(solid-js@1.8.7)(vite@4.5.1) packages: - /@ampproject/remapping/2.2.0: + /@ampproject/remapping@2.2.0: resolution: {integrity: sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==} engines: {node: '>=6.0.0'} dependencies: @@ -50,31 +81,39 @@ packages: '@jridgewell/trace-mapping': 0.3.17 dev: true - /@babel/code-frame/7.18.6: + /@babel/code-frame@7.18.6: resolution: {integrity: sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==} engines: {node: '>=6.9.0'} dependencies: '@babel/highlight': 7.18.6 dev: true - /@babel/compat-data/7.20.10: + /@babel/code-frame@7.23.5: + resolution: {integrity: sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/highlight': 7.23.4 + chalk: 2.4.2 + dev: true + + /@babel/compat-data@7.20.10: resolution: {integrity: sha512-sEnuDPpOJR/fcafHMjpcpGN5M2jbUGUHwmuWKM/YdPzeEDJg8bgmbcWQFUfE32MQjti1koACvoPVsDe8Uq+idg==} engines: {node: '>=6.9.0'} dev: true - /@babel/core/7.20.7: + /@babel/core@7.20.7: resolution: {integrity: sha512-t1ZjCluspe5DW24bn2Rr1CDb2v9rn/hROtg9a2tmd0+QYf4bsloYfLQzjG4qHPNMhWtKdGC33R5AxGR2Af2cBw==} engines: {node: '>=6.9.0'} dependencies: '@ampproject/remapping': 2.2.0 '@babel/code-frame': 7.18.6 '@babel/generator': 7.20.7 - '@babel/helper-compilation-targets': 7.20.7_@babel+core@7.20.7 + '@babel/helper-compilation-targets': 7.20.7(@babel/core@7.20.7) '@babel/helper-module-transforms': 7.20.11 '@babel/helpers': 7.20.7 '@babel/parser': 7.20.7 '@babel/template': 7.20.7 - '@babel/traverse': 7.20.10 + '@babel/traverse': 7.23.7 '@babel/types': 7.20.7 convert-source-map: 1.9.0 debug: 4.3.4 @@ -85,7 +124,7 @@ packages: - supports-color dev: true - /@babel/generator/7.17.7: + /@babel/generator@7.17.7: resolution: {integrity: sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w==} engines: {node: '>=6.9.0'} dependencies: @@ -94,7 +133,7 @@ packages: source-map: 0.5.7 dev: true - /@babel/generator/7.20.7: + /@babel/generator@7.20.7: resolution: {integrity: sha512-7wqMOJq8doJMZmP4ApXTzLxSr7+oO2jroJURrVEp6XShrQUObV8Tq/D0NCcoYg2uHqUrjzO0zwBjoYzelxK+sw==} engines: {node: '>=6.9.0'} dependencies: @@ -103,14 +142,24 @@ packages: jsesc: 2.5.2 dev: true - /@babel/helper-annotate-as-pure/7.18.6: + /@babel/generator@7.23.6: + resolution: {integrity: sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.6 + '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/trace-mapping': 0.3.20 + jsesc: 2.5.2 + dev: true + + /@babel/helper-annotate-as-pure@7.18.6: resolution: {integrity: sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==} engines: {node: '>=6.9.0'} dependencies: '@babel/types': 7.20.7 dev: true - /@babel/helper-compilation-targets/7.20.7_@babel+core@7.20.7: + /@babel/helper-compilation-targets@7.20.7(@babel/core@7.20.7): resolution: {integrity: sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ==} engines: {node: '>=6.9.0'} peerDependencies: @@ -124,7 +173,7 @@ packages: semver: 6.3.0 dev: true - /@babel/helper-create-class-features-plugin/7.20.7_@babel+core@7.20.7: + /@babel/helper-create-class-features-plugin@7.20.7(@babel/core@7.20.7): resolution: {integrity: sha512-LtoWbDXOaidEf50hmdDqn9g8VEzsorMexoWMQdQODbvmqYmaF23pBP5VNPAGIFHsFQCIeKokDiz3CH5Y2jlY6w==} engines: {node: '>=6.9.0'} peerDependencies: @@ -142,12 +191,17 @@ packages: - supports-color dev: true - /@babel/helper-environment-visitor/7.18.9: + /@babel/helper-environment-visitor@7.18.9: resolution: {integrity: sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==} engines: {node: '>=6.9.0'} dev: true - /@babel/helper-function-name/7.19.0: + /@babel/helper-environment-visitor@7.22.20: + resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-function-name@7.19.0: resolution: {integrity: sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==} engines: {node: '>=6.9.0'} dependencies: @@ -155,35 +209,50 @@ packages: '@babel/types': 7.20.7 dev: true - /@babel/helper-hoist-variables/7.18.6: + /@babel/helper-function-name@7.23.0: + resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.22.15 + '@babel/types': 7.23.6 + dev: true + + /@babel/helper-hoist-variables@7.18.6: resolution: {integrity: sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==} engines: {node: '>=6.9.0'} dependencies: '@babel/types': 7.20.7 dev: true - /@babel/helper-member-expression-to-functions/7.20.7: + /@babel/helper-hoist-variables@7.22.5: + resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.6 + dev: true + + /@babel/helper-member-expression-to-functions@7.20.7: resolution: {integrity: sha512-9J0CxJLq315fEdi4s7xK5TQaNYjZw+nDVpVqr1axNGKzdrdwYBD5b4uKv3n75aABG0rCCTK8Im8Ww7eYfMrZgw==} engines: {node: '>=6.9.0'} dependencies: '@babel/types': 7.20.7 dev: true - /@babel/helper-module-imports/7.16.0: + /@babel/helper-module-imports@7.16.0: resolution: {integrity: sha512-kkH7sWzKPq0xt3H1n+ghb4xEMP8k0U7XV3kkB+ZGy69kDk2ySFW1qPi06sjKzFY3t1j6XbJSqr4mF9L7CYVyhg==} engines: {node: '>=6.9.0'} dependencies: '@babel/types': 7.20.7 dev: true - /@babel/helper-module-imports/7.18.6: + /@babel/helper-module-imports@7.18.6: resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} engines: {node: '>=6.9.0'} dependencies: '@babel/types': 7.20.7 dev: true - /@babel/helper-module-transforms/7.20.11: + /@babel/helper-module-transforms@7.20.11: resolution: {integrity: sha512-uRy78kN4psmji1s2QtbtcCSaj/LILFDp0f/ymhpQH5QY3nljUZCaNWz9X1dEj/8MBdBEFECs7yRhKn8i7NjZgg==} engines: {node: '>=6.9.0'} dependencies: @@ -193,25 +262,25 @@ packages: '@babel/helper-split-export-declaration': 7.18.6 '@babel/helper-validator-identifier': 7.19.1 '@babel/template': 7.20.7 - '@babel/traverse': 7.20.10 + '@babel/traverse': 7.23.7 '@babel/types': 7.20.7 transitivePeerDependencies: - supports-color dev: true - /@babel/helper-optimise-call-expression/7.18.6: + /@babel/helper-optimise-call-expression@7.18.6: resolution: {integrity: sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==} engines: {node: '>=6.9.0'} dependencies: '@babel/types': 7.20.7 dev: true - /@babel/helper-plugin-utils/7.20.2: + /@babel/helper-plugin-utils@7.20.2: resolution: {integrity: sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==} engines: {node: '>=6.9.0'} dev: true - /@babel/helper-replace-supers/7.20.7: + /@babel/helper-replace-supers@7.20.7: resolution: {integrity: sha512-vujDMtB6LVfNW13jhlCrp48QNslK6JXi7lQG736HVbHz/mbf4Dc7tIRh1Xf5C0rF7BP8iiSxGMCmY6Ci1ven3A==} engines: {node: '>=6.9.0'} dependencies: @@ -219,53 +288,70 @@ packages: '@babel/helper-member-expression-to-functions': 7.20.7 '@babel/helper-optimise-call-expression': 7.18.6 '@babel/template': 7.20.7 - '@babel/traverse': 7.20.10 + '@babel/traverse': 7.23.7 '@babel/types': 7.20.7 transitivePeerDependencies: - supports-color dev: true - /@babel/helper-simple-access/7.20.2: + /@babel/helper-simple-access@7.20.2: resolution: {integrity: sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==} engines: {node: '>=6.9.0'} dependencies: '@babel/types': 7.20.7 dev: true - /@babel/helper-split-export-declaration/7.18.6: + /@babel/helper-split-export-declaration@7.18.6: resolution: {integrity: sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==} engines: {node: '>=6.9.0'} dependencies: '@babel/types': 7.20.7 dev: true - /@babel/helper-string-parser/7.19.4: + /@babel/helper-split-export-declaration@7.22.6: + resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.6 + dev: true + + /@babel/helper-string-parser@7.19.4: resolution: {integrity: sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==} engines: {node: '>=6.9.0'} dev: true - /@babel/helper-validator-identifier/7.19.1: + /@babel/helper-string-parser@7.23.4: + resolution: {integrity: sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-identifier@7.19.1: resolution: {integrity: sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==} engines: {node: '>=6.9.0'} dev: true - /@babel/helper-validator-option/7.18.6: + /@babel/helper-validator-identifier@7.22.20: + resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-option@7.18.6: resolution: {integrity: sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==} engines: {node: '>=6.9.0'} dev: true - /@babel/helpers/7.20.7: + /@babel/helpers@7.20.7: resolution: {integrity: sha512-PBPjs5BppzsGaxHQCDKnZ6Gd9s6xl8bBCluz3vEInLGRJmnZan4F6BYCeqtyXqkk4W5IlPmjK4JlOuZkpJ3xZA==} engines: {node: '>=6.9.0'} dependencies: '@babel/template': 7.20.7 - '@babel/traverse': 7.20.10 + '@babel/traverse': 7.23.7 '@babel/types': 7.20.7 transitivePeerDependencies: - supports-color dev: true - /@babel/highlight/7.18.6: + /@babel/highlight@7.18.6: resolution: {integrity: sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==} engines: {node: '>=6.9.0'} dependencies: @@ -274,7 +360,16 @@ packages: js-tokens: 4.0.0 dev: true - /@babel/parser/7.20.7: + /@babel/highlight@7.23.4: + resolution: {integrity: sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.22.20 + chalk: 2.4.2 + js-tokens: 4.0.0 + dev: true + + /@babel/parser@7.20.7: resolution: {integrity: sha512-T3Z9oHybU+0vZlY9CiDSJQTD5ZapcW18ZctFMi0MOAl/4BjFF4ul7NVSARLdbGO5vDqy9eQiGTV0LtKfvCYvcg==} engines: {node: '>=6.0.0'} hasBin: true @@ -282,7 +377,15 @@ packages: '@babel/types': 7.20.7 dev: true - /@babel/plugin-syntax-jsx/7.18.6_@babel+core@7.20.7: + /@babel/parser@7.23.6: + resolution: {integrity: sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.23.6 + dev: true + + /@babel/plugin-syntax-jsx@7.18.6(@babel/core@7.20.7): resolution: {integrity: sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==} engines: {node: '>=6.9.0'} peerDependencies: @@ -292,7 +395,7 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true - /@babel/plugin-syntax-typescript/7.20.0_@babel+core@7.20.7: + /@babel/plugin-syntax-typescript@7.20.0(@babel/core@7.20.7): resolution: {integrity: sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ==} engines: {node: '>=6.9.0'} peerDependencies: @@ -302,21 +405,21 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true - /@babel/plugin-transform-typescript/7.20.7_@babel+core@7.20.7: + /@babel/plugin-transform-typescript@7.20.7(@babel/core@7.20.7): resolution: {integrity: sha512-m3wVKEvf6SoszD8pu4NZz3PvfKRCMgk6D6d0Qi9hNnlM5M6CFS92EgF4EiHVLKbU0r/r7ty1hg7NPZwE7WRbYw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.7 - '@babel/helper-create-class-features-plugin': 7.20.7_@babel+core@7.20.7 + '@babel/helper-create-class-features-plugin': 7.20.7(@babel/core@7.20.7) '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-typescript': 7.20.0_@babel+core@7.20.7 + '@babel/plugin-syntax-typescript': 7.20.0(@babel/core@7.20.7) transitivePeerDependencies: - supports-color dev: true - /@babel/preset-typescript/7.18.6_@babel+core@7.20.7: + /@babel/preset-typescript@7.18.6(@babel/core@7.20.7): resolution: {integrity: sha512-s9ik86kXBAnD760aybBucdpnLsAt0jK1xqJn2juOn9lkOvSHV60os5hxoVJsPzMQxvnUJFAlkont2DvvaYEBtQ==} engines: {node: '>=6.9.0'} peerDependencies: @@ -325,12 +428,19 @@ packages: '@babel/core': 7.20.7 '@babel/helper-plugin-utils': 7.20.2 '@babel/helper-validator-option': 7.18.6 - '@babel/plugin-transform-typescript': 7.20.7_@babel+core@7.20.7 + '@babel/plugin-transform-typescript': 7.20.7(@babel/core@7.20.7) transitivePeerDependencies: - supports-color dev: true - /@babel/template/7.20.7: + /@babel/runtime@7.22.10: + resolution: {integrity: sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.14.0 + dev: false + + /@babel/template@7.20.7: resolution: {integrity: sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==} engines: {node: '>=6.9.0'} dependencies: @@ -339,7 +449,16 @@ packages: '@babel/types': 7.20.7 dev: true - /@babel/traverse/7.17.3: + /@babel/template@7.22.15: + resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.23.5 + '@babel/parser': 7.23.6 + '@babel/types': 7.23.6 + dev: true + + /@babel/traverse@7.17.3: resolution: {integrity: sha512-5irClVky7TxRWIRtxlh2WPUUOLhcPN06AGgaQSB8AEwuyEBgJVuJ5imdHm5zxk8w0QS5T+tDfnDxAlhWjpb7cw==} engines: {node: '>=6.9.0'} dependencies: @@ -357,25 +476,25 @@ packages: - supports-color dev: true - /@babel/traverse/7.20.10: - resolution: {integrity: sha512-oSf1juCgymrSez8NI4A2sr4+uB/mFd9MXplYGPEBnfAuWmmyeVcHa6xLPiaRBcXkcb/28bgxmQLTVwFKE1yfsg==} + /@babel/traverse@7.23.7: + resolution: {integrity: sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg==} engines: {node: '>=6.9.0'} dependencies: - '@babel/code-frame': 7.18.6 - '@babel/generator': 7.20.7 - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-function-name': 7.19.0 - '@babel/helper-hoist-variables': 7.18.6 - '@babel/helper-split-export-declaration': 7.18.6 - '@babel/parser': 7.20.7 - '@babel/types': 7.20.7 + '@babel/code-frame': 7.23.5 + '@babel/generator': 7.23.6 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-hoist-variables': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/parser': 7.23.6 + '@babel/types': 7.23.6 debug: 4.3.4 globals: 11.12.0 transitivePeerDependencies: - supports-color dev: true - /@babel/types/7.17.0: + /@babel/types@7.17.0: resolution: {integrity: sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==} engines: {node: '>=6.9.0'} dependencies: @@ -383,7 +502,7 @@ packages: to-fast-properties: 2.0.0 dev: true - /@babel/types/7.20.7: + /@babel/types@7.20.7: resolution: {integrity: sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg==} engines: {node: '>=6.9.0'} dependencies: @@ -392,26 +511,35 @@ packages: to-fast-properties: 2.0.0 dev: true - /@esbuild/android-arm/0.16.17: - resolution: {integrity: sha512-N9x1CMXVhtWEAMS7pNNONyA14f71VPQN9Cnavj1XQh6T7bskqiLLrSca4O0Vr8Wdcga943eThxnVp3JLnBMYtw==} + /@babel/types@7.23.6: + resolution: {integrity: sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.23.4 + '@babel/helper-validator-identifier': 7.22.20 + to-fast-properties: 2.0.0 + dev: true + + /@esbuild/android-arm64@0.18.20: + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} engines: {node: '>=12'} - cpu: [arm] + cpu: [arm64] os: [android] requiresBuild: true dev: true optional: true - /@esbuild/android-arm64/0.16.17: - resolution: {integrity: sha512-MIGl6p5sc3RDTLLkYL1MyL8BMRN4tLMRCn+yRJJmEDvYZ2M7tmAf80hx1kbNEUX2KJ50RRtxZ4JHLvCfuB6kBg==} + /@esbuild/android-arm@0.18.20: + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} engines: {node: '>=12'} - cpu: [arm64] + cpu: [arm] os: [android] requiresBuild: true dev: true optional: true - /@esbuild/android-x64/0.16.17: - resolution: {integrity: sha512-a3kTv3m0Ghh4z1DaFEuEDfz3OLONKuFvI4Xqczqx4BqLyuFaFkuaG4j2MtA6fuWEFeC5x9IvqnX7drmRq/fyAQ==} + /@esbuild/android-x64@0.18.20: + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} engines: {node: '>=12'} cpu: [x64] os: [android] @@ -419,8 +547,8 @@ packages: dev: true optional: true - /@esbuild/darwin-arm64/0.16.17: - resolution: {integrity: sha512-/2agbUEfmxWHi9ARTX6OQ/KgXnOWfsNlTeLcoV7HSuSTv63E4DqtAc+2XqGw1KHxKMHGZgbVCZge7HXWX9Vn+w==} + /@esbuild/darwin-arm64@0.18.20: + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} engines: {node: '>=12'} cpu: [arm64] os: [darwin] @@ -428,8 +556,8 @@ packages: dev: true optional: true - /@esbuild/darwin-x64/0.16.17: - resolution: {integrity: sha512-2By45OBHulkd9Svy5IOCZt376Aa2oOkiE9QWUK9fe6Tb+WDr8hXL3dpqi+DeLiMed8tVXspzsTAvd0jUl96wmg==} + /@esbuild/darwin-x64@0.18.20: + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} engines: {node: '>=12'} cpu: [x64] os: [darwin] @@ -437,8 +565,8 @@ packages: dev: true optional: true - /@esbuild/freebsd-arm64/0.16.17: - resolution: {integrity: sha512-mt+cxZe1tVx489VTb4mBAOo2aKSnJ33L9fr25JXpqQqzbUIw/yzIzi+NHwAXK2qYV1lEFp4OoVeThGjUbmWmdw==} + /@esbuild/freebsd-arm64@0.18.20: + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} engines: {node: '>=12'} cpu: [arm64] os: [freebsd] @@ -446,8 +574,8 @@ packages: dev: true optional: true - /@esbuild/freebsd-x64/0.16.17: - resolution: {integrity: sha512-8ScTdNJl5idAKjH8zGAsN7RuWcyHG3BAvMNpKOBaqqR7EbUhhVHOqXRdL7oZvz8WNHL2pr5+eIT5c65kA6NHug==} + /@esbuild/freebsd-x64@0.18.20: + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} engines: {node: '>=12'} cpu: [x64] os: [freebsd] @@ -455,26 +583,26 @@ packages: dev: true optional: true - /@esbuild/linux-arm/0.16.17: - resolution: {integrity: sha512-iihzrWbD4gIT7j3caMzKb/RsFFHCwqqbrbH9SqUSRrdXkXaygSZCZg1FybsZz57Ju7N/SHEgPyaR0LZ8Zbe9gQ==} + /@esbuild/linux-arm64@0.18.20: + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} engines: {node: '>=12'} - cpu: [arm] + cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /@esbuild/linux-arm64/0.16.17: - resolution: {integrity: sha512-7S8gJnSlqKGVJunnMCrXHU9Q8Q/tQIxk/xL8BqAP64wchPCTzuM6W3Ra8cIa1HIflAvDnNOt2jaL17vaW+1V0g==} + /@esbuild/linux-arm@0.18.20: + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} engines: {node: '>=12'} - cpu: [arm64] + cpu: [arm] os: [linux] requiresBuild: true dev: true optional: true - /@esbuild/linux-ia32/0.16.17: - resolution: {integrity: sha512-kiX69+wcPAdgl3Lonh1VI7MBr16nktEvOfViszBSxygRQqSpzv7BffMKRPMFwzeJGPxcio0pdD3kYQGpqQ2SSg==} + /@esbuild/linux-ia32@0.18.20: + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} engines: {node: '>=12'} cpu: [ia32] os: [linux] @@ -482,8 +610,8 @@ packages: dev: true optional: true - /@esbuild/linux-loong64/0.16.17: - resolution: {integrity: sha512-dTzNnQwembNDhd654cA4QhbS9uDdXC3TKqMJjgOWsC0yNCbpzfWoXdZvp0mY7HU6nzk5E0zpRGGx3qoQg8T2DQ==} + /@esbuild/linux-loong64@0.18.20: + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} engines: {node: '>=12'} cpu: [loong64] os: [linux] @@ -491,8 +619,8 @@ packages: dev: true optional: true - /@esbuild/linux-mips64el/0.16.17: - resolution: {integrity: sha512-ezbDkp2nDl0PfIUn0CsQ30kxfcLTlcx4Foz2kYv8qdC6ia2oX5Q3E/8m6lq84Dj/6b0FrkgD582fJMIfHhJfSw==} + /@esbuild/linux-mips64el@0.18.20: + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} engines: {node: '>=12'} cpu: [mips64el] os: [linux] @@ -500,8 +628,8 @@ packages: dev: true optional: true - /@esbuild/linux-ppc64/0.16.17: - resolution: {integrity: sha512-dzS678gYD1lJsW73zrFhDApLVdM3cUF2MvAa1D8K8KtcSKdLBPP4zZSLy6LFZ0jYqQdQ29bjAHJDgz0rVbLB3g==} + /@esbuild/linux-ppc64@0.18.20: + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} engines: {node: '>=12'} cpu: [ppc64] os: [linux] @@ -509,8 +637,8 @@ packages: dev: true optional: true - /@esbuild/linux-riscv64/0.16.17: - resolution: {integrity: sha512-ylNlVsxuFjZK8DQtNUwiMskh6nT0vI7kYl/4fZgV1llP5d6+HIeL/vmmm3jpuoo8+NuXjQVZxmKuhDApK0/cKw==} + /@esbuild/linux-riscv64@0.18.20: + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} engines: {node: '>=12'} cpu: [riscv64] os: [linux] @@ -518,8 +646,8 @@ packages: dev: true optional: true - /@esbuild/linux-s390x/0.16.17: - resolution: {integrity: sha512-gzy7nUTO4UA4oZ2wAMXPNBGTzZFP7mss3aKR2hH+/4UUkCOyqmjXiKpzGrY2TlEUhbbejzXVKKGazYcQTZWA/w==} + /@esbuild/linux-s390x@0.18.20: + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} engines: {node: '>=12'} cpu: [s390x] os: [linux] @@ -527,8 +655,8 @@ packages: dev: true optional: true - /@esbuild/linux-x64/0.16.17: - resolution: {integrity: sha512-mdPjPxfnmoqhgpiEArqi4egmBAMYvaObgn4poorpUaqmvzzbvqbowRllQ+ZgzGVMGKaPkqUmPDOOFQRUFDmeUw==} + /@esbuild/linux-x64@0.18.20: + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} engines: {node: '>=12'} cpu: [x64] os: [linux] @@ -536,8 +664,8 @@ packages: dev: true optional: true - /@esbuild/netbsd-x64/0.16.17: - resolution: {integrity: sha512-/PzmzD/zyAeTUsduZa32bn0ORug+Jd1EGGAUJvqfeixoEISYpGnAezN6lnJoskauoai0Jrs+XSyvDhppCPoKOA==} + /@esbuild/netbsd-x64@0.18.20: + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} engines: {node: '>=12'} cpu: [x64] os: [netbsd] @@ -545,8 +673,8 @@ packages: dev: true optional: true - /@esbuild/openbsd-x64/0.16.17: - resolution: {integrity: sha512-2yaWJhvxGEz2RiftSk0UObqJa/b+rIAjnODJgv2GbGGpRwAfpgzyrg1WLK8rqA24mfZa9GvpjLcBBg8JHkoodg==} + /@esbuild/openbsd-x64@0.18.20: + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} engines: {node: '>=12'} cpu: [x64] os: [openbsd] @@ -554,8 +682,8 @@ packages: dev: true optional: true - /@esbuild/sunos-x64/0.16.17: - resolution: {integrity: sha512-xtVUiev38tN0R3g8VhRfN7Zl42YCJvyBhRKw1RJjwE1d2emWTVToPLNEQj/5Qxc6lVFATDiy6LjVHYhIPrLxzw==} + /@esbuild/sunos-x64@0.18.20: + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} engines: {node: '>=12'} cpu: [x64] os: [sunos] @@ -563,8 +691,8 @@ packages: dev: true optional: true - /@esbuild/win32-arm64/0.16.17: - resolution: {integrity: sha512-ga8+JqBDHY4b6fQAmOgtJJue36scANy4l/rL97W+0wYmijhxKetzZdKOJI7olaBaMhWt8Pac2McJdZLxXWUEQw==} + /@esbuild/win32-arm64@0.18.20: + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} engines: {node: '>=12'} cpu: [arm64] os: [win32] @@ -572,8 +700,8 @@ packages: dev: true optional: true - /@esbuild/win32-ia32/0.16.17: - resolution: {integrity: sha512-WnsKaf46uSSF/sZhwnqE4L/F89AYNMiD4YtEcYekBt9Q7nj0DiId2XH2Ng2PHM54qi5oPrQ8luuzGszqi/veig==} + /@esbuild/win32-ia32@0.18.20: + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} engines: {node: '>=12'} cpu: [ia32] os: [win32] @@ -581,8 +709,8 @@ packages: dev: true optional: true - /@esbuild/win32-x64/0.16.17: - resolution: {integrity: sha512-y+EHuSchhL7FjHgvQL/0fnnFmO4T1bhvWANX6gcnqTjtnKWbTvUMCpGnv2+t+31d7RzyEAYAd4u2fnIhHL6N/Q==} + /@esbuild/win32-x64@0.18.20: + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} engines: {node: '>=12'} cpu: [x64] os: [win32] @@ -590,7 +718,7 @@ packages: dev: true optional: true - /@eslint-community/eslint-utils/4.4.0_eslint@8.42.0: + /@eslint-community/eslint-utils@4.4.0(eslint@8.42.0): resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -600,12 +728,12 @@ packages: eslint-visitor-keys: 3.4.1 dev: true - /@eslint-community/regexpp/4.5.1: + /@eslint-community/regexpp@4.5.1: resolution: {integrity: sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} dev: true - /@eslint/eslintrc/2.0.3: + /@eslint/eslintrc@2.0.3: resolution: {integrity: sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: @@ -622,12 +750,12 @@ packages: - supports-color dev: true - /@eslint/js/8.42.0: + /@eslint/js@8.42.0: resolution: {integrity: sha512-6SWlXpWU5AvId8Ac7zjzmIOqMOba/JWY8XZ4A7q7Gn1Vlfg/SFFIlrtHXt9nPn4op9ZPAkl91Jao+QQv3r/ukw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /@humanwhocodes/config-array/0.11.10: + /@humanwhocodes/config-array@0.11.10: resolution: {integrity: sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==} engines: {node: '>=10.10.0'} dependencies: @@ -638,16 +766,16 @@ packages: - supports-color dev: true - /@humanwhocodes/module-importer/1.0.1: + /@humanwhocodes/module-importer@1.0.1: resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} dev: true - /@humanwhocodes/object-schema/1.2.1: + /@humanwhocodes/object-schema@1.2.1: resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} dev: true - /@jridgewell/gen-mapping/0.1.1: + /@jridgewell/gen-mapping@0.1.1: resolution: {integrity: sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==} engines: {node: '>=6.0.0'} dependencies: @@ -655,7 +783,7 @@ packages: '@jridgewell/sourcemap-codec': 1.4.14 dev: true - /@jridgewell/gen-mapping/0.3.2: + /@jridgewell/gen-mapping@0.3.2: resolution: {integrity: sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==} engines: {node: '>=6.0.0'} dependencies: @@ -664,28 +792,53 @@ packages: '@jridgewell/trace-mapping': 0.3.17 dev: true - /@jridgewell/resolve-uri/3.1.0: + /@jridgewell/gen-mapping@0.3.3: + resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/set-array': 1.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.20 + dev: true + + /@jridgewell/resolve-uri@3.1.0: resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} engines: {node: '>=6.0.0'} dev: true - /@jridgewell/set-array/1.1.2: + /@jridgewell/resolve-uri@3.1.1: + resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/set-array@1.1.2: resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} engines: {node: '>=6.0.0'} dev: true - /@jridgewell/sourcemap-codec/1.4.14: + /@jridgewell/sourcemap-codec@1.4.14: resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} dev: true - /@jridgewell/trace-mapping/0.3.17: + /@jridgewell/sourcemap-codec@1.4.15: + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + dev: true + + /@jridgewell/trace-mapping@0.3.17: resolution: {integrity: sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==} dependencies: '@jridgewell/resolve-uri': 3.1.0 '@jridgewell/sourcemap-codec': 1.4.14 dev: true - /@nodelib/fs.scandir/2.1.5: + /@jridgewell/trace-mapping@0.3.20: + resolution: {integrity: sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==} + dependencies: + '@jridgewell/resolve-uri': 3.1.1 + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /@nodelib/fs.scandir@2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} dependencies: @@ -693,12 +846,12 @@ packages: run-parallel: 1.2.0 dev: true - /@nodelib/fs.stat/2.0.5: + /@nodelib/fs.stat@2.0.5: resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} engines: {node: '>= 8'} dev: true - /@nodelib/fs.walk/1.2.8: + /@nodelib/fs.walk@1.2.8: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} dependencies: @@ -706,15 +859,36 @@ packages: fastq: 1.15.0 dev: true - /@solidjs/router/0.8.2_solid-js@1.7.8: - resolution: {integrity: sha512-gUKW+LZqxtX6y/Aw6JKyy4gQ9E7dLqp513oB9pSYJR1HM5c56Pf7eijzyXX+b3WuXig18Cxqah4tMtF0YGu80w==} + /@solid-primitives/i18n@2.0.0(solid-js@1.8.7): + resolution: {integrity: sha512-2sfhiUGl3eCPrDnPpQapbKnZ2iVo1IY6aI5vJTozc2fIf/yEM2+LlCY4PwQTf8vkM5B0Pf3jA+7vC70fR6xbmg==} + peerDependencies: + solid-js: ^1.6.12 + dependencies: + solid-js: 1.8.7 + dev: false + + /@solidjs/router@0.8.3(solid-js@1.8.7): + resolution: {integrity: sha512-oJuqQo10rVTiQYhe1qXIG1NyZIZ2YOwHnlLc8Xx+g/iJhFCJo1saLOIrD/Dkh2fpIaIny5ZMkz1cYYqoTYGJbg==} peerDependencies: solid-js: ^1.5.3 dependencies: - solid-js: 1.7.8 + solid-js: 1.8.7 + dev: false + + /@tanstack/query-core@4.32.6: + resolution: {integrity: sha512-YVB+mVWENQwPyv+40qO7flMgKZ0uI41Ph7qXC2Zf1ft5AIGfnXnMZyifB2ghhZ27u+5wm5mlzO4Y6lwwadzxCA==} + dev: false + + /@tanstack/solid-query@4.32.6(solid-js@1.8.7): + resolution: {integrity: sha512-vdAkQDyjhkO/cCB+2O79YdhKukraR7oApZ8Xv81yorqHuF2HZaqceFtm67aBJ/kO+aZkFLFuyb+67UbM7cEyxQ==} + peerDependencies: + solid-js: ^1.5.7 + dependencies: + '@tanstack/query-core': 4.32.6 + solid-js: 1.8.7 dev: false - /@trivago/prettier-plugin-sort-imports/4.1.1_prettier@2.8.8: + /@trivago/prettier-plugin-sort-imports@4.1.1(prettier@2.8.8): resolution: {integrity: sha512-dQ2r2uzNr1x6pJsuh/8x0IRA3CBUB+pWEW3J/7N98axqt7SQSm+2fy0FLNXvXGg77xEDC7KHxJlHfLYyi7PDcw==} peerDependencies: '@vue/compiler-sfc': 3.x @@ -734,19 +908,19 @@ packages: - supports-color dev: true - /@types/json-schema/7.0.12: + /@types/json-schema@7.0.12: resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==} dev: true - /@types/normalize-package-data/2.4.1: + /@types/normalize-package-data@2.4.1: resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} dev: true - /@types/semver/7.5.0: + /@types/semver@7.5.0: resolution: {integrity: sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==} dev: true - /@typescript-eslint/eslint-plugin/5.59.9_rb335mcpj44evzzhtrutinykce: + /@typescript-eslint/eslint-plugin@5.59.9(@typescript-eslint/parser@5.62.0)(eslint@8.42.0)(typescript@5.1.6): resolution: {integrity: sha512-4uQIBq1ffXd2YvF7MAvehWKW3zVv/w+mSfRAu+8cKbfj3nwzyqJLNcZJpQ/WZ1HLbJDiowwmQ6NO+63nCA+fqA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -758,23 +932,23 @@ packages: optional: true dependencies: '@eslint-community/regexpp': 4.5.1 - '@typescript-eslint/parser': 5.62.0_2hgafj6nysxfrciqm6sw63gw3m + '@typescript-eslint/parser': 5.62.0(eslint@8.42.0)(typescript@5.1.6) '@typescript-eslint/scope-manager': 5.59.9 - '@typescript-eslint/type-utils': 5.59.9_2hgafj6nysxfrciqm6sw63gw3m - '@typescript-eslint/utils': 5.59.9_2hgafj6nysxfrciqm6sw63gw3m + '@typescript-eslint/type-utils': 5.59.9(eslint@8.42.0)(typescript@5.1.6) + '@typescript-eslint/utils': 5.59.9(eslint@8.42.0)(typescript@5.1.6) debug: 4.3.4 eslint: 8.42.0 grapheme-splitter: 1.0.4 ignore: 5.2.4 natural-compare-lite: 1.4.0 semver: 7.5.1 - tsutils: 3.21.0_typescript@5.1.6 + tsutils: 3.21.0(typescript@5.1.6) typescript: 5.1.6 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/parser/5.62.0_2hgafj6nysxfrciqm6sw63gw3m: + /@typescript-eslint/parser@5.62.0(eslint@8.42.0)(typescript@5.1.6): resolution: {integrity: sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -786,7 +960,7 @@ packages: dependencies: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 - '@typescript-eslint/typescript-estree': 5.62.0_typescript@5.1.6 + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.1.6) debug: 4.3.4 eslint: 8.42.0 typescript: 5.1.6 @@ -794,7 +968,7 @@ packages: - supports-color dev: true - /@typescript-eslint/scope-manager/5.59.9: + /@typescript-eslint/scope-manager@5.59.9: resolution: {integrity: sha512-8RA+E+w78z1+2dzvK/tGZ2cpGigBZ58VMEHDZtpE1v+LLjzrYGc8mMaTONSxKyEkz3IuXFM0IqYiGHlCsmlZxQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: @@ -802,7 +976,7 @@ packages: '@typescript-eslint/visitor-keys': 5.59.9 dev: true - /@typescript-eslint/scope-manager/5.62.0: + /@typescript-eslint/scope-manager@5.62.0: resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: @@ -810,7 +984,7 @@ packages: '@typescript-eslint/visitor-keys': 5.62.0 dev: true - /@typescript-eslint/type-utils/5.59.9_2hgafj6nysxfrciqm6sw63gw3m: + /@typescript-eslint/type-utils@5.59.9(eslint@8.42.0)(typescript@5.1.6): resolution: {integrity: sha512-ksEsT0/mEHg9e3qZu98AlSrONAQtrSTljL3ow9CGej8eRo7pe+yaC/mvTjptp23Xo/xIf2mLZKC6KPv4Sji26Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -820,27 +994,27 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/typescript-estree': 5.59.9_typescript@5.1.6 - '@typescript-eslint/utils': 5.59.9_2hgafj6nysxfrciqm6sw63gw3m + '@typescript-eslint/typescript-estree': 5.59.9(typescript@5.1.6) + '@typescript-eslint/utils': 5.59.9(eslint@8.42.0)(typescript@5.1.6) debug: 4.3.4 eslint: 8.42.0 - tsutils: 3.21.0_typescript@5.1.6 + tsutils: 3.21.0(typescript@5.1.6) typescript: 5.1.6 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/types/5.59.9: + /@typescript-eslint/types@5.59.9: resolution: {integrity: sha512-uW8H5NRgTVneSVTfiCVffBb8AbwWSKg7qcA4Ot3JI3MPCJGsB4Db4BhvAODIIYE5mNj7Q+VJkK7JxmRhk2Lyjw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /@typescript-eslint/types/5.62.0: + /@typescript-eslint/types@5.62.0: resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /@typescript-eslint/typescript-estree/5.59.9_typescript@5.1.6: + /@typescript-eslint/typescript-estree@5.59.9(typescript@5.1.6): resolution: {integrity: sha512-pmM0/VQ7kUhd1QyIxgS+aRvMgw+ZljB3eDb+jYyp6d2bC0mQWLzUDF+DLwCTkQ3tlNyVsvZRXjFyV0LkU/aXjA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -855,13 +1029,13 @@ packages: globby: 11.1.0 is-glob: 4.0.3 semver: 7.5.1 - tsutils: 3.21.0_typescript@5.1.6 + tsutils: 3.21.0(typescript@5.1.6) typescript: 5.1.6 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/typescript-estree/5.62.0_typescript@5.1.6: + /@typescript-eslint/typescript-estree@5.62.0(typescript@5.1.6): resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -876,24 +1050,24 @@ packages: globby: 11.1.0 is-glob: 4.0.3 semver: 7.5.4 - tsutils: 3.21.0_typescript@5.1.6 + tsutils: 3.21.0(typescript@5.1.6) typescript: 5.1.6 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/utils/5.59.9_2hgafj6nysxfrciqm6sw63gw3m: + /@typescript-eslint/utils@5.59.9(eslint@8.42.0)(typescript@5.1.6): resolution: {integrity: sha512-1PuMYsju/38I5Ggblaeb98TOoUvjhRvLpLa1DoTOFaLWqaXl/1iQ1eGurTXgBY58NUdtfTXKP5xBq7q9NDaLKg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: - '@eslint-community/eslint-utils': 4.4.0_eslint@8.42.0 + '@eslint-community/eslint-utils': 4.4.0(eslint@8.42.0) '@types/json-schema': 7.0.12 '@types/semver': 7.5.0 '@typescript-eslint/scope-manager': 5.59.9 '@typescript-eslint/types': 5.59.9 - '@typescript-eslint/typescript-estree': 5.59.9_typescript@5.1.6 + '@typescript-eslint/typescript-estree': 5.59.9(typescript@5.1.6) eslint: 8.42.0 eslint-scope: 5.1.1 semver: 7.5.1 @@ -902,7 +1076,7 @@ packages: - typescript dev: true - /@typescript-eslint/visitor-keys/5.59.9: + /@typescript-eslint/visitor-keys@5.59.9: resolution: {integrity: sha512-bT7s0td97KMaLwpEBckbzj/YohnvXtqbe2XgqNvTl6RJVakY5mvENOTPvw5u66nljfZxthESpDozs86U+oLY8Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: @@ -910,7 +1084,7 @@ packages: eslint-visitor-keys: 3.4.1 dev: true - /@typescript-eslint/visitor-keys/5.62.0: + /@typescript-eslint/visitor-keys@5.62.0: resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: @@ -918,7 +1092,7 @@ packages: eslint-visitor-keys: 3.4.2 dev: true - /acorn-jsx/5.3.2_acorn@8.8.2: + /acorn-jsx@5.3.2(acorn@8.8.2): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -926,13 +1100,13 @@ packages: acorn: 8.8.2 dev: true - /acorn/8.8.2: + /acorn@8.8.2: resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==} engines: {node: '>=0.4.0'} hasBin: true dev: true - /ajv/6.12.6: + /ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} dependencies: fast-deep-equal: 3.1.3 @@ -941,37 +1115,37 @@ packages: uri-js: 4.4.1 dev: true - /ansi-regex/5.0.1: + /ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} dev: true - /ansi-styles/3.2.1: + /ansi-styles@3.2.1: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} engines: {node: '>=4'} dependencies: color-convert: 1.9.3 dev: true - /ansi-styles/4.3.0: + /ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} dependencies: color-convert: 2.0.1 dev: true - /argparse/2.0.1: + /argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} dev: true - /array-buffer-byte-length/1.0.0: + /array-buffer-byte-length@1.0.0: resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} dependencies: call-bind: 1.0.2 is-array-buffer: 3.0.2 dev: true - /array-includes/3.1.6: + /array-includes@3.1.6: resolution: {integrity: sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==} engines: {node: '>= 0.4'} dependencies: @@ -982,12 +1156,12 @@ packages: is-string: 1.0.7 dev: true - /array-union/2.1.0: + /array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} dev: true - /array.prototype.flat/1.3.1: + /array.prototype.flat@1.3.1: resolution: {integrity: sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==} engines: {node: '>= 0.4'} dependencies: @@ -997,61 +1171,61 @@ packages: es-shim-unscopables: 1.0.0 dev: true - /async/3.2.4: + /async@3.2.4: resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==} dev: true - /available-typed-arrays/1.0.5: + /available-typed-arrays@1.0.5: resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} engines: {node: '>= 0.4'} dev: true - /babel-plugin-jsx-dom-expressions/0.35.8_@babel+core@7.20.7: + /babel-plugin-jsx-dom-expressions@0.35.8(@babel/core@7.20.7): resolution: {integrity: sha512-IzObXlDFA80wyEW/IUtCxaUAoJnq4CTpvcvC1xBZBlMpJDwmK6mIYnTZ9xgFyGCrAjC0LxVcqeDQx31gJJ4UJQ==} peerDependencies: '@babel/core': ^7.0.0 dependencies: '@babel/core': 7.20.7 '@babel/helper-module-imports': 7.16.0 - '@babel/plugin-syntax-jsx': 7.18.6_@babel+core@7.20.7 + '@babel/plugin-syntax-jsx': 7.18.6(@babel/core@7.20.7) '@babel/types': 7.20.7 html-entities: 2.3.2 dev: true - /babel-preset-solid/1.6.6_@babel+core@7.20.7: + /babel-preset-solid@1.6.6(@babel/core@7.20.7): resolution: {integrity: sha512-uG6svyjDRmQxLtRyydlJjFkvlOGYEd/xvfUZu58UuzJdiv40lZ34K+EcgbAFD85JPUdlnkr6bbHUpUXP/VK+Jg==} peerDependencies: '@babel/core': ^7.0.0 dependencies: '@babel/core': 7.20.7 - babel-plugin-jsx-dom-expressions: 0.35.8_@babel+core@7.20.7 + babel-plugin-jsx-dom-expressions: 0.35.8(@babel/core@7.20.7) dev: true - /balanced-match/1.0.2: + /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} dev: true - /brace-expansion/1.1.11: + /brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 dev: true - /brace-expansion/2.0.1: + /brace-expansion@2.0.1: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} dependencies: balanced-match: 1.0.2 dev: true - /braces/3.0.2: + /braces@3.0.2: resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} engines: {node: '>=8'} dependencies: fill-range: 7.0.1 dev: true - /browserslist/4.21.4: + /browserslist@4.21.4: resolution: {integrity: sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -1059,37 +1233,37 @@ packages: caniuse-lite: 1.0.30001441 electron-to-chromium: 1.4.284 node-releases: 2.0.8 - update-browserslist-db: 1.0.10_browserslist@4.21.4 + update-browserslist-db: 1.0.10(browserslist@4.21.4) dev: true - /btoa/1.2.1: + /btoa@1.2.1: resolution: {integrity: sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==} engines: {node: '>= 0.4.0'} hasBin: true dev: true - /builtin-modules/3.3.0: + /builtin-modules@3.3.0: resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} engines: {node: '>=6'} dev: true - /call-bind/1.0.2: + /call-bind@1.0.2: resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} dependencies: function-bind: 1.1.1 get-intrinsic: 1.2.1 dev: true - /callsites/3.1.0: + /callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} dev: true - /caniuse-lite/1.0.30001441: + /caniuse-lite@1.0.30001441: resolution: {integrity: sha512-OyxRR4Vof59I3yGWXws6i908EtGbMzVUi3ganaZQHmydk1iwDhRnvaPG2WaR0KcqrDFKrxVZHULT396LEPhXfg==} dev: true - /chalk/2.4.2: + /chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} dependencies: @@ -1098,7 +1272,7 @@ packages: supports-color: 5.5.0 dev: true - /chalk/4.1.2: + /chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} dependencies: @@ -1106,19 +1280,19 @@ packages: supports-color: 7.2.0 dev: true - /ci-info/3.8.0: + /ci-info@3.8.0: resolution: {integrity: sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==} engines: {node: '>=8'} dev: true - /clean-regexp/1.0.0: + /clean-regexp@1.0.0: resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} engines: {node: '>=4'} dependencies: escape-string-regexp: 1.0.5 dev: true - /cliui/7.0.4: + /cliui@7.0.4: resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} dependencies: string-width: 4.2.3 @@ -1126,42 +1300,42 @@ packages: wrap-ansi: 7.0.0 dev: true - /color-convert/1.9.3: + /color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} dependencies: color-name: 1.1.3 dev: true - /color-convert/2.0.1: + /color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} dependencies: color-name: 1.1.4 dev: true - /color-name/1.1.3: + /color-name@1.1.3: resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} dev: true - /color-name/1.1.4: + /color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} dev: true - /concat-map/0.0.1: + /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} dev: true - /convert-source-map/1.9.0: + /convert-source-map@1.9.0: resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} dev: true - /copy-anything/2.0.6: + /copy-anything@2.0.6: resolution: {integrity: sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==} dependencies: is-what: 3.14.1 dev: true - /cross-spawn/7.0.3: + /cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} dependencies: @@ -1170,22 +1344,17 @@ packages: which: 2.0.2 dev: true - /csstype/3.1.1: + /csstype@3.1.1: resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==} - /debug/3.2.7: - resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true + /date-fns@2.30.0: + resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} + engines: {node: '>=0.11'} dependencies: - ms: 2.1.2 - dev: true - optional: true + '@babel/runtime': 7.22.10 + dev: false - /debug/4.3.4: + /debug@4.3.4: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} peerDependencies: @@ -1197,11 +1366,11 @@ packages: ms: 2.1.2 dev: true - /deep-is/0.1.4: + /deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} dev: true - /define-properties/1.2.0: + /define-properties@1.2.0: resolution: {integrity: sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==} engines: {node: '>= 0.4'} dependencies: @@ -1209,25 +1378,25 @@ packages: object-keys: 1.1.1 dev: true - /dir-glob/3.0.1: + /dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} dependencies: path-type: 4.0.0 dev: true - /doctrine/3.0.0: + /doctrine@3.0.0: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} dependencies: esutils: 2.0.3 dev: true - /duplexer/0.1.2: + /duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} dev: true - /ejs/3.1.9: + /ejs@3.1.9: resolution: {integrity: sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==} engines: {node: '>=0.10.0'} hasBin: true @@ -1235,15 +1404,15 @@ packages: jake: 10.8.7 dev: true - /electron-to-chromium/1.4.284: + /electron-to-chromium@1.4.284: resolution: {integrity: sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==} dev: true - /emoji-regex/8.0.0: + /emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} dev: true - /errno/0.1.8: + /errno@0.1.8: resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==} hasBin: true requiresBuild: true @@ -1252,13 +1421,13 @@ packages: dev: true optional: true - /error-ex/1.3.2: + /error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} dependencies: is-arrayish: 0.2.1 dev: true - /es-abstract/1.21.3: + /es-abstract@1.21.3: resolution: {integrity: sha512-ZU4miiY1j3sGPFLJ34VJXEqhpmL+HGByCinGHv4HC+Fxl2fI2Z4yR6tl0mORnDr6PA8eihWo4LmSWDbvhALckg==} engines: {node: '>= 0.4'} dependencies: @@ -1299,7 +1468,7 @@ packages: which-typed-array: 1.1.10 dev: true - /es-set-tostringtag/2.0.1: + /es-set-tostringtag@2.0.1: resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==} engines: {node: '>= 0.4'} dependencies: @@ -1308,13 +1477,13 @@ packages: has-tostringtag: 1.0.0 dev: true - /es-shim-unscopables/1.0.0: + /es-shim-unscopables@1.0.0: resolution: {integrity: sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==} dependencies: has: 1.0.3 dev: true - /es-to-primitive/1.2.1: + /es-to-primitive@1.2.1: resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} engines: {node: '>= 0.4'} dependencies: @@ -1323,62 +1492,62 @@ packages: is-symbol: 1.0.4 dev: true - /esbuild/0.16.17: - resolution: {integrity: sha512-G8LEkV0XzDMNwXKgM0Jwu3nY3lSTwSGY6XbxM9cr9+s0T/qSV1q1JVPBGzm3dcjhCic9+emZDmMffkwgPeOeLg==} + /esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} engines: {node: '>=12'} hasBin: true requiresBuild: true optionalDependencies: - '@esbuild/android-arm': 0.16.17 - '@esbuild/android-arm64': 0.16.17 - '@esbuild/android-x64': 0.16.17 - '@esbuild/darwin-arm64': 0.16.17 - '@esbuild/darwin-x64': 0.16.17 - '@esbuild/freebsd-arm64': 0.16.17 - '@esbuild/freebsd-x64': 0.16.17 - '@esbuild/linux-arm': 0.16.17 - '@esbuild/linux-arm64': 0.16.17 - '@esbuild/linux-ia32': 0.16.17 - '@esbuild/linux-loong64': 0.16.17 - '@esbuild/linux-mips64el': 0.16.17 - '@esbuild/linux-ppc64': 0.16.17 - '@esbuild/linux-riscv64': 0.16.17 - '@esbuild/linux-s390x': 0.16.17 - '@esbuild/linux-x64': 0.16.17 - '@esbuild/netbsd-x64': 0.16.17 - '@esbuild/openbsd-x64': 0.16.17 - '@esbuild/sunos-x64': 0.16.17 - '@esbuild/win32-arm64': 0.16.17 - '@esbuild/win32-ia32': 0.16.17 - '@esbuild/win32-x64': 0.16.17 - dev: true - - /escalade/3.1.1: + '@esbuild/android-arm': 0.18.20 + '@esbuild/android-arm64': 0.18.20 + '@esbuild/android-x64': 0.18.20 + '@esbuild/darwin-arm64': 0.18.20 + '@esbuild/darwin-x64': 0.18.20 + '@esbuild/freebsd-arm64': 0.18.20 + '@esbuild/freebsd-x64': 0.18.20 + '@esbuild/linux-arm': 0.18.20 + '@esbuild/linux-arm64': 0.18.20 + '@esbuild/linux-ia32': 0.18.20 + '@esbuild/linux-loong64': 0.18.20 + '@esbuild/linux-mips64el': 0.18.20 + '@esbuild/linux-ppc64': 0.18.20 + '@esbuild/linux-riscv64': 0.18.20 + '@esbuild/linux-s390x': 0.18.20 + '@esbuild/linux-x64': 0.18.20 + '@esbuild/netbsd-x64': 0.18.20 + '@esbuild/openbsd-x64': 0.18.20 + '@esbuild/sunos-x64': 0.18.20 + '@esbuild/win32-arm64': 0.18.20 + '@esbuild/win32-ia32': 0.18.20 + '@esbuild/win32-x64': 0.18.20 + dev: true + + /escalade@3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} engines: {node: '>=6'} dev: true - /escape-html/1.0.3: + /escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} dev: true - /escape-string-regexp/1.0.5: + /escape-string-regexp@1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} dev: true - /escape-string-regexp/4.0.0: + /escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} dev: true - /eslint-plugin-solid/0.12.1_2hgafj6nysxfrciqm6sw63gw3m: + /eslint-plugin-solid@0.12.1(eslint@8.42.0)(typescript@5.1.6): resolution: {integrity: sha512-fM0sEg9PcS1mcNbWklwc+W/lOv1/XyEwXf53HmFFy4GOA8E3u41h8JW+hc+Vv1m3kh01umKoTalOTET08zKdAQ==} engines: {node: '>=12.0.0'} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: - '@typescript-eslint/utils': 5.59.9_2hgafj6nysxfrciqm6sw63gw3m + '@typescript-eslint/utils': 5.59.9(eslint@8.42.0)(typescript@5.1.6) eslint: 8.42.0 is-html: 2.0.0 jsx-ast-utils: 3.3.4 @@ -1390,7 +1559,7 @@ packages: - typescript dev: true - /eslint-plugin-sonarjs/0.19.0_eslint@8.42.0: + /eslint-plugin-sonarjs@0.19.0(eslint@8.42.0): resolution: {integrity: sha512-6+s5oNk5TFtVlbRxqZN7FIGmjdPCYQKaTzFPmqieCmsU1kBYDzndTeQav0xtQNwZJWu5awWfTGe8Srq9xFOGnw==} engines: {node: '>=14'} peerDependencies: @@ -1399,14 +1568,14 @@ packages: eslint: 8.42.0 dev: true - /eslint-plugin-unicorn/47.0.0_eslint@8.42.0: + /eslint-plugin-unicorn@47.0.0(eslint@8.42.0): resolution: {integrity: sha512-ivB3bKk7fDIeWOUmmMm9o3Ax9zbMz1Bsza/R2qm46ufw4T6VBFBaJIR1uN3pCKSmSXm8/9Nri8V+iUut1NhQGA==} engines: {node: '>=16'} peerDependencies: eslint: '>=8.38.0' dependencies: '@babel/helper-validator-identifier': 7.19.1 - '@eslint-community/eslint-utils': 4.4.0_eslint@8.42.0 + '@eslint-community/eslint-utils': 4.4.0(eslint@8.42.0) ci-info: 3.8.0 clean-regexp: 1.0.0 eslint: 8.42.0 @@ -1424,7 +1593,7 @@ packages: strip-indent: 3.0.0 dev: true - /eslint-scope/5.1.1: + /eslint-scope@5.1.1: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} engines: {node: '>=8.0.0'} dependencies: @@ -1432,7 +1601,7 @@ packages: estraverse: 4.3.0 dev: true - /eslint-scope/7.2.0: + /eslint-scope@7.2.0: resolution: {integrity: sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: @@ -1440,22 +1609,22 @@ packages: estraverse: 5.3.0 dev: true - /eslint-visitor-keys/3.4.1: + /eslint-visitor-keys@3.4.1: resolution: {integrity: sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /eslint-visitor-keys/3.4.2: + /eslint-visitor-keys@3.4.2: resolution: {integrity: sha512-8drBzUEyZ2llkpCA67iYrgEssKDUu68V8ChqqOfFupIaG/LCVPUT+CoGJpT77zJprs4T/W7p07LP7zAIMuweVw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /eslint/8.42.0: + /eslint@8.42.0: resolution: {integrity: sha512-ulg9Ms6E1WPf67PHaEY4/6E2tEn5/f7FXGzr3t9cBMugOmf1INYvuUwwh1aXQN4MfJ6a5K2iNwP3w4AColvI9A==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} hasBin: true dependencies: - '@eslint-community/eslint-utils': 4.4.0_eslint@8.42.0 + '@eslint-community/eslint-utils': 4.4.0(eslint@8.42.0) '@eslint-community/regexpp': 4.5.1 '@eslint/eslintrc': 2.0.3 '@eslint/js': 8.42.0 @@ -1498,49 +1667,49 @@ packages: - supports-color dev: true - /espree/9.5.2: + /espree@9.5.2: resolution: {integrity: sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: acorn: 8.8.2 - acorn-jsx: 5.3.2_acorn@8.8.2 + acorn-jsx: 5.3.2(acorn@8.8.2) eslint-visitor-keys: 3.4.1 dev: true - /esquery/1.5.0: + /esquery@1.5.0: resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} engines: {node: '>=0.10'} dependencies: estraverse: 5.3.0 dev: true - /esrecurse/4.3.0: + /esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} engines: {node: '>=4.0'} dependencies: estraverse: 5.3.0 dev: true - /estraverse/4.3.0: + /estraverse@4.3.0: resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} engines: {node: '>=4.0'} dev: true - /estraverse/5.3.0: + /estraverse@5.3.0: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} dev: true - /esutils/2.0.3: + /esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} dev: true - /fast-deep-equal/3.1.3: + /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} dev: true - /fast-glob/3.3.1: + /fast-glob@3.3.1: resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} engines: {node: '>=8.6.0'} dependencies: @@ -1551,41 +1720,41 @@ packages: micromatch: 4.0.5 dev: true - /fast-json-stable-stringify/2.1.0: + /fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} dev: true - /fast-levenshtein/2.0.6: + /fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} dev: true - /fastq/1.15.0: + /fastq@1.15.0: resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} dependencies: reusify: 1.0.4 dev: true - /file-entry-cache/6.0.1: + /file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} dependencies: flat-cache: 3.0.4 dev: true - /filelist/1.0.4: + /filelist@1.0.4: resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} dependencies: minimatch: 5.1.6 dev: true - /fill-range/7.0.1: + /fill-range@7.0.1: resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} engines: {node: '>=8'} dependencies: to-regex-range: 5.0.1 dev: true - /find-up/4.1.0: + /find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} dependencies: @@ -1593,7 +1762,7 @@ packages: path-exists: 4.0.0 dev: true - /find-up/5.0.0: + /find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} dependencies: @@ -1601,7 +1770,7 @@ packages: path-exists: 4.0.0 dev: true - /flat-cache/3.0.4: + /flat-cache@3.0.4: resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==} engines: {node: ^10.12.0 || >=12.0.0} dependencies: @@ -1609,33 +1778,33 @@ packages: rimraf: 3.0.2 dev: true - /flatted/3.2.7: + /flatted@3.2.7: resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==} dev: true - /for-each/0.3.3: + /for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} dependencies: is-callable: 1.2.7 dev: true - /fs.realpath/1.0.0: + /fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} dev: true - /fsevents/2.3.2: - resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] requiresBuild: true dev: true optional: true - /function-bind/1.1.1: + /function-bind@1.1.1: resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} dev: true - /function.prototype.name/1.1.5: + /function.prototype.name@1.1.5: resolution: {integrity: sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==} engines: {node: '>= 0.4'} dependencies: @@ -1645,21 +1814,21 @@ packages: functions-have-names: 1.2.3 dev: true - /functions-have-names/1.2.3: + /functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} dev: true - /gensync/1.0.0-beta.2: + /gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} dev: true - /get-caller-file/2.0.5: + /get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} dev: true - /get-intrinsic/1.2.1: + /get-intrinsic@1.2.1: resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==} dependencies: function-bind: 1.1.1 @@ -1668,7 +1837,7 @@ packages: has-symbols: 1.0.3 dev: true - /get-symbol-description/1.0.0: + /get-symbol-description@1.0.0: resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} engines: {node: '>= 0.4'} dependencies: @@ -1676,21 +1845,21 @@ packages: get-intrinsic: 1.2.1 dev: true - /glob-parent/5.1.2: + /glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} dependencies: is-glob: 4.0.3 dev: true - /glob-parent/6.0.2: + /glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} dependencies: is-glob: 4.0.3 dev: true - /glob/7.2.3: + /glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} dependencies: fs.realpath: 1.0.0 @@ -1701,26 +1870,26 @@ packages: path-is-absolute: 1.0.1 dev: true - /globals/11.12.0: + /globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} dev: true - /globals/13.20.0: + /globals@13.20.0: resolution: {integrity: sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==} engines: {node: '>=8'} dependencies: type-fest: 0.20.2 dev: true - /globalthis/1.0.3: + /globalthis@1.0.3: resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} engines: {node: '>= 0.4'} dependencies: define-properties: 1.2.0 dev: true - /globby/11.1.0: + /globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} dependencies: @@ -1732,110 +1901,111 @@ packages: slash: 3.0.0 dev: true - /gopd/1.0.1: + /gopd@1.0.1: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} dependencies: get-intrinsic: 1.2.1 dev: true - /graceful-fs/4.2.11: + /graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} requiresBuild: true dev: true optional: true - /grapheme-splitter/1.0.4: + /grapheme-splitter@1.0.4: resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} dev: true - /graphemer/1.4.0: + /graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} dev: true - /gzip-size/6.0.0: + /gzip-size@6.0.0: resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} engines: {node: '>=10'} dependencies: duplexer: 0.1.2 dev: true - /has-bigints/1.0.2: + /has-bigints@1.0.2: resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} dev: true - /has-flag/3.0.0: + /has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} dev: true - /has-flag/4.0.0: + /has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} dev: true - /has-property-descriptors/1.0.0: + /has-property-descriptors@1.0.0: resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} dependencies: get-intrinsic: 1.2.1 dev: true - /has-proto/1.0.1: + /has-proto@1.0.1: resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} engines: {node: '>= 0.4'} dev: true - /has-symbols/1.0.3: + /has-symbols@1.0.3: resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} engines: {node: '>= 0.4'} dev: true - /has-tostringtag/1.0.0: + /has-tostringtag@1.0.0: resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} engines: {node: '>= 0.4'} dependencies: has-symbols: 1.0.3 dev: true - /has/1.0.3: + /has@1.0.3: resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} engines: {node: '>= 0.4.0'} dependencies: function-bind: 1.1.1 dev: true - /hosted-git-info/2.8.9: + /hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} dev: true - /html-entities/2.3.2: + /html-entities@2.3.2: resolution: {integrity: sha512-c3Ab/url5ksaT0WyleslpBEthOzWhrjQbg75y7XUsfSzi3Dgzt0l8w5e7DylRn15MTlMMD58dTfzddNS2kcAjQ==} dev: true - /html-tags/3.3.1: + /html-tags@3.3.1: resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} engines: {node: '>=8'} dev: true - /husky/8.0.3: + /husky@8.0.3: resolution: {integrity: sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==} engines: {node: '>=14'} hasBin: true dev: true - /iconv-lite/0.6.3: + /iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + requiresBuild: true dependencies: safer-buffer: 2.1.2 dev: true optional: true - /ignore/5.2.4: + /ignore@5.2.4: resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} engines: {node: '>= 4'} dev: true - /image-size/0.5.5: + /image-size@0.5.5: resolution: {integrity: sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==} engines: {node: '>=0.10.0'} hasBin: true @@ -1843,7 +2013,7 @@ packages: dev: true optional: true - /import-fresh/3.3.0: + /import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} dependencies: @@ -1851,32 +2021,32 @@ packages: resolve-from: 4.0.0 dev: true - /imurmurhash/0.1.4: + /imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} dev: true - /indent-string/4.0.0: + /indent-string@4.0.0: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} dev: true - /inflight/1.0.6: + /inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} dependencies: once: 1.4.0 wrappy: 1.0.2 dev: true - /inherits/2.0.4: + /inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} dev: true - /inline-style-parser/0.1.1: + /inline-style-parser@0.1.1: resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==} dev: true - /internal-slot/1.0.5: + /internal-slot@1.0.5: resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==} engines: {node: '>= 0.4'} dependencies: @@ -1885,7 +2055,7 @@ packages: side-channel: 1.0.4 dev: true - /is-array-buffer/3.0.2: + /is-array-buffer@3.0.2: resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} dependencies: call-bind: 1.0.2 @@ -1893,17 +2063,17 @@ packages: is-typed-array: 1.1.10 dev: true - /is-arrayish/0.2.1: + /is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} dev: true - /is-bigint/1.0.4: + /is-bigint@1.0.4: resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} dependencies: has-bigints: 1.0.2 dev: true - /is-boolean-object/1.1.2: + /is-boolean-object@1.1.2: resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} engines: {node: '>= 0.4'} dependencies: @@ -1911,84 +2081,84 @@ packages: has-tostringtag: 1.0.0 dev: true - /is-builtin-module/3.2.1: + /is-builtin-module@3.2.1: resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} engines: {node: '>=6'} dependencies: builtin-modules: 3.3.0 dev: true - /is-callable/1.2.7: + /is-callable@1.2.7: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} dev: true - /is-core-module/2.11.0: + /is-core-module@2.11.0: resolution: {integrity: sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==} dependencies: has: 1.0.3 dev: true - /is-date-object/1.0.5: + /is-date-object@1.0.5: resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} engines: {node: '>= 0.4'} dependencies: has-tostringtag: 1.0.0 dev: true - /is-docker/2.2.1: + /is-docker@2.2.1: resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} engines: {node: '>=8'} hasBin: true dev: true - /is-extglob/2.1.1: + /is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} dev: true - /is-fullwidth-code-point/3.0.0: + /is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} dev: true - /is-glob/4.0.3: + /is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} dependencies: is-extglob: 2.1.1 dev: true - /is-html/2.0.0: + /is-html@2.0.0: resolution: {integrity: sha512-S+OpgB5i7wzIue/YSE5hg0e5ZYfG3hhpNh9KGl6ayJ38p7ED6wxQLd1TV91xHpcTvw90KMJ9EwN3F/iNflHBVg==} engines: {node: '>=8'} dependencies: html-tags: 3.3.1 dev: true - /is-negative-zero/2.0.2: + /is-negative-zero@2.0.2: resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} engines: {node: '>= 0.4'} dev: true - /is-number-object/1.0.7: + /is-number-object@1.0.7: resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} engines: {node: '>= 0.4'} dependencies: has-tostringtag: 1.0.0 dev: true - /is-number/7.0.0: + /is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} dev: true - /is-path-inside/3.0.3: + /is-path-inside@3.0.3: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} dev: true - /is-regex/1.1.4: + /is-regex@1.1.4: resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} engines: {node: '>= 0.4'} dependencies: @@ -1996,27 +2166,27 @@ packages: has-tostringtag: 1.0.0 dev: true - /is-shared-array-buffer/1.0.2: + /is-shared-array-buffer@1.0.2: resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} dependencies: call-bind: 1.0.2 dev: true - /is-string/1.0.7: + /is-string@1.0.7: resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} engines: {node: '>= 0.4'} dependencies: has-tostringtag: 1.0.0 dev: true - /is-symbol/1.0.4: + /is-symbol@1.0.4: resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} engines: {node: '>= 0.4'} dependencies: has-symbols: 1.0.3 dev: true - /is-typed-array/1.1.10: + /is-typed-array@1.1.10: resolution: {integrity: sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==} engines: {node: '>= 0.4'} dependencies: @@ -2027,33 +2197,33 @@ packages: has-tostringtag: 1.0.0 dev: true - /is-weakref/1.0.2: + /is-weakref@1.0.2: resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} dependencies: call-bind: 1.0.2 dev: true - /is-what/3.14.1: + /is-what@3.14.1: resolution: {integrity: sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==} dev: true - /is-what/4.1.8: + /is-what@4.1.8: resolution: {integrity: sha512-yq8gMao5upkPoGEU9LsB2P+K3Kt8Q3fQFCGyNCWOAnJAMzEXVV9drYb0TXr42TTliLLhKIBvulgAXgtLLnwzGA==} engines: {node: '>=12.13'} dev: true - /is-wsl/2.2.0: + /is-wsl@2.2.0: resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} engines: {node: '>=8'} dependencies: is-docker: 2.2.1 dev: true - /isexe/2.0.0: + /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true - /jake/10.8.7: + /jake@10.8.7: resolution: {integrity: sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==} engines: {node: '>=10'} hasBin: true @@ -2064,57 +2234,57 @@ packages: minimatch: 3.1.2 dev: true - /javascript-natural-sort/0.7.1: + /javascript-natural-sort@0.7.1: resolution: {integrity: sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==} dev: true - /js-tokens/4.0.0: + /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} dev: true - /js-yaml/4.1.0: + /js-yaml@4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true dependencies: argparse: 2.0.1 dev: true - /jsesc/0.5.0: + /jsesc@0.5.0: resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} hasBin: true dev: true - /jsesc/2.5.2: + /jsesc@2.5.2: resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} engines: {node: '>=4'} hasBin: true dev: true - /jsesc/3.0.2: + /jsesc@3.0.2: resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} engines: {node: '>=6'} hasBin: true dev: true - /json-parse-even-better-errors/2.3.1: + /json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} dev: true - /json-schema-traverse/0.4.1: + /json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} dev: true - /json-stable-stringify-without-jsonify/1.0.1: + /json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} dev: true - /json5/2.2.2: + /json5@2.2.2: resolution: {integrity: sha512-46Tk9JiOL2z7ytNQWFLpj99RZkVgeHf87yGQKsIkaPz1qSH9UczKH1rO7K3wgRselo0tYMUNfecYpm/p1vC7tQ==} engines: {node: '>=6'} hasBin: true dev: true - /jsx-ast-utils/3.3.4: + /jsx-ast-utils@3.3.4: resolution: {integrity: sha512-fX2TVdCViod6HwKEtSWGHs57oFhVfCMwieb9PuRDgjDPh5XeqJiHFFFJCHxU5cnTc3Bu/GRL+kPiFmw8XWOfKw==} engines: {node: '>=4.0'} dependencies: @@ -2124,16 +2294,16 @@ packages: object.values: 1.1.6 dev: true - /kebab-case/1.0.2: + /kebab-case@1.0.2: resolution: {integrity: sha512-7n6wXq4gNgBELfDCpzKc+mRrZFs7D+wgfF5WRFLNAr4DA/qtr9Js8uOAVAfHhuLMfAcQ0pRKqbpjx+TcJVdE1Q==} dev: true - /known-css-properties/0.24.0: + /known-css-properties@0.24.0: resolution: {integrity: sha512-RTSoaUAfLvpR357vWzAz/50Q/BmHfmE6ETSWfutT0AJiw10e6CmcdYRQJlLRd95B53D0Y2aD1jSxD3V3ySF+PA==} dev: true - /less/4.1.3: - resolution: {integrity: sha512-w16Xk/Ta9Hhyei0Gpz9m7VS8F28nieJaL/VyShID7cYvP6IL5oHeL6p4TXSDJqZE/lNv0oJ2pGVjJsRkfwm5FA==} + /less@4.2.0: + resolution: {integrity: sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==} engines: {node: '>=6'} hasBin: true dependencies: @@ -2146,13 +2316,11 @@ packages: image-size: 0.5.5 make-dir: 2.1.0 mime: 1.6.0 - needle: 3.2.0 + needle: 3.3.1 source-map: 0.6.1 - transitivePeerDependencies: - - supports-color dev: true - /levn/0.4.1: + /levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} dependencies: @@ -2160,46 +2328,46 @@ packages: type-check: 0.4.0 dev: true - /lines-and-columns/1.2.4: + /lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} dev: true - /locate-path/5.0.0: + /locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} dependencies: p-locate: 4.1.0 dev: true - /locate-path/6.0.0: + /locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} dependencies: p-locate: 5.0.0 dev: true - /lodash.merge/4.6.2: + /lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} dev: true - /lodash/4.17.21: + /lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} dev: true - /lru-cache/5.1.1: + /lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} dependencies: yallist: 3.1.1 dev: true - /lru-cache/6.0.0: + /lru-cache@6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} dependencies: yallist: 4.0.0 dev: true - /make-dir/2.1.0: + /make-dir@2.1.0: resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} engines: {node: '>=6'} requiresBuild: true @@ -2209,23 +2377,23 @@ packages: dev: true optional: true - /material-symbols/0.10.0: + /material-symbols@0.10.0: resolution: {integrity: sha512-/C4LI/s8o1lz1M4AUA0qVQ3zqYGe3eF30ftRrtT48OY3pGOmScJFZo0TX0CGhoRNF2D59PNrpoccAyBBmtXoig==} dev: false - /merge-anything/5.1.4: + /merge-anything@5.1.4: resolution: {integrity: sha512-7PWKwGOs5WWcpw+/OvbiFiAvEP6bv/QHiicigpqMGKIqPPAtGhBLR8LFJW+Zu6m9TXiR/a8+AiPlGG0ko1ruoQ==} engines: {node: '>=12.13'} dependencies: is-what: 4.1.8 dev: true - /merge2/1.4.1: + /merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} dev: true - /micromatch/4.0.5: + /micromatch@4.0.5: resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} engines: {node: '>=8.6'} dependencies: @@ -2233,7 +2401,7 @@ packages: picomatch: 2.3.1 dev: true - /mime/1.6.0: + /mime@1.6.0: resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} engines: {node: '>=4'} hasBin: true @@ -2241,72 +2409,69 @@ packages: dev: true optional: true - /min-indent/1.0.1: + /min-indent@1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} dev: true - /minimatch/3.1.2: + /minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} dependencies: brace-expansion: 1.1.11 dev: true - /minimatch/5.1.6: + /minimatch@5.1.6: resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} engines: {node: '>=10'} dependencies: brace-expansion: 2.0.1 dev: true - /minimist/1.2.8: + /minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} dev: true - /mkdirp/0.5.6: + /mkdirp@0.5.6: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true dependencies: minimist: 1.2.8 dev: true - /ms/2.1.2: + /ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} dev: true - /nanoid/3.3.4: - resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==} + /nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true dev: true - /natural-compare-lite/1.4.0: + /natural-compare-lite@1.4.0: resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} dev: true - /natural-compare/1.4.0: + /natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: true - /needle/3.2.0: - resolution: {integrity: sha512-oUvzXnyLiVyVGoianLijF9O/RecZUf7TkBfimjGrLM4eQhXyeJwM6GeAWccwfQ9aa4gMCZKqhAOuLaMIcQxajQ==} + /needle@3.3.1: + resolution: {integrity: sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==} engines: {node: '>= 4.4.x'} hasBin: true requiresBuild: true dependencies: - debug: 3.2.7 iconv-lite: 0.6.3 sax: 1.2.4 - transitivePeerDependencies: - - supports-color dev: true optional: true - /node-releases/2.0.8: + /node-releases@2.0.8: resolution: {integrity: sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A==} dev: true - /normalize-package-data/2.5.0: + /normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} dependencies: hosted-git-info: 2.8.9 @@ -2315,16 +2480,16 @@ packages: validate-npm-package-license: 3.0.4 dev: true - /object-inspect/1.12.3: + /object-inspect@1.12.3: resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} dev: true - /object-keys/1.1.1: + /object-keys@1.1.1: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} dev: true - /object.assign/4.1.4: + /object.assign@4.1.4: resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} engines: {node: '>= 0.4'} dependencies: @@ -2334,7 +2499,7 @@ packages: object-keys: 1.1.1 dev: true - /object.values/1.1.6: + /object.values@1.1.6: resolution: {integrity: sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==} engines: {node: '>= 0.4'} dependencies: @@ -2343,13 +2508,13 @@ packages: es-abstract: 1.21.3 dev: true - /once/1.4.0: + /once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} dependencies: wrappy: 1.0.2 dev: true - /open/7.4.2: + /open@7.4.2: resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==} engines: {node: '>=8'} dependencies: @@ -2357,7 +2522,7 @@ packages: is-wsl: 2.2.0 dev: true - /optionator/0.9.1: + /optionator@0.9.1: resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==} engines: {node: '>= 0.8.0'} dependencies: @@ -2369,47 +2534,47 @@ packages: word-wrap: 1.2.3 dev: true - /p-limit/2.3.0: + /p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} dependencies: p-try: 2.2.0 dev: true - /p-limit/3.1.0: + /p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} dependencies: yocto-queue: 0.1.0 dev: true - /p-locate/4.1.0: + /p-locate@4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} dependencies: p-limit: 2.3.0 dev: true - /p-locate/5.0.0: + /p-locate@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} dependencies: p-limit: 3.1.0 dev: true - /p-try/2.2.0: + /p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} dev: true - /parent-module/1.0.1: + /parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} dependencies: callsites: 3.1.0 dev: true - /parse-json/5.2.0: + /parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} dependencies: @@ -2419,90 +2584,92 @@ packages: lines-and-columns: 1.2.4 dev: true - /parse-node-version/1.0.1: + /parse-node-version@1.0.1: resolution: {integrity: sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==} engines: {node: '>= 0.10'} dev: true - /path-exists/4.0.0: + /path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} dev: true - /path-is-absolute/1.0.1: + /path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} dev: true - /path-key/3.1.1: + /path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} dev: true - /path-parse/1.0.7: + /path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} dev: true - /path-type/4.0.0: + /path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} dev: true - /picocolors/1.0.0: + /picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} dev: true - /picomatch/2.3.1: + /picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} dev: true - /pify/4.0.1: + /pify@4.0.1: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} engines: {node: '>=6'} + requiresBuild: true dev: true optional: true - /pluralize/8.0.0: + /pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} dev: true - /postcss/8.4.21: - resolution: {integrity: sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==} + /postcss@8.4.32: + resolution: {integrity: sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==} engines: {node: ^10 || ^12 || >=14} dependencies: - nanoid: 3.3.4 + nanoid: 3.3.7 picocolors: 1.0.0 source-map-js: 1.0.2 dev: true - /prelude-ls/1.2.1: + /prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} dev: true - /prettier/2.8.8: + /prettier@2.8.8: resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} engines: {node: '>=10.13.0'} hasBin: true dev: true - /prr/1.0.1: + /prr@1.0.1: resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==} + requiresBuild: true dev: true optional: true - /punycode/2.3.0: + /punycode@2.3.0: resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} engines: {node: '>=6'} dev: true - /queue-microtask/1.2.3: + /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: true - /read-pkg-up/7.0.1: + /read-pkg-up@7.0.1: resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} engines: {node: '>=8'} dependencies: @@ -2511,7 +2678,7 @@ packages: type-fest: 0.8.1 dev: true - /read-pkg/5.2.0: + /read-pkg@5.2.0: resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} engines: {node: '>=8'} dependencies: @@ -2521,12 +2688,16 @@ packages: type-fest: 0.6.0 dev: true - /regexp-tree/0.1.27: + /regenerator-runtime@0.14.0: + resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==} + dev: false + + /regexp-tree@0.1.27: resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} hasBin: true dev: true - /regexp.prototype.flags/1.5.0: + /regexp.prototype.flags@1.5.0: resolution: {integrity: sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==} engines: {node: '>= 0.4'} dependencies: @@ -2535,24 +2706,24 @@ packages: functions-have-names: 1.2.3 dev: true - /regjsparser/0.10.0: + /regjsparser@0.10.0: resolution: {integrity: sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA==} hasBin: true dependencies: jsesc: 0.5.0 dev: true - /require-directory/2.1.1: + /require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} dev: true - /resolve-from/4.0.0: + /resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} dev: true - /resolve/1.22.1: + /resolve@1.22.1: resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==} hasBin: true dependencies: @@ -2561,40 +2732,40 @@ packages: supports-preserve-symlinks-flag: 1.0.0 dev: true - /reusify/1.0.4: + /reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} dev: true - /rimraf/2.6.3: + /rimraf@2.6.3: resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==} hasBin: true dependencies: glob: 7.2.3 dev: true - /rimraf/3.0.2: + /rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} hasBin: true dependencies: glob: 7.2.3 dev: true - /rollup/3.13.0: - resolution: {integrity: sha512-HJwQtrXAc0AmyDohTJ/2c+Bx/sWPScJLlAUJ1kuD7rAkCro8Cr2SnVB2gVYBiSLxpgD2kZ24jbyXtG++GumrYQ==} + /rollup@3.28.0: + resolution: {integrity: sha512-d7zhvo1OUY2SXSM6pfNjgD5+d0Nz87CUp4mt8l/GgVP3oBsPwzNvSzyu1me6BSG9JIgWNTVcafIXBIyM8yQ3yw==} engines: {node: '>=14.18.0', npm: '>=8.0.0'} hasBin: true optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 dev: true - /run-parallel/1.2.0: + /run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} dependencies: queue-microtask: 1.2.3 dev: true - /safe-regex-test/1.0.0: + /safe-regex-test@1.0.0: resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} dependencies: call-bind: 1.0.2 @@ -2602,39 +2773,42 @@ packages: is-regex: 1.1.4 dev: true - /safe-regex/2.1.1: + /safe-regex@2.1.1: resolution: {integrity: sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==} dependencies: regexp-tree: 0.1.27 dev: true - /safer-buffer/2.1.2: + /safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + requiresBuild: true dev: true optional: true - /sax/1.2.4: + /sax@1.2.4: resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==} + requiresBuild: true dev: true optional: true - /semver/5.7.1: + /semver@5.7.1: resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==} hasBin: true + requiresBuild: true dev: true optional: true - /semver/5.7.2: + /semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true dev: true - /semver/6.3.0: + /semver@6.3.0: resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==} hasBin: true dev: true - /semver/7.5.1: + /semver@7.5.1: resolution: {integrity: sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==} engines: {node: '>=10'} hasBin: true @@ -2642,7 +2816,7 @@ packages: lru-cache: 6.0.0 dev: true - /semver/7.5.4: + /semver@7.5.4: resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} engines: {node: '>=10'} hasBin: true @@ -2650,23 +2824,23 @@ packages: lru-cache: 6.0.0 dev: true - /seroval/0.5.1: - resolution: {integrity: sha512-ZfhQVB59hmIauJG5Ydynupy8KHyr5imGNtdDhbZG68Ufh1Ynkv9KOYOAABf71oVbQxJ8VkWnMHAjEHE7fWkH5g==} + /seroval@0.15.1: + resolution: {integrity: sha512-OPVtf0qmeC7RW+ScVX+7aOS+xoIM7pWcZ0jOWg2aTZigCydgRB04adfteBRbecZnnrO1WuGQ+C3tLeBBzX2zSQ==} engines: {node: '>=10'} - /shebang-command/2.0.0: + /shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} dependencies: shebang-regex: 3.0.0 dev: true - /shebang-regex/3.0.0: + /shebang-regex@3.0.0: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} dev: true - /side-channel/1.0.4: + /side-channel@1.0.4: resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} dependencies: call-bind: 1.0.2 @@ -2674,18 +2848,18 @@ packages: object-inspect: 1.12.3 dev: true - /slash/3.0.0: + /slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} dev: true - /solid-js/1.7.8: - resolution: {integrity: sha512-XHBWk1FvFd0JMKljko7FfhefJMTSgYEuVKcQ2a8hzRXfiuSJAGsrPPafqEo+f6l+e8Oe3cROSpIL6kbzjC1fjQ==} + /solid-js@1.8.7: + resolution: {integrity: sha512-9dzrSVieh2zj3SnJ02II6xZkonR6c+j/91b7XZUNcC6xSaldlqjjGh98F1fk5cRJ8ZTkzqF5fPIWDxEOs6QZXA==} dependencies: csstype: 3.1.1 - seroval: 0.5.1 + seroval: 0.15.1 - /solid-refresh/0.4.1_solid-js@1.7.8: + /solid-refresh@0.4.1(solid-js@1.8.7): resolution: {integrity: sha512-v3tD/OXQcUyXLrWjPW1dXZyeWwP7/+GQNs8YTL09GBq+5FguA6IejJWUvJDrLIA4M0ho9/5zK2e9n+uy+4488g==} peerDependencies: solid-js: ^1.3 @@ -2693,10 +2867,10 @@ packages: '@babel/generator': 7.20.7 '@babel/helper-module-imports': 7.18.6 '@babel/types': 7.20.7 - solid-js: 1.7.8 + solid-js: 1.8.7 dev: true - /source-map-explorer/2.5.3: + /source-map-explorer@2.5.3: resolution: {integrity: sha512-qfUGs7UHsOBE5p/lGfQdaAj/5U/GWYBw2imEpD6UQNkqElYonkow8t+HBL1qqIl3CuGZx7n8/CQo4x1HwSHhsg==} engines: {node: '>=12'} hasBin: true @@ -2715,51 +2889,51 @@ packages: yargs: 16.2.0 dev: true - /source-map-js/1.0.2: + /source-map-js@1.0.2: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} dev: true - /source-map/0.5.7: + /source-map@0.5.7: resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} engines: {node: '>=0.10.0'} dev: true - /source-map/0.6.1: + /source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} requiresBuild: true dev: true optional: true - /source-map/0.7.4: + /source-map@0.7.4: resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} engines: {node: '>= 8'} dev: true - /spdx-correct/3.2.0: + /spdx-correct@3.2.0: resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} dependencies: spdx-expression-parse: 3.0.1 spdx-license-ids: 3.0.13 dev: true - /spdx-exceptions/2.3.0: + /spdx-exceptions@2.3.0: resolution: {integrity: sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==} dev: true - /spdx-expression-parse/3.0.1: + /spdx-expression-parse@3.0.1: resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} dependencies: spdx-exceptions: 2.3.0 spdx-license-ids: 3.0.13 dev: true - /spdx-license-ids/3.0.13: + /spdx-license-ids@3.0.13: resolution: {integrity: sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==} dev: true - /string-width/4.2.3: + /string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} dependencies: @@ -2768,7 +2942,7 @@ packages: strip-ansi: 6.0.1 dev: true - /string.prototype.trim/1.2.7: + /string.prototype.trim@1.2.7: resolution: {integrity: sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==} engines: {node: '>= 0.4'} dependencies: @@ -2777,7 +2951,7 @@ packages: es-abstract: 1.21.3 dev: true - /string.prototype.trimend/1.0.6: + /string.prototype.trimend@1.0.6: resolution: {integrity: sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==} dependencies: call-bind: 1.0.2 @@ -2785,7 +2959,7 @@ packages: es-abstract: 1.21.3 dev: true - /string.prototype.trimstart/1.0.6: + /string.prototype.trimstart@1.0.6: resolution: {integrity: sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==} dependencies: call-bind: 1.0.2 @@ -2793,51 +2967,51 @@ packages: es-abstract: 1.21.3 dev: true - /strip-ansi/6.0.1: + /strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} dependencies: ansi-regex: 5.0.1 dev: true - /strip-indent/3.0.0: + /strip-indent@3.0.0: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} dependencies: min-indent: 1.0.1 dev: true - /strip-json-comments/3.1.1: + /strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} dev: true - /style-to-object/0.3.0: + /style-to-object@0.3.0: resolution: {integrity: sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==} dependencies: inline-style-parser: 0.1.1 dev: true - /supports-color/5.5.0: + /supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} dependencies: has-flag: 3.0.0 dev: true - /supports-color/7.2.0: + /supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} dependencies: has-flag: 4.0.0 dev: true - /supports-preserve-symlinks-flag/1.0.0: + /supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} dev: true - /temp/0.9.4: + /temp@0.9.4: resolution: {integrity: sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==} engines: {node: '>=6.0.0'} dependencies: @@ -2845,31 +3019,31 @@ packages: rimraf: 2.6.3 dev: true - /text-table/0.2.0: + /text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} dev: true - /to-fast-properties/2.0.0: + /to-fast-properties@2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} dev: true - /to-regex-range/5.0.1: + /to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} dependencies: is-number: 7.0.0 dev: true - /tslib/1.14.1: + /tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} dev: true - /tslib/2.6.0: + /tslib@2.6.0: resolution: {integrity: sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==} dev: true - /tsutils/3.21.0_typescript@5.1.6: + /tsutils@3.21.0(typescript@5.1.6): resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'} peerDependencies: @@ -2879,29 +3053,29 @@ packages: typescript: 5.1.6 dev: true - /type-check/0.4.0: + /type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} dependencies: prelude-ls: 1.2.1 dev: true - /type-fest/0.20.2: + /type-fest@0.20.2: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} dev: true - /type-fest/0.6.0: + /type-fest@0.6.0: resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} engines: {node: '>=8'} dev: true - /type-fest/0.8.1: + /type-fest@0.8.1: resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} engines: {node: '>=8'} dev: true - /typed-array-byte-offset/1.0.0: + /typed-array-byte-offset@1.0.0: resolution: {integrity: sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==} engines: {node: '>= 0.4'} dependencies: @@ -2912,7 +3086,7 @@ packages: is-typed-array: 1.1.10 dev: true - /typed-array-length/1.0.4: + /typed-array-length@1.0.4: resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} dependencies: call-bind: 1.0.2 @@ -2920,13 +3094,13 @@ packages: is-typed-array: 1.1.10 dev: true - /typescript/5.1.6: + /typescript@5.1.6: resolution: {integrity: sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==} engines: {node: '>=14.17'} hasBin: true dev: true - /unbox-primitive/1.0.2: + /unbox-primitive@1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} dependencies: call-bind: 1.0.2 @@ -2935,7 +3109,7 @@ packages: which-boxed-primitive: 1.0.2 dev: true - /update-browserslist-db/1.0.10_browserslist@4.21.4: + /update-browserslist-db@1.0.10(browserslist@4.21.4): resolution: {integrity: sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==} hasBin: true peerDependencies: @@ -2946,44 +3120,45 @@ packages: picocolors: 1.0.0 dev: true - /uri-js/4.4.1: + /uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} dependencies: punycode: 2.3.0 dev: true - /validate-npm-package-license/3.0.4: + /validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} dependencies: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 dev: true - /vite-plugin-solid/2.5.0_solid-js@1.7.8+vite@4.1.1: + /vite-plugin-solid@2.5.0(solid-js@1.8.7)(vite@4.5.1): resolution: {integrity: sha512-VneGd3RyFJvwaiffsqgymeMaofn0IzQLPwDzafTV2f1agoWeeJlk5VrI5WqT9BTtLe69vNNbCJWqLhHr9fOdDw==} peerDependencies: solid-js: ^1.3.17 || ^1.4.0 || ^1.5.0 || ^1.6.0 vite: ^3.0.0 || ^4.0.0 dependencies: '@babel/core': 7.20.7 - '@babel/preset-typescript': 7.18.6_@babel+core@7.20.7 - babel-preset-solid: 1.6.6_@babel+core@7.20.7 + '@babel/preset-typescript': 7.18.6(@babel/core@7.20.7) + babel-preset-solid: 1.6.6(@babel/core@7.20.7) merge-anything: 5.1.4 - solid-js: 1.7.8 - solid-refresh: 0.4.1_solid-js@1.7.8 - vite: 4.1.1_less@4.1.3 - vitefu: 0.2.4_vite@4.1.1 + solid-js: 1.8.7 + solid-refresh: 0.4.1(solid-js@1.8.7) + vite: 4.5.1(less@4.2.0) + vitefu: 0.2.4(vite@4.5.1) transitivePeerDependencies: - supports-color dev: true - /vite/4.1.1_less@4.1.3: - resolution: {integrity: sha512-LM9WWea8vsxhr782r9ntg+bhSFS06FJgCvvB0+8hf8UWtvaiDagKYWXndjfX6kGl74keHJUcpzrQliDXZlF5yg==} + /vite@4.5.1(less@4.2.0): + resolution: {integrity: sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA==} engines: {node: ^14.18.0 || >=16.0.0} hasBin: true peerDependencies: '@types/node': '>= 14' less: '*' + lightningcss: ^1.21.0 sass: '*' stylus: '*' sugarss: '*' @@ -2993,6 +3168,8 @@ packages: optional: true less: optional: true + lightningcss: + optional: true sass: optional: true stylus: @@ -3002,16 +3179,15 @@ packages: terser: optional: true dependencies: - esbuild: 0.16.17 - less: 4.1.3 - postcss: 8.4.21 - resolve: 1.22.1 - rollup: 3.13.0 + esbuild: 0.18.20 + less: 4.2.0 + postcss: 8.4.32 + rollup: 3.28.0 optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 dev: true - /vitefu/0.2.4_vite@4.1.1: + /vitefu@0.2.4(vite@4.5.1): resolution: {integrity: sha512-fanAXjSaf9xXtOOeno8wZXIhgia+CZury481LsDaV++lSvcU2R9Ch2bPh3PYFyoHW+w9LqAeYRISVQjUIew14g==} peerDependencies: vite: ^3.0.0 || ^4.0.0 @@ -3019,10 +3195,10 @@ packages: vite: optional: true dependencies: - vite: 4.1.1_less@4.1.3 + vite: 4.5.1(less@4.2.0) dev: true - /which-boxed-primitive/1.0.2: + /which-boxed-primitive@1.0.2: resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} dependencies: is-bigint: 1.0.4 @@ -3032,7 +3208,7 @@ packages: is-symbol: 1.0.4 dev: true - /which-typed-array/1.1.10: + /which-typed-array@1.1.10: resolution: {integrity: sha512-uxoA5vLUfRPdjCuJ1h5LlYdmTLbYfums398v3WLkM+i/Wltl2/XyZpQWKbN++ck5L64SR/grOHqtXCUKmlZPNA==} engines: {node: '>= 0.4'} dependencies: @@ -3044,7 +3220,7 @@ packages: is-typed-array: 1.1.10 dev: true - /which/2.0.2: + /which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} hasBin: true @@ -3052,12 +3228,12 @@ packages: isexe: 2.0.0 dev: true - /word-wrap/1.2.3: + /word-wrap@1.2.3: resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==} engines: {node: '>=0.10.0'} dev: true - /wrap-ansi/7.0.0: + /wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} dependencies: @@ -3066,29 +3242,29 @@ packages: strip-ansi: 6.0.1 dev: true - /wrappy/1.0.2: + /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} dev: true - /y18n/5.0.8: + /y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} dev: true - /yallist/3.1.1: + /yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} dev: true - /yallist/4.0.0: + /yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} dev: true - /yargs-parser/20.2.9: + /yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} dev: true - /yargs/16.2.0: + /yargs@16.2.0: resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} engines: {node: '>=10'} dependencies: @@ -3101,7 +3277,11 @@ packages: yargs-parser: 20.2.9 dev: true - /yocto-queue/0.1.0: + /yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} dev: true + + /zod@3.21.4: + resolution: {integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==} + dev: false diff --git a/y-web/public/assets/fonts/OFL.txt b/y-web/public/assets/fonts/Inter-OFL.txt similarity index 100% rename from y-web/public/assets/fonts/OFL.txt rename to y-web/public/assets/fonts/Inter-OFL.txt diff --git a/y-web/public/assets/fonts/SourceCodePro-Italic-VariableFont_wght.ttf b/y-web/public/assets/fonts/SourceCodePro-Italic-VariableFont_wght.ttf new file mode 100644 index 0000000..9e2638f Binary files /dev/null and b/y-web/public/assets/fonts/SourceCodePro-Italic-VariableFont_wght.ttf differ diff --git a/y-web/public/assets/fonts/SourceCodePro-OFL.txt b/y-web/public/assets/fonts/SourceCodePro-OFL.txt new file mode 100644 index 0000000..366206f --- /dev/null +++ b/y-web/public/assets/fonts/SourceCodePro-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/y-web/public/assets/fonts/SourceCodePro-VariableFont_wght.ttf b/y-web/public/assets/fonts/SourceCodePro-VariableFont_wght.ttf new file mode 100644 index 0000000..033fa91 Binary files /dev/null and b/y-web/public/assets/fonts/SourceCodePro-VariableFont_wght.ttf differ diff --git a/y-web/src/App.tsx b/y-web/src/App.tsx deleted file mode 100644 index daeadc5..0000000 --- a/y-web/src/App.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import type { Component } from "solid-js" - -import { Route, Router, Routes } from "@solidjs/router" - -import { AppLayout } from "@/app/app-layout" - -const App: Component = () => { - return ( - - - - - - - ) -} - -export default App diff --git a/y-web/src/app.tsx b/y-web/src/app.tsx new file mode 100644 index 0000000..5dda95f --- /dev/null +++ b/y-web/src/app.tsx @@ -0,0 +1,38 @@ +import type { Component } from "solid-js" + +import { Route, Router, Routes } from "@solidjs/router" +import { QueryClient, QueryClientProvider } from "@tanstack/solid-query" + +import { AppLayout } from "@/app/app-layout" +import LoginPage from "@/modules/core/pages/login/login" + +import { AppToasts } from "./app/layout/app-toasts" + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + keepPreviousData: true, + + retry: false, + refetchInterval: false, + refetchOnWindowFocus: false, + }, + }, +}) + +const App: Component = () => { + return ( + +
+ + + + + + + + + ) +} + +export default App diff --git a/y-web/src/app/app-layout.less b/y-web/src/app/app-layout.less index b8204af..859bcad 100644 --- a/y-web/src/app/app-layout.less +++ b/y-web/src/app/app-layout.less @@ -1,6 +1,94 @@ +@keyframes modal-show { + 0% { + opacity: 0; + transform: translateY(10%) scale(0.97); + } + 100% { + opacity: 1; + transform: translateY(0) scale(1); + } +} + #app-main { display: flex; align-items: flex-start; height: calc(100vh - 50px); } + +#app-modals-container { + position: fixed; + z-index: 100; + + width: 100vw; + height: 100vh; + + pointer-events: none; +} + +#app-modals-container .ui-modal-container { + position: absolute; + display: flex; + align-items: center; + justify-content: center; + + top: 0; + left: 0; + + width: 100%; + height: 100%; + + background-color: rgba(0, 0, 0, 0.66); + + pointer-events: none; + + // Hidden state + opacity: 0; + + transition: 0.15s ease-in-out; + + > .ui-modal { + max-width: 800px; + max-height: 80vh; + + border-radius: 15px; + background-color: white; + + box-shadow: inset 0 -2px 0 0 rgba(0, 0, 0, 0.1), + inset 0 -4px 0 0 rgba(0, 0, 0, 0.05), 0 5px 10px 0 rgba(0, 0, 0, 0.15); + + // Hidden state + opacity: 0; + pointer-events: none; + transform: translateY(10%) scale(0.97); + + transition: 0.15s ease-in-out; + + animation: modal-show 0.25s ease-in-out; + animation-play-state: running; + + > .modal-header { + padding: 1.5em 2em; + + border-top-right-radius: 15px; + border-top-left-radius: 15px; + + background-color: var(--color-grey-15); + } + + > .modal-content { + padding: 2em; + } + } + + &.open { + opacity: 1; + pointer-events: all; + } + + &.open > .ui-modal { + opacity: 1; + pointer-events: auto; + transform: translateY(0) scale(1); + } +} diff --git a/y-web/src/app/app-layout.tsx b/y-web/src/app/app-layout.tsx index 5d1b766..9218d2c 100644 --- a/y-web/src/app/app-layout.tsx +++ b/y-web/src/app/app-layout.tsx @@ -1,19 +1,39 @@ -import { Component } from "solid-js" +import { Component, Show, createEffect, lazy } from "solid-js" + +import { Route, Routes, useNavigate } from "@solidjs/router" -import { AppAside } from "@/app/layout/app-aside" -import { AppContent } from "@/app/layout/app-content" import { AppMenubar } from "@/app/layout/app-menubar" +import { useAuth } from "@/modules/core/auth/auth.service" import "./app-layout.less" +import { routes } from "./routes" + +const AdminLayout = lazy( + async () => import("@/modules/admin/layout/admin-layout") +) export const AppLayout: Component = () => { + const navigate = useNavigate() + + const $auth = useAuth() + + createEffect(() => { + if ($auth.isError && !($auth.isFetching || $auth.isRefetching)) { + navigate(`${routes["/login"]}?return=${window.location.pathname}`) + } + }) + return ( -
- -
- - + +
+ +
+ + + + +
-
+ ) } diff --git a/y-web/src/app/app-router.tsx b/y-web/src/app/app-router.tsx deleted file mode 100644 index 166337d..0000000 --- a/y-web/src/app/app-router.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { Component } from "solid-js" - -import { Route, Routes } from "@solidjs/router" - -export const AppRouter: Component = () => { - return ( - - - - ) -} diff --git a/y-web/src/app/components/button/button.less b/y-web/src/app/components/common/button/button.less similarity index 70% rename from y-web/src/app/components/button/button.less rename to y-web/src/app/components/common/button/button.less index a0c41af..544612d 100644 --- a/y-web/src/app/components/button/button.less +++ b/y-web/src/app/components/common/button/button.less @@ -1,89 +1,98 @@ -.ui-button { - &.disabled > button, - &.disabled > button > .content-wrapper > .ui-icon { - pointer-events: none; - color: var(--color-text-grey-1); - } - - /* Normal state */ - &.primary > button { - box-shadow: inset 0 0 0 1px var(--color-border-15); - background-color: white; - } - - &.secondary > button { - background-color: var(--color-grey-1); - } - - &.text > button { - background-color: transparent; - } - - /* Hovered state */ - &.primary > button:hover { - box-shadow: inset 0 0 0 1px var(--color-border-2), - 0 2px 4px var(--color-border-1); - } - - &.secondary > button:hover { - background-color: var(--color-grey-1); - box-shadow: inset 0 0 0 1px var(--color-border-15), - 0 2px 4px var(--color-border-1); - } - - &.text > button:hover { - background-color: var(--color-grey-1); - } - - /* Active state */ - &.primary > button:active { - transition: 0s; - - background-color: var(--color-grey-1); - box-shadow: inset 0 0 0 1px var(--color-border-2); - } - - &.secondary > button:active { - transition: 0s; - - background-color: var(--color-grey-15); - box-shadow: none; - } - - &.text > button:active { - transition: 0s; - - background-color: var(--color-grey-15); - } - - > button { - padding: 0.75em 1.4em; - - color: var(--color-text); - font-family: "Inter", sans-serif; - font-weight: 450; - font-size: var(--text-body); - - cursor: pointer; - user-select: none; - - border: none; - border-radius: 5px; - - transition: 0.15s; - - > .content-wrapper { - display: flex; - align-items: center; - justify-content: center; - - > .ui-icon { - color: hsl(0, 0%, 20%); - - &:first-child { - margin-right: 0.75em; - } - } - } - } -} +.ui-button { + /* Normal state */ + &.primary > button { + box-shadow: inset 0 0 0 1px var(--color-border-25); + background-color: white; + } + + &.secondary > button { + background-color: var(--color-grey-1); + box-shadow: inset 0 0 0 1px var(--color-border-15); + } + + &.text > button { + background-color: transparent; + } + + /* Hovered state */ + &.primary > button:hover { + box-shadow: inset 0 0 0 1px var(--color-border-3), + 0 2px 6px var(--color-border-1); + } + + &.secondary > button:hover { + background-color: var(--color-grey-15); + box-shadow: inset 0 0 0 1px var(--color-border-2), + 0 2px 4px var(--color-border-1); + } + + &.text > button:hover { + background-color: var(--color-grey-1); + } + + /* Active state */ + &.primary > button:active { + transition: 0s; + + background-color: var(--color-grey-2); + box-shadow: none; + } + + &.secondary > button:active { + transition: 0s; + + background-color: var(--color-grey-15); + box-shadow: none; + } + + &.text > button:active { + transition: 0s; + + background-color: var(--color-grey-15); + } + + &.disabled > button { + pointer-events: none; + + color: var(--color-text-grey-1); + + background-color: var(--color-grey-15); + box-shadow: inset 0 0 0 1px var(--color-grey-15); + } + + > button { + padding: 0.75em 1.4em; + + color: var(--color-text); + font-family: "Inter", sans-serif; + font-weight: 500; + font-size: var(--text-body); + + cursor: pointer; + user-select: none; + white-space: nowrap; + + border: none; + border-radius: 5px; + + transition: 0.15s; + + > .content-wrapper { + display: flex; + align-items: center; + justify-content: center; + + > .ui-icon { + color: hsl(0, 0%, 20%); + + &:first-child { + margin-right: 0.75em; + } + + &:only-child { + margin-right: 0; + } + } + } + } +} diff --git a/y-web/src/app/components/button/button.tsx b/y-web/src/app/components/common/button/button.tsx similarity index 75% rename from y-web/src/app/components/button/button.tsx rename to y-web/src/app/components/common/button/button.tsx index 3e0d6ad..c75a6fd 100644 --- a/y-web/src/app/components/button/button.tsx +++ b/y-web/src/app/components/common/button/button.tsx @@ -1,7 +1,6 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ -import { Show } from "solid-js" +import { JSX, Show } from "solid-js" -import { Icon } from "@/app/components/icon/icon" +import { Icon } from "@/app/components/common/icon/icon" import { ComponentWithChildren } from "@/module" import "./button.less" @@ -17,6 +16,8 @@ export type ButtonProps = { onClick?: (event: MouseEvent) => void buttonType?: "button" | "submit" | "reset" + + style?: JSX.CSSProperties } export const Button: ComponentWithChildren = (props) => { @@ -29,7 +30,7 @@ export const Button: ComponentWithChildren = (props) => { }} >
diff --git a/y-web/src/app/components/common/card/card.less b/y-web/src/app/components/common/card/card.less new file mode 100644 index 0000000..dc1272e --- /dev/null +++ b/y-web/src/app/components/common/card/card.less @@ -0,0 +1,9 @@ +.ui-card { + border-radius: 6px; + + background-color: hsla(234, 15%, 75%, 0.033); + box-shadow: 0 0 0 1px hsla(234, 15%, 75%, 0.5), + 0 10px 15px 0 rgba(0, 0, 0, 0.033); + + padding: 1.25em; +} diff --git a/y-web/src/app/components/common/card/card.tsx b/y-web/src/app/components/common/card/card.tsx new file mode 100644 index 0000000..452f357 --- /dev/null +++ b/y-web/src/app/components/common/card/card.tsx @@ -0,0 +1,17 @@ +import { JSX } from "solid-js" + +import { ComponentWithChildren } from "@/module" + +import "./card.less" + +export type CardProps = { + style?: JSX.CSSProperties +} + +export const Card: ComponentWithChildren = (props) => { + return ( +
+
{props.children}
+
+ ) +} diff --git a/y-web/src/app/components/common/checkbox/checkbox.less b/y-web/src/app/components/common/checkbox/checkbox.less new file mode 100644 index 0000000..fc5308f --- /dev/null +++ b/y-web/src/app/components/common/checkbox/checkbox.less @@ -0,0 +1,105 @@ +.ui-checkbox-container { + display: flex; + align-items: center; + width: fit-content; + + border-radius: 5px; + + transition: 0.15s ease-in-out; + + &.checked { + background-color: var(--color-primary-t-15); + } + + &.disabled { + pointer-events: none; + background-color: var(--color-border-1); + + > .checked-label { + color: var(--color-text); + } + + > .ui-checkbox { + background-color: var(--color-border-1); + + &.checked { + background-color: var(--color-border-2); + border: 1px solid var(--color-border-2); + + .ui-icon { + color: var(--color-text); + } + } + } + } + + .checked-label { + pointer-events: none; + + font-weight: 500; + font-size: var(--text-sm); + color: var(--color-primary-d-1); + + // Hidden state + opacity: 0; + width: 0; + margin: 0; + } + + &.checked > .checked-label { + opacity: 1; + width: fit-content; + margin: 0 0.66em; + } + + > .ui-checkbox { + display: flex; + align-items: center; + justify-content: center; + + background-color: white; + border: 1px solid var(--color-border-2); + border-radius: 5px; + + cursor: pointer; + + transition: 0.1s ease-in-out; + + &.checked { + background-color: var(--color-primary); + border: 1px solid var(--color-primary); + + &:hover { + background-color: var(--color-primary-d-1); + border: 1px solid var(--color-primary-d-1); + } + } + + &.m { + width: 18px; + height: 18px; + } + + &.l { + width: 24px; + height: 24px; + } + + > .ui-icon { + color: white; + + // Hidden state + opacity: 0; + transform: translateY(50%) scale(0.5); + + transition: 0.15s; + } + + &.checked > .ui-icon { + opacity: 1; + transform: translateY(0) scale(1); + + font-variation-settings: "FILL" 1, "wght" 400, "GRAD" 25 !important; + } + } +} diff --git a/y-web/src/app/components/common/checkbox/checkbox.tsx b/y-web/src/app/components/common/checkbox/checkbox.tsx new file mode 100644 index 0000000..9c107a6 --- /dev/null +++ b/y-web/src/app/components/common/checkbox/checkbox.tsx @@ -0,0 +1,100 @@ +import { Component, Show, createEffect, createSignal, onMount } from "solid-js" + +import { Icon } from "../icon/icon" +import "./checkbox.less" + +export type CheckboxProps = { + onChange?: (checked: boolean) => void + + /** Use if you want this checkbox to be uncontrolled. */ + ref?: HTMLInputElement | ((inputRef: HTMLInputElement) => unknown) + + /** Use if you want this checkbox to be controlled. */ + value?: boolean + + checkedLabel?: string + size?: "m" | "l" + + disabled?: boolean +} + +export const Checkbox: Component = (props) => { + // eslint-disable-next-line solid/reactivity, no-undefined + const isControlled = props.value !== undefined + + let ref: HTMLInputElement + + const [checked, setChecked] = createSignal( + // eslint-disable-next-line solid/reactivity, no-undefined + props.value === undefined ? false : props.value + ) + + onMount(() => { + if (!isControlled) { + setChecked(ref.checked) + ref.addEventListener("change", () => { + setChecked(ref.checked) + }) + } + }) + + createEffect(() => { + if (!isControlled) { + props.onChange?.(checked()) + } + }) + + const toggle = () => { + if (props.disabled) return + + if (isControlled) { + setChecked((value) => !value) + + ref.checked = checked() + props.onChange?.(checked()) + } else { + ref.checked = !ref.checked + } + + ref.dispatchEvent(new Event("change", { bubbles: true })) + } + + return ( +
+ { + ref = inputRef + if (typeof props.ref === "function") { + props.ref(inputRef) + } else { + // eslint-disable-next-line solid/reactivity + props.ref = inputRef + } + }} + /> + + +
{props.checkedLabel}
+
+
+ ) +} diff --git a/y-web/src/app/components/common/expand-button/expand-button-entry.tsx b/y-web/src/app/components/common/expand-button/expand-button-entry.tsx new file mode 100644 index 0000000..6f5ff80 --- /dev/null +++ b/y-web/src/app/components/common/expand-button/expand-button-entry.tsx @@ -0,0 +1,37 @@ +import { Show } from "solid-js" + +import { Icon } from "@/app/components/common/icon/icon" +import { ComponentWithChildren } from "@/module" + +export const ExpandButtonEntries: ComponentWithChildren = (props) => { + return
{props.children}
+} + +export type ExpandButtonEntryProps = { + icon?: string + + variant?: "default" | "danger" + + onClick?: () => void +} + +export const ExpandButtonEntry: ComponentWithChildren< + ExpandButtonEntryProps +> = (props) => { + return ( + + ) +} diff --git a/y-web/src/app/components/common/expand-button/expand-button.less b/y-web/src/app/components/common/expand-button/expand-button.less new file mode 100644 index 0000000..11cb371 --- /dev/null +++ b/y-web/src/app/components/common/expand-button/expand-button.less @@ -0,0 +1,188 @@ +.ui-expand-button { + position: relative; + + > .button { + position: relative; + + display: flex; + align-items: center; + + padding: 0.66em 1.33em; + + cursor: pointer; + + color: var(--color-primary); + font-family: "Inter", sans-serif; + font-weight: 500; + + border: 1px solid transparent; + border-bottom: 0; + + border-radius: 5px; + + transition: 0.15s; + + &.filled { + background-color: var(--color-primary-t-025); + box-shadow: inset 0 0 0 1px var(--color-primary-t-2); + } + + &.text { + background-color: transparent; + } + + &.no-label { + padding: 0.66em; + } + + > .ui-icon { + margin-right: 0.5em; + } + + > .label { + white-space: nowrap; + } + + &.no-label > .ui-icon { + margin-right: 0; + } + + &:hover { + background-color: var(--color-primary-t-1); + } + + &.filled:hover { + background-color: var(--color-primary-t-1); + } + } + + &.expanded > .button { + z-index: 11; + + background-color: white; + border-radius: 6px; + border: 1px solid var(--color-border-15); + + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + border-bottom: 0; + + box-shadow: none; + } + + &.expanded > .button:hover { + background-color: transparent; + } + + > .expand-panel { + position: absolute; + z-index: 10; + + top: 1; + + box-sizing: border-box; + + min-width: 150%; + + padding: 0.33em; + + margin-top: -1px; + + border: 1px solid hsla(234, 15%, 75%, 0.5); + box-shadow: 0 2px 15px 0 hsla(234, 15%, 75%, 0.15); + background-color: white; + + border-radius: 6px; + + // Hidden state + opacity: 0; + pointer-events: none; + + transition: 0.15s; + + &.left { + right: 0; + border-top-right-radius: 0; + } + + &.right { + left: 0; + border-top-left-radius: 0; + } + + .ui-expand-button-entries { + display: flex; + flex-direction: column; + gap: 0.33em; + + > hr { + width: 90%; + height: 1px; + + margin: 0 auto; + + background-color: var(--color-border-1); + } + + > .ui-expand-button-entry { + display: flex; + align-items: center; + + padding: 0.33em 0.66em 0.33em 0.33em; + + cursor: pointer; + + border: 0; + + background-color: white; + border-radius: 4px; + + transition: 0.15s; + + > .ui-icon { + padding: 0.25em; + + border-radius: 4px; + + margin-right: 0.66em; + + transition: 0.15s; + } + + > .label { + font-family: "Inter", sans-serif; + font-weight: 500; + + white-space: nowrap; + } + + &:focus, + &:hover { + color: var(--color-primary); + background-color: var(--color-primary-t-1); + + > .ui-icon { + background-color: var(--color-primary-t-1); + + font-variation-settings: "FILL" 1, "wght" 400, "GRAD" 25, "opsz" 16 !important; + } + } + + &.danger:focus, + &.danger:hover { + color: var(--color-error); + background-color: var(--color-error-o-1); + + > .ui-icon { + background-color: var(--color-error-o-2); + } + } + } + } + } + + &.expanded > .expand-panel { + opacity: 1; + pointer-events: all; + } +} diff --git a/y-web/src/app/components/common/expand-button/expand-button.tsx b/y-web/src/app/components/common/expand-button/expand-button.tsx new file mode 100644 index 0000000..476b41a --- /dev/null +++ b/y-web/src/app/components/common/expand-button/expand-button.tsx @@ -0,0 +1,57 @@ +import { JSXElement, Show, createSignal } from "solid-js" + +import { Icon } from "@/app/components/common/icon/icon" +import { ComponentWithChildren } from "@/module" + +import "./expand-button.less" + +export type ExpandButtonProps = { + label?: JSXElement + icon?: string + variant?: "filled" | "text" + position?: "left" | "right" +} + +export const ExpandButton: ComponentWithChildren = ( + props +) => { + const [expanded, setExpanded] = createSignal(false) + + const toggleExpanded = () => setExpanded((value) => !value) + + return ( +
+ +
+ {props.children} +
+
+ ) +} diff --git a/y-web/src/app/components/icon/icon.tsx b/y-web/src/app/components/common/icon/icon.tsx similarity index 100% rename from y-web/src/app/components/icon/icon.tsx rename to y-web/src/app/components/common/icon/icon.tsx diff --git a/y-web/src/app/components/input-error/input-error.less b/y-web/src/app/components/common/input-error/input-error.less similarity index 100% rename from y-web/src/app/components/input-error/input-error.less rename to y-web/src/app/components/common/input-error/input-error.less diff --git a/y-web/src/app/components/input-error/input-error.tsx b/y-web/src/app/components/common/input-error/input-error.tsx similarity index 83% rename from y-web/src/app/components/input-error/input-error.tsx rename to y-web/src/app/components/common/input-error/input-error.tsx index d60fbdc..f4b2a71 100644 --- a/y-web/src/app/components/input-error/input-error.tsx +++ b/y-web/src/app/components/common/input-error/input-error.tsx @@ -1,11 +1,11 @@ import { Component } from "solid-js" -import { Icon } from "@/app/components/icon/icon" +import { Icon } from "@/app/components/common/icon/icon" import "./input-error.less" export type InputErrorProps = { - error: string + error?: string } export const InputError: Component = (props) => { diff --git a/y-web/src/app/components/input-field/input-field.less b/y-web/src/app/components/common/input-field/input-field.less similarity index 97% rename from y-web/src/app/components/input-field/input-field.less rename to y-web/src/app/components/common/input-field/input-field.less index 3ca978a..247f6d2 100644 --- a/y-web/src/app/components/input-field/input-field.less +++ b/y-web/src/app/components/common/input-field/input-field.less @@ -47,7 +47,7 @@ } &.monospace > .container > input { - font-family: "JetBrains Mono", monospace; + font-family: "SourceCodePro", monospace; font-weight: 500; } @@ -71,6 +71,7 @@ position: relative; box-shadow: 0 0 0 1px var(--color-border-2); + background-color: white; border-radius: 5px; diff --git a/y-web/src/app/components/input-field/input-field.tsx b/y-web/src/app/components/common/input-field/input-field.tsx similarity index 91% rename from y-web/src/app/components/input-field/input-field.tsx rename to y-web/src/app/components/common/input-field/input-field.tsx index 78c1e51..0434512 100644 --- a/y-web/src/app/components/input-field/input-field.tsx +++ b/y-web/src/app/components/common/input-field/input-field.tsx @@ -1,16 +1,17 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ import { Component, JSX, Show, createSignal, onMount } from "solid-js" import { InputError, InputErrorProps, -} from "@/app/components/input-error/input-error" +} from "@/app/components/common/input-error/input-error" import "./input-field.less" export type InputFieldProps = Partial & { ref?: HTMLInputElement | ((inputRef: HTMLInputElement) => unknown) type?: string + name?: string + maxLength?: number disabled?: boolean @@ -71,16 +72,18 @@ export const InputField: Component = (props) => { props.ref = inputRef } }} + name={props.name} type={props.type ?? "text"} disabled={props.disabled} placeholder={props.placeholder} onFocus={[setIsFocused, true]} onBlur={[setIsFocused, false]} + maxLength={props.maxLength} /> - + diff --git a/y-web/src/app/components/common/key-value/key-value.less b/y-web/src/app/components/common/key-value/key-value.less new file mode 100644 index 0000000..4223856 --- /dev/null +++ b/y-web/src/app/components/common/key-value/key-value.less @@ -0,0 +1,120 @@ +.ui-keyvalue-fields { + display: flex; + flex-direction: column; + gap: 0.5em; +} + +.ui-keyvalue { + display: flex; + align-items: flex-start; + gap: 0.25em; + + font-size: var(--text-sm); + + > .key { + flex-shrink: 0; + + padding: 0.25em 0.5em; + + color: rgba(0, 0, 0, 0.5); + font-weight: 500; + text-align: right; + } + + > .value { + width: 100%; + + &.active { + > .value-display { + display: none; + } + + > .value-input { + display: flex; + } + } + + > .value-display { + display: flex; + + min-height: 16px; + + padding: 0.25em 0.5em; + + font-weight: 500; + + border-radius: 4px; + + cursor: pointer; + + transition: 0.15s; + + &:hover { + background-color: var(--color-grey-15); + } + } + + > .value-input { + display: none; + align-items: flex-end; + gap: 0.66em; + + > input { + outline: none; + + padding: 0.15em 0.33em; + + width: 100%; + + color: var(--color-text); + font-family: "Inter", sans-serif; + font-size: var(--text-sm); + font-weight: 420; + + border: 1px solid var(--color-border-2); + border-radius: 4px; + + transition: 0.15s; + + &:hover { + border-color: var(--color-border-3); + } + } + + > .actions { + display: flex; + align-items: center; + gap: 0.33em; + + flex-shrink: 0; + + > .action { + display: flex; + align-items: center; + gap: 0.25em; + + outline: none; + + padding: 0.25em 0.25em; + + color: var(--color-primary); + font-family: "Inter", sans-serif; + font-size: var(--text-sm); + font-weight: 500; + + border: none; + background-color: var(--color-primary-t-05); + border-radius: 4px; + + cursor: pointer; + + transition: 0.15s; + + &:hover { + background-color: var(--color-primary-t-1); + } + } + } + } + } +} diff --git a/y-web/src/app/components/common/key-value/key-value.tsx b/y-web/src/app/components/common/key-value/key-value.tsx new file mode 100644 index 0000000..d04b892 --- /dev/null +++ b/y-web/src/app/components/common/key-value/key-value.tsx @@ -0,0 +1,156 @@ +import { + Accessor, + JSX, + Show, + createEffect, + createMemo, + createSignal, + on, + onMount, +} from "solid-js" + +import { ComponentWithChildren } from "@/module" + +import { Icon } from "../icon/icon" +import "./key-value.less" + +export type KeyValueProps = { + label: string + + value: TValue + onChange: (value: TValue) => void + + getValueString?: (value: TValue | null) => string + + inputField?: (inputProps: { + onChange: (value: TValue) => void + value: Accessor + }) => JSX.Element + + keyWidth?: string +} + +export type KeyValueFieldsProps = { + style?: JSX.CSSProperties +} + +export const KeyValueFields: ComponentWithChildren = ( + props +) => { + return ( +
+ {props.children} +
+ ) +} + +export const KeyValue = (props: KeyValueProps) => { + let inputRef: HTMLInputElement | undefined + + // eslint-disable-next-line solid/reactivity + const [value, setValue] = createSignal(props.value) + const [active, setActive] = createSignal(false) + + // eslint-disable-next-line no-confusing-arrow + const valueString = createMemo(() => + props.getValueString ? props.getValueString(value()) : String(value()) + ) + + createEffect( + on( + () => props.value, + () => { + setValue(() => props.value) + } + ) + ) + + createEffect(() => { + if (active() && inputRef) { + inputRef.focus() + inputRef.select() + } + }) + + onMount(() => { + inputRef?.addEventListener("keydown", (event) => { + if (event.key === "Escape") { + setValue(() => props.value) + return setActive(false) + } + + if (event.key === "Enter") { + setActive(false) + props.onChange(inputRef!.value as TValue) + } + }) + }) + + return ( +
+
+ {props.label} +
+
!active() && setActive((state) => !state)} + > +
{valueString()}
+
+ + setValue(() => event.currentTarget.value as TValue) + } + ref={(ref) => (inputRef = ref)} + /> + } + > + {props.inputField!({ + onChange: setValue, + value, + })} + + +
+ + +
+
+
+
+ ) +} diff --git a/y-web/src/app/components/common/layout/container.less b/y-web/src/app/components/common/layout/container.less new file mode 100644 index 0000000..8da5f2b --- /dev/null +++ b/y-web/src/app/components/common/layout/container.less @@ -0,0 +1,25 @@ +.ui-container { + box-sizing: border-box; + + margin: 0 auto; + padding: 2em; + + height: fit-content; + width: 100%; + + &.s { + max-width: 900px; + } + + &.m { + max-width: 1200px; + } + + &.l { + max-width: 1600px; + } + + &.xl { + max-width: 1920px; + } +} diff --git a/y-web/src/app/components/common/layout/container.tsx b/y-web/src/app/components/common/layout/container.tsx new file mode 100644 index 0000000..2a7c98c --- /dev/null +++ b/y-web/src/app/components/common/layout/container.tsx @@ -0,0 +1,25 @@ +import { JSX } from "solid-js" + +import { ComponentWithChildren } from "@/module" + +import "./container.less" + +export type ContainerProps = { + size: "s" | "m" | "l" | "xl" + + style?: JSX.CSSProperties +} + +export const Container: ComponentWithChildren = (props) => { + return ( +
+ {props.children} +
+ ) +} diff --git a/y-web/src/app/components/common/link/link.less b/y-web/src/app/components/common/link/link.less new file mode 100644 index 0000000..57f9b74 --- /dev/null +++ b/y-web/src/app/components/common/link/link.less @@ -0,0 +1,96 @@ +.ui-link { + &::after { + display: none; + + content: "›"; + + margin-left: 0.5em; + + color: var(--color-text-grey-2); + } + + &:hover { + &::after { + display: inline-block; + } + } + + &.text-secondary { + display: inline-flex; + align-items: center; + + padding: 0.2em 0em; + + box-sizing: border-box; + + color: var(--color-text-grey-1); + + font-weight: 450; + + text-decoration: none; + + border-radius: 4px; + + transition: 0.15s; + + &:hover { + padding: 0.2em 0.5em; + + color: var(--color-primary) !important; + + background-color: var(--color-primary-l-1-t-1); + } + } + + &.text { + display: inline-flex; + align-items: center; + + padding: 0.2em 0em; + + box-sizing: border-box; + + color: var(--color-primary); + font-weight: 450; + + text-decoration: none; + + border-radius: 4px; + + transition: 0.15s; + + &:hover { + color: var(--color-primary) !important; + + padding: 0.2em 0.5em; + + background-color: var(--color-primary-l-1-t-1); + } + } + + &.filled { + display: inline-flex; + align-items: center; + + box-sizing: border-box; + + padding: 0.2em 0.5em; + + color: var(--color-primary); + font-weight: 450; + + text-decoration: none; + + border-radius: 4px; + + background-color: var(--color-primary-t-1); + + transition: 0.15s; + + &:hover { + color: var(--color-primary-d-1); + + background-color: var(--color-primary-t-2); + } + } +} diff --git a/y-web/src/app/components/common/link/link.tsx b/y-web/src/app/components/common/link/link.tsx new file mode 100644 index 0000000..271435d --- /dev/null +++ b/y-web/src/app/components/common/link/link.tsx @@ -0,0 +1,27 @@ +import { JSX } from "solid-js" + +import { ComponentWithChildren } from "@/module" + +import "./link.less" + +export type LinkProps = { + href: string + variant?: "filled" | "text" | "text-secondary" + + style?: JSX.CSSProperties +} + +export const Link: ComponentWithChildren = (props) => { + return ( + + {props.children} + + ) +} diff --git a/y-web/src/app/components/common/modal/modal.tsx b/y-web/src/app/components/common/modal/modal.tsx new file mode 100644 index 0000000..0ab03f6 --- /dev/null +++ b/y-web/src/app/components/common/modal/modal.tsx @@ -0,0 +1,39 @@ +import { JSX, Show } from "solid-js" +import { Portal } from "solid-js/web" + +import { CardProps } from "@/app/components/common/card/card" +import { ComponentWithChildren } from "@/module" + +export type ModalProps = { + header?: JSX.Element + + open: boolean + onClose: () => void + + keepMounted?: boolean + style?: CardProps["style"] +} + +export const Modal: ComponentWithChildren = (props) => { + return ( + +
props.onClose()} + > + +
event.stopPropagation()} + style={props.style} + > + + + + +
+
+
+
+ ) +} diff --git a/y-web/src/app/components/common/note/note.less b/y-web/src/app/components/common/note/note.less new file mode 100644 index 0000000..fa7f403 --- /dev/null +++ b/y-web/src/app/components/common/note/note.less @@ -0,0 +1,19 @@ +.ui-note { + padding: 0.75em 1em; + + border-radius: 4px; + + &.secondary { + color: var(--color-text-grey-05); + font-weight: 450; + + background-color: var(--color-grey-15); + } + + &.critical { + color: var(--color-error); + font-weight: 500; + + background-color: var(--color-error-o-1); + } +} diff --git a/y-web/src/app/components/common/note/note.tsx b/y-web/src/app/components/common/note/note.tsx new file mode 100644 index 0000000..8e533e2 --- /dev/null +++ b/y-web/src/app/components/common/note/note.tsx @@ -0,0 +1,25 @@ +import { JSX } from "solid-js" + +import { ComponentWithChildren } from "@/module" + +import "./note.less" + +export type NoteProps = { + type: "secondary" | "critical" + + style?: JSX.CSSProperties +} + +export const Note: ComponentWithChildren = (props) => { + return ( +
+ {props.children} +
+ ) +} diff --git a/y-web/src/app/components/common/pill/pill.less b/y-web/src/app/components/common/pill/pill.less new file mode 100644 index 0000000..f54e48b --- /dev/null +++ b/y-web/src/app/components/common/pill/pill.less @@ -0,0 +1,12 @@ +.ui-pill { + padding: 0.2em 0.4em; + + font-weight: 450; + + border-radius: 5px; + + &.secondary { + color: var(--color-text-grey-1); + background-color: var(--color-grey-2); + } +} diff --git a/y-web/src/app/components/common/pill/pill.tsx b/y-web/src/app/components/common/pill/pill.tsx new file mode 100644 index 0000000..190378e --- /dev/null +++ b/y-web/src/app/components/common/pill/pill.tsx @@ -0,0 +1,25 @@ +import { JSX } from "solid-js" + +import { ComponentWithChildren } from "@/module" + +import "./pill.less" + +export type PillProps = { + variant?: "secondary" | "info" + + style?: JSX.CSSProperties +} + +export const Pill: ComponentWithChildren = (props) => { + return ( +
+ {props.children} +
+ ) +} diff --git a/y-web/src/app/components/common/popup/popup-container.tsx b/y-web/src/app/components/common/popup/popup-container.tsx new file mode 100644 index 0000000..8e26a39 --- /dev/null +++ b/y-web/src/app/components/common/popup/popup-container.tsx @@ -0,0 +1,7 @@ +import { ComponentWithChildren } from "@/module" + +import "./popup.less" + +export const PopupContainer: ComponentWithChildren = (props) => { + return
{props.children}
+} diff --git a/y-web/src/app/components/common/popup/popup-entry.tsx b/y-web/src/app/components/common/popup/popup-entry.tsx new file mode 100644 index 0000000..c711d88 --- /dev/null +++ b/y-web/src/app/components/common/popup/popup-entry.tsx @@ -0,0 +1,21 @@ +import { ComponentWithChildren } from "@/module" + +export type PopupEntryProps = { + onClick?: (event: MouseEvent) => void + size?: "normal" | "large" +} + +export const PopupEntry: ComponentWithChildren = (props) => { + return ( + + ) +} diff --git a/y-web/src/app/components/common/popup/popup.less b/y-web/src/app/components/common/popup/popup.less new file mode 100644 index 0000000..88ba879 --- /dev/null +++ b/y-web/src/app/components/common/popup/popup.less @@ -0,0 +1,83 @@ +.ui-popup-container { + position: relative; + + > .ui-popup { + position: absolute; + z-index: 10; + + top: 0; + + margin-top: 3em; + + // Hidden state + pointer-events: none; + opacity: 0; + transform: translateY(-0.5em) scale(0.98); + visibility: hidden; + + transition: 0.15s ease-in-out; + + &.shown { + pointer-events: auto; + opacity: 1; + transform: translateY(0) scale(1); + visibility: visible; + } + + .ui-popup-entry { + display: flex; + align-items: center; + gap: 1em; + + width: 100%; + white-space: nowrap; + + padding: 0.5em 1em; + + user-select: none; + cursor: pointer; + + font-weight: 450; + font-family: "Inter", sans-serif; + font-size: var(--text-body); + text-align: left; + + border: 0; + border-radius: 4px; + background-color: transparent; + + transition: 0.2s; + + &:focus, + &:hover { + color: var(--color-primary-d-1); + + background-color: var(--color-primary-o-1); + box-shadow: inset 0 0 0 1px var(--color-primary-t-1); + } + + &.large { + padding: 0.66em 1em; + } + } + + > .ui-popup-block { + min-width: 100px; + + background-color: white; + box-shadow: 0 0 0 1px var(--color-border-15), 0 1px 8px 0 hsl(0, 0%, 95%); + + border-radius: 4px; + + > .content { + > .popup-list { + display: flex; + flex-direction: column; + gap: 0.33em; + } + + padding: 0.33em; + } + } + } +} diff --git a/y-web/src/app/components/common/popup/popup.tsx b/y-web/src/app/components/common/popup/popup.tsx new file mode 100644 index 0000000..6fc0749 --- /dev/null +++ b/y-web/src/app/components/common/popup/popup.tsx @@ -0,0 +1,22 @@ +import { ComponentWithChildren } from "@/module" + +export type PopupProps = { + show?: boolean + position: "left" | "right" +} + +export const Popup: ComponentWithChildren = (props) => { + return ( +
+
+
{props.children}
+
+
+ ) +} diff --git a/y-web/src/app/components/common/select-field/select-field.less b/y-web/src/app/components/common/select-field/select-field.less new file mode 100644 index 0000000..fa8148e --- /dev/null +++ b/y-web/src/app/components/common/select-field/select-field.less @@ -0,0 +1,188 @@ +.ui-select-field { + &.active > .field > .expand-floater { + opacity: 1; + pointer-events: all; + } + + &.disabled { + pointer-events: none; + } + + &.disabled > .field > .container { + background-color: var(--color-grey-15); + + > .selected-options > .option { + background-color: var(--color-border-1); + } + } + + > .field { + position: relative; + + > .container { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5em; + + position: relative; + + padding: 0.3em 0.4em; + + box-shadow: 0 0 0 1px var(--color-border-2); + background-color: white; + + border-radius: 5px; + + cursor: pointer; + + transition: 0.15s; + + &:hover { + background-color: var(--color-grey-05); + + box-shadow: 0 0 0 1px var(--color-border-3), + 0 2px 4px var(--color-border-1); + + > .floater { + > .expand-button { + color: var(--color-text); + background-color: var(--color-grey-15); + } + } + } + + > .selected-options { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 0.3em; + + > .option { + display: flex; + align-items: center; + + padding: 0.25em 0.5em; + + text-align: left; + + border: 1px solid var(--color-border-2); + background-color: white; + + border-radius: 5px; + + cursor: pointer; + + transition: 0.15s; + + &::before { + position: relative; + + content: ""; + } + + &:hover { + color: var(--color-error); + border-color: var(--color-error-o-2); + background-color: var(--color-error-o-2); + } + } + } + + > .floater { + > .expand-button { + display: flex; + align-items: center; + justify-content: center; + + padding: 0.33em; + + color: var(--color-text-grey-1); + + border-radius: 5px; + + transition: 0.15s; + } + } + } + + > .expand-floater { + position: absolute; + display: flex; + flex-direction: column; + gap: 0.5em; + + top: 100%; + left: 0; + + width: 100%; + height: fit-content; + + margin-top: 0.5em; + + // hidden state + opacity: 0; + pointer-events: none; + + transition: 0.15s; + + > .panel { + padding: 0.66em; + + background-color: white; + border-radius: 5px; + box-shadow: 0 0 0 1px hsla(234, 15%, 75%, 0.5), + 0 10px 15px 0 rgba(0, 0, 0, 0.033); + + > .available-options { + display: flex; + flex-direction: column; + gap: 0.15em; + + overflow-y: auto; + + max-height: 25vh; + + > .option { + display: flex; + align-items: center; + + padding: 0.4em 0.6em; + + color: var(--color-text); + font-family: "Inter", sans-serif; + font-weight: 480; + font-size: var(--text-sm); + text-align: left; + + background-color: white; + border: 0; + border-radius: 4px; + + cursor: pointer; + + transition: 0.15s; + + &.selected, + &.selected:hover { + color: var(--color-primary-d-1); + background-color: var(--color-primary-d-1-t-1); + } + + &:hover { + color: var(--color-text); + background-color: var(--color-grey-15); + } + } + } + } + } + } + + > .input-subtext { + margin-top: 0.4em; + + font-size: var(--text-sm); + color: var(--color-text-grey-05); + } +} diff --git a/y-web/src/app/components/common/select-field/select-field.tsx b/y-web/src/app/components/common/select-field/select-field.tsx new file mode 100644 index 0000000..91ce998 --- /dev/null +++ b/y-web/src/app/components/common/select-field/select-field.tsx @@ -0,0 +1,156 @@ +import { + Accessor, + Component, + For, + JSX, + Show, + createEffect, + createMemo, + createSignal, +} from "solid-js" + +import { Icon } from "../icon/icon" +import { InputError, InputErrorProps } from "../input-error/input-error" +import { Text } from "../text/text" +import "./select-field.less" + +export type SelectOption = { + name: string + id: string +} + +export type SelectProps = + Partial & { + options: SelectOption[] + + value: Accessor | null> + onChange: (values: Array) => void + + label?: string + subtext?: JSX.Element + + width?: string + + disabled?: boolean + } + +export const SelectField: Component = < + TOption extends SelectOption = SelectOption +>( + props: SelectProps +) => { + const [active, setActive] = createSignal(false) + + const options = createMemo(() => { + const newOptions: Record = {} + + for (const option of props.options) { + newOptions[option.id] = option + } + + return newOptions + }) + + const toggleActive = () => setActive((state) => !state) + + const toggleOption = (id: string) => { + if (props.value() === null) { + props.onChange([id]) + } else { + if (props.value()!.includes(id)) { + props.onChange(props.value()!.filter((v) => v !== id)) + } else { + props.onChange([...props.value()!, id]) + } + } + } + + createEffect(() => { + if (!props.value()) { + props.onChange([]) + } + }) + + return ( +
+ + + {props.label} + + + +
+
+
+ + {(selectedOptionId) => ( + + )} + +
+ +
+
+ +
+
+
+ +
+
+
+ + {(option) => ( + + )} + +
+
+
+
+ + + + + + +
{props.subtext}
+
+
+ ) +} diff --git a/y-web/src/app/components/common/stack/stack.tsx b/y-web/src/app/components/common/stack/stack.tsx new file mode 100644 index 0000000..020991f --- /dev/null +++ b/y-web/src/app/components/common/stack/stack.tsx @@ -0,0 +1,32 @@ +import { JSX } from "solid-js" + +import { ComponentWithChildren } from "@/module" + +export type StackProps = { + direction?: JSX.CSSProperties["flex-direction"] + justifyContent?: JSX.CSSProperties["justify-content"] + alignItems?: JSX.CSSProperties["align-items"] + spacing?: JSX.CSSProperties["gap"] + + style?: JSX.CSSProperties + id?: string +} + +export const Stack: ComponentWithChildren = (props) => { + return ( +
+ {props.children} +
+ ) +} diff --git a/y-web/src/app/components/common/tab/tab.less b/y-web/src/app/components/common/tab/tab.less new file mode 100644 index 0000000..cdc1728 --- /dev/null +++ b/y-web/src/app/components/common/tab/tab.less @@ -0,0 +1,39 @@ +.ui-tabs-container { + display: flex; + align-items: center; + + gap: 1em; +} + +.ui-tab { + display: flex; + + padding: 0.5em 1.25em; + + font-family: "Inter", sans-serif; + font-weight: 500; + color: var(--color-text-grey-075); + + border: 0; + border-radius: 5px; + background-color: white; + + cursor: pointer; + + transition: 0.15s; + + &.selected { + color: var(--color-primary-d-1); + background-color: var(--color-primary-d-1-t-1); + box-shadow: inset 0 0 0 1px var(--color-primary-d-1-t-1); + + &:hover { + color: var(--color-primary); + background-color: var(--color-primary-t-2); + } + } + + &:hover { + background-color: var(--color-grey-15); + } +} diff --git a/y-web/src/app/components/common/tab/tab.tsx b/y-web/src/app/components/common/tab/tab.tsx new file mode 100644 index 0000000..73c82ec --- /dev/null +++ b/y-web/src/app/components/common/tab/tab.tsx @@ -0,0 +1,27 @@ +import { Component, JSX } from "solid-js" + +import { ComponentWithChildren } from "@/module" + +import "./tab.less" + +export type TabProps = { + label: JSX.Element + selected?: boolean + + onClick?: (event: MouseEvent) => void +} + +export const TabsContainer: ComponentWithChildren = (props) => { + return
{props.children}
+} + +export const Tab: Component = (props) => { + return ( + + ) +} diff --git a/y-web/src/app/components/common/text/text.less b/y-web/src/app/components/common/text/text.less new file mode 100644 index 0000000..1fe8772 --- /dev/null +++ b/y-web/src/app/components/common/text/text.less @@ -0,0 +1,41 @@ +.ui-text { + display: inline-flex; + + margin: 0; +} + +h1.ui-text { + color: var(--color-text-header); + + font-size: 2em; + font-weight: 800; +} + +h2.ui-text { + color: var(--color-text-header); + + font-size: 1.5em; + font-weight: 700; +} + +h3.ui-text { + color: var(--color-text-header); + + font-size: 1.2em; + font-weight: 600; +} + +.ui-text.body2 { + font-size: var(--text-body2); +} + +.ui-text.secondary { + color: var(--color-text-grey-1); +} + +.ui-text.pill { + padding: 0.15em 0.66em; + border-radius: 3px; + width: fit-content; + background-color: var(--color-border-15); +} diff --git a/y-web/src/app/components/common/text/text.tsx b/y-web/src/app/components/common/text/text.tsx new file mode 100644 index 0000000..5edd7bf --- /dev/null +++ b/y-web/src/app/components/common/text/text.tsx @@ -0,0 +1,97 @@ +import { JSX, Match, Switch, createMemo } from "solid-js" + +import { ComponentWithChildren } from "@/module" + +import "./text.less" + +export type TextProps = { + variant?: "h1" | "h2" | "h3" | "body" | "body2" | "secondary" + fontWeight?: JSX.CSSProperties["font-weight"] + fontSize?: JSX.CSSProperties["font-size"] + textAlign?: JSX.CSSProperties["text-align"] + color?: JSX.CSSProperties["color"] + monospace?: boolean + container?: "text" | "pill" + + style?: JSX.CSSProperties +} + +export const Text: ComponentWithChildren = (props) => { + const componentProps = createMemo(() => { + return { + style: { + "font-weight": props.fontWeight, + "text-align": props.textAlign, + + ...(props.color && { + color: props.color, + }), + + ...(props.fontSize && { + "font-size": props.fontSize, + }), + + ...(props.monospace && { + // eslint-disable-next-line @typescript-eslint/quotes + "font-family": '"SourceCodePro", monospace', + "font-weight": 450, + }), + + ...props.style, + } as JSX.CSSProperties, + } + }) + + const classList = createMemo(() => ({ + "ui-text": true, + pill: props.container === "pill", + })) + + return ( + + {props.children} + + } + > + +

+ {props.children} +

+
+ +

+ {props.children} +

+
+ +

+ {props.children} +

+
+ +
+ {props.children} +
+
+ +
+ {props.children} +
+
+
+ ) +} diff --git a/y-web/src/app/components/list-page-switcher/list-page-switcher.less b/y-web/src/app/components/list-page-switcher/list-page-switcher.less new file mode 100644 index 0000000..98cca06 --- /dev/null +++ b/y-web/src/app/components/list-page-switcher/list-page-switcher.less @@ -0,0 +1,51 @@ +.ui-list-page-switcher { + display: flex; + align-items: center; + justify-content: space-between; + + user-select: none; + + > * { + display: flex; + align-items: center; + + gap: 0.5em; + + > .status-text { + padding: 0.15em 0.5em; + + color: white; + font-weight: 600; + font-size: var(--text-sm); + font-family: "SourceCodePro", monospace; + + border-radius: 4px; + + transition: 0.15s; + + &.idle { + color: var(--color-success); + background-color: var(--color-success-t-3); + } + + &.loading { + color: var(--color-warning-d-1); + background-color: var(--color-warning-t-3); + } + } + + > .info-text { + padding: 0.15em 0.5em; + + color: var(--color-text-grey-1); + font-weight: 450; + font-size: var(--text-sm); + + border-radius: 4px; + + background-color: var(--color-border-1); + + transition: 0.15s; + } + } +} diff --git a/y-web/src/app/components/list-page-switcher/list-page-switcher.tsx b/y-web/src/app/components/list-page-switcher/list-page-switcher.tsx new file mode 100644 index 0000000..b19ecbc --- /dev/null +++ b/y-web/src/app/components/list-page-switcher/list-page-switcher.tsx @@ -0,0 +1,95 @@ +import { Component, Setter, Show } from "solid-js" + +import { CreateQueryResult } from "@tanstack/solid-query" + +import { Button } from "../common/button/button" +import "./list-page-switcher.less" + +export type ListPageSwitcherProps = { + totalCount: number + rowsPerPage: number + currentPage: number + onPageChange: Setter + + currentCount?: number + query?: CreateQueryResult +} + +export const ListPageSwitcher: Component = (props) => { + const totalPages = () => Math.max(1, props.totalCount / props.rowsPerPage) + + const canGoBack = () => props.currentPage > 0 + const canGoForward = () => props.currentPage + 1 < totalPages() + + const goBack = () => { + // eslint-disable-next-line solid/reactivity + props.onPageChange((page) => (canGoBack() ? page - 1 : page)) + } + + const goForward = () => { + // eslint-disable-next-line solid/reactivity + props.onPageChange((page) => { + return canGoForward() ? page + 1 : page + }) + } + + return ( +
+
+ +
+ updating... +
+
+ + +
{props.currentCount} entries found
+
+ +
{props.totalCount} entries total
+
+ +
+
+ Page {props.currentPage + 1} of {totalPages()} +
+ +
+
+ ) +} diff --git a/y-web/src/app/components/list/components/list-entry-link.less b/y-web/src/app/components/list/components/list-entry-link.less new file mode 100644 index 0000000..a822335 --- /dev/null +++ b/y-web/src/app/components/list/components/list-entry-link.less @@ -0,0 +1,58 @@ +.ui-list-entry-link { + position: relative; + display: flex; + width: fit-content; + + > a { + color: var(--color-text); + text-decoration: none; + + > .content { + cursor: pointer; + + padding: 0.25em 0.5em; + + border-radius: 4px; + + // Hidden state + background-color: transparent; + + transition: 0.15s ease-in-out; + } + } + + &:hover > a > .content { + background-color: var(--color-grey-15); + } + + > .arrow { + display: flex; + align-items: center; + justify-content: center; + + box-sizing: border-box; + + height: 100%; + + padding: 0.33em; + + position: absolute; + top: 50%; + left: 100%; + margin-left: 0.5em; + + border-radius: 4px; + background-color: var(--color-grey-15); + + // Hidden state + opacity: 0; + transform: translateY(-50%) translateX(-10%); + + transition: 0.15s ease-out; + } + + &:hover > .arrow { + opacity: 1; + transform: translateY(-50%) translateX(0); + } +} diff --git a/y-web/src/app/components/list/components/list-entry-link.tsx b/y-web/src/app/components/list/components/list-entry-link.tsx new file mode 100644 index 0000000..1710472 --- /dev/null +++ b/y-web/src/app/components/list/components/list-entry-link.tsx @@ -0,0 +1,23 @@ +import { Link, LinkProps } from "@solidjs/router" + +import { Icon } from "@/app/components/common/icon/icon" +import { ComponentWithChildren } from "@/module" + +import "./list-entry-link.less" + +export type ListEntryLinkProps = LinkProps + +export const ListEntryLink: ComponentWithChildren = ( + props +) => { + return ( +
+ +
{props.children}
+ +
+ +
+
+ ) +} diff --git a/y-web/src/app/components/list/list.less b/y-web/src/app/components/list/list.less new file mode 100644 index 0000000..5b3f69c --- /dev/null +++ b/y-web/src/app/components/list/list.less @@ -0,0 +1,72 @@ +.ui-list-container { + > .list-head { + padding: 1.25em; + margin-bottom: 1em; + + border-radius: 5px; + + background-color: hsla(234, 15%, 75%, 0.033); + box-shadow: 0 0 0 1px hsla(234, 15%, 75%, 0.5), + 0 10px 15px 0 rgba(0, 0, 0, 0.033); + } + + > .list-footer { + padding: 1.25em; + margin-top: 1em; + + border-radius: 5px; + + background-color: hsla(234, 15%, 75%, 0.033); + box-shadow: 0 0 0 1px hsla(234, 15%, 75%, 0.5), + 0 10px 15px 0 rgba(0, 0, 0, 0.033); + } + + > .list-entries { + display: flex; + flex-direction: column; + + border-radius: 5px; + + box-shadow: 0 0 0 1px hsla(234, 15%, 75%, 0.5), + 0 10px 15px 0 rgba(0, 0, 0, 0.033); + + &:empty { + display: none; + } + + > * { + position: relative; + + padding: 0.25em 0.25em 0.25em 1em; + + transition: background-color 0.1s, box-shadow 0.1s; + + border-bottom: 1px solid var(--color-border-1); + + .entry-actions { + opacity: 0; + + transition: 0.1s; + } + + &:hover { + > .entry-actions { + opacity: 1; + } + } + + &.selected { + background-color: var(--color-primary-l-1-t-05); + border-bottom: 1px solid var(--color-primary-l-1-t-1); + + &:last-child { + border-bottom: none; + } + } + + &:last-child { + border-bottom: none; + } + } + } +} diff --git a/y-web/src/app/components/list/list.tsx b/y-web/src/app/components/list/list.tsx new file mode 100644 index 0000000..950a644 --- /dev/null +++ b/y-web/src/app/components/list/list.tsx @@ -0,0 +1,47 @@ +import { JSX } from "solid-js" + +import { ComponentWithChildren } from "@/module" + +import "./list.less" + +export const ListHead: ComponentWithChildren< + JSX.HTMLAttributes +> = (props) => { + return ( +
+ {props.children} +
+ ) +} + +export const ListEntries: ComponentWithChildren< + JSX.HTMLAttributes +> = (props) => { + return ( +
+ {props.children} +
+ ) +} + +export const ListFooter: ComponentWithChildren< + JSX.HTMLAttributes +> = (props) => { + return ( + + ) +} + +export const List: ComponentWithChildren = (props) => { + return ( +
+ {props.children} +
+ ) +} diff --git a/y-web/src/app/core/request.ts b/y-web/src/app/core/request.ts new file mode 100644 index 0000000..0ac4fe8 --- /dev/null +++ b/y-web/src/app/core/request.ts @@ -0,0 +1,59 @@ +import { Tail } from "@/app/core/utils" + +type RequestMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" + +export type RequestData = { + query?: URLSearchParams + body?: Record +} + +export type ResponseError = { + code: string +} + +export type ResponseData = { + error?: ResponseError +} & Record + +const request = async ( + method: RequestMethod, + endpoint: string, + data?: RequestData, + requestOptions?: RequestInit +) => { + const path = data?.query ? `${endpoint}?${data.query.toString()}` : endpoint + + const response = await fetch(`/api${path}`, { + method: method, + mode: "cors", + credentials: "same-origin", + referrerPolicy: "same-origin", + headers: { + "Content-Type": "application/json", + }, + + body: data?.body && JSON.stringify(data.body), + + ...requestOptions, + }) + + return response.json().then((json: ResponseData) => { + if (json.error) throw json.error + return json + }) +} + +type RequestParameters = Tail> + +const createRequest = + (method: RequestMethod) => + async (...args: RequestParameters) => + request(method, ...args) + +export const { get, post, put, patch, del } = { + get: createRequest("GET"), + post: createRequest("POST"), + put: createRequest("PUT"), + patch: createRequest("PATCH"), + del: createRequest("DELETE"), +} diff --git a/y-web/src/app/core/request.utils.ts b/y-web/src/app/core/request.utils.ts new file mode 100644 index 0000000..62a543f --- /dev/null +++ b/y-web/src/app/core/request.utils.ts @@ -0,0 +1,13 @@ +export type TableInput = Partial<{ + orderBy: string + limit: number + skip: number + search: string +}> + +export const appendTableInput = (query: URLSearchParams, input: TableInput) => { + if (input.orderBy) query.set("order_by", input.orderBy) + if (input.limit) query.set("limit", input.limit.toString()) + if (input.skip) query.set("skip", input.skip.toString()) + if (input.search) query.set("search", input.search.trim()) +} diff --git a/y-web/src/app/core/toast.ts b/y-web/src/app/core/toast.ts new file mode 100644 index 0000000..e7ff468 --- /dev/null +++ b/y-web/src/app/core/toast.ts @@ -0,0 +1,57 @@ +import { createRoot, createSignal, createUniqueId } from "solid-js" + +const DEFAULT_TOAST_DURATION_MS = 6000 +export type ToastSeverity = "info" | "success" | "error" + +export type ToastInput = { + title: string + content?: string + icon?: string + + severity?: ToastSeverity + + duration?: number +} + +export type Toast = ToastInput & { + id: string + duration: number +} + +const createToastController = () => { + const [toasts, setToasts] = createSignal([]) + + const notify = (newToast: ToastInput) => { + const toastId = createUniqueId() + const toastDuration = newToast.duration ?? DEFAULT_TOAST_DURATION_MS + + setToasts((currentToasts) => [ + ...currentToasts, + { + ...newToast, + id: toastId, + duration: toastDuration, + }, + ]) + + setTimeout(() => { + setToasts((currentToasts) => + currentToasts.filter((toast) => toast.id !== toastId) + ) + }, toastDuration) + } + + const remove = (toastId: string) => { + setToasts((currentToasts) => + currentToasts.filter((toast) => toast.id !== toastId) + ) + } + + return { + toasts, + notify, + remove, + } +} + +export const toastCtl = createRoot(createToastController) diff --git a/y-web/src/app/core/use-form.ts b/y-web/src/app/core/use-form.ts index 829261d..70f1c2c 100644 --- a/y-web/src/app/core/use-form.ts +++ b/y-web/src/app/core/use-form.ts @@ -22,6 +22,7 @@ import { TWatchedFields, ValueFromPath, findDefaultValue, + isFieldWatched, validateFieldValue, } from "./use-form.utils" import { debug } from "./utils" @@ -30,6 +31,7 @@ export type UseFormInput = { defaultValues?: { [K in keyof FieldValues]: FieldValues[K] } watch?: WatchedFields onSubmit?: (values: FieldValues) => void + disabled?: () => boolean } export type FieldValidateFunction = ( @@ -92,7 +94,7 @@ export type Form< ? FieldValues[FieldName] : unknown > - setValue: Setter< + onChange: Setter< FieldName extends keyof FieldValues & string ? FieldValues[FieldName] : unknown @@ -103,9 +105,15 @@ export type Form< FieldName extends | WatchedFields[number] | `${WatchedFields[number]}.${string}` + | string, + CustomFieldValue >( name: FieldNameFromPath - ) => () => ValueFromPath + ) => () => FieldName extends + | WatchedFields[number] + | `${WatchedFields[number]}.${string}` + ? ValueFromPath + : CustomFieldValue useFieldset: < FieldsetName extends @@ -144,6 +152,7 @@ export const useForm = < defaultValues, watch, onSubmit, + disabled, }: UseFormInput = {}): Form< FieldValues, WatchedFields @@ -174,21 +183,31 @@ export const useForm = < defaultValues && findDefaultValue(defaultValues, name.split(".")) if (defaultValue) { - ref.value = defaultValue as string + if (ref.type === "checkbox") { + ref.checked = defaultValue as boolean + } else { + ref.value = defaultValue as string + } } const controller = createSignal(defaultValue) if ( fieldsetName - ? watch?.includes(fieldsetName as WatchedFields[number]) - : watch?.includes(name as WatchedFields[number]) + ? isFieldWatched(fieldsetName, (watch ?? []) as Readonly) + : isFieldWatched(name, (watch ?? []) as Readonly) ) { debug("[use-form] registering a watcher", { name }) - ref.addEventListener("input", () => { - controller[1](ref.value as FieldValues[keyof FieldValues]) - }) + if (ref.type === "checkbox") { + ref.addEventListener("change", () => { + controller[1](ref.checked as FieldValues[keyof FieldValues]) + }) + } else { + ref.addEventListener("input", () => { + controller[1](ref.value as FieldValues[keyof FieldValues]) + }) + } } setForm( @@ -228,6 +247,8 @@ export const useForm = < FieldName extends | WatchedFields[number] | `${WatchedFields[number]}.${string}` + | string, + CustomFieldValue >( name: FieldNameFromPath ) => { @@ -245,7 +266,11 @@ export const useForm = < return createMutable( () => fieldValue()?.controller?.[0]?.() ?? null - ) as () => ValueFromPath + ) as () => FieldName extends + | WatchedFields[number] + | `${WatchedFields[number]}.${string}` + ? ValueFromPath + : CustomFieldValue } const processFieldsetDefaultValues = < @@ -504,6 +529,7 @@ export const useForm = < } if (!onSubmit) return + if (disabled && disabled()) return const values: Partial> = {} const processedFieldsets: string[] = [] @@ -515,7 +541,9 @@ export const useForm = < if (!isFieldsetField) { const value = field.controlled ? field.controller?.[0]() - : field.inputRef?.value ?? null + : (field.inputRef?.type === "checkbox" + ? field.inputRef.checked + : field.inputRef?.value) ?? null const validateResult = validateFieldValue(field) @@ -556,13 +584,29 @@ export const useForm = < (registeredField) => registeredField.name === fieldName ) - if (field?.inputRef) { - field.inputRef.value = + if (field?.controlled && field.controller?.[1]) { + const rawValue = typeof value === "function" - ? ((value as (currentValue: FieldValue) => FieldValue)( + ? (value as (currentValue: FieldValue) => FieldValue)( + field.controller[0]() as FieldValue + ) + : value + + field.controller[1](rawValue) + } else if (field?.inputRef) { + const rawValue = + typeof value === "function" + ? (value as (currentValue: FieldValue) => FieldValue)( field.inputRef.value as FieldValue - ) as string) - : (value as string) + ) + : value + + if (field.inputRef.type === "checkbox") { + field.inputRef.checked = rawValue + field.inputRef.dispatchEvent(new Event("change", { bubbles: true })) + } else { + field.inputRef.value = rawValue + } field.inputRef.dispatchEvent(new Event("input")) } @@ -637,7 +681,7 @@ export const useForm = < name, value: fieldValue, - setValue: setFieldValue, + onChange: setFieldValue, } } diff --git a/y-web/src/app/core/use-form.utils.ts b/y-web/src/app/core/use-form.utils.ts index caa82ab..9e19d70 100644 --- a/y-web/src/app/core/use-form.utils.ts +++ b/y-web/src/app/core/use-form.utils.ts @@ -79,3 +79,18 @@ export const findDefaultValue = < return null } + +export const isFieldWatched = >( + fieldName: string, + watch: WatchedFields +) => { + return ( + watch.includes(fieldName) || + // ! TODO This is a pretty dumb way to check this + watch.some( + (watchedField) => + watchedField.includes("*") && + fieldName.includes(watchedField.replace("*", "")) + ) + ) +} diff --git a/y-web/src/app/core/use-table-state.ts b/y-web/src/app/core/use-table-state.ts new file mode 100644 index 0000000..380f343 --- /dev/null +++ b/y-web/src/app/core/use-table-state.ts @@ -0,0 +1,93 @@ +import { batch, createMemo, createSignal } from "solid-js" + +import { DEFAULT_DEBOUNCE_MS } from "./utils" + +const DEFAULT_ROWS_PER_PAGE = 50 + +export type Order = "asc" | "desc" + +export type UseTableStateConfig = { + defaultOrder?: Order + defaultOrderBy?: string + defaultSearch?: string + defaultRowsPerPage?: number + defaultPage?: number + + defaultSelectedEntries?: TData[] + + searchDebounceMs?: number +} + +export const useTableState = ( + config: UseTableStateConfig = {} +) => { + let timeout: number | undefined + + const [order, setOrder] = createSignal(config.defaultOrder || "asc") + const [orderBy, setOrderBy] = createSignal(config.defaultOrderBy) + const [rowsPerPage, setRowsPerPage] = createSignal( + config.defaultRowsPerPage ?? DEFAULT_ROWS_PER_PAGE + ) + + const [page, setPage] = createSignal(config.defaultPage || 0) + + const [searchText, setSearchText] = createSignal(config.defaultSearch || "") + const [search, setSearch] = createSignal(config.defaultSearch || "") + + const [selectedEntries, setSelectedEntries] = createSignal>( + config.defaultSelectedEntries + ? new Set(config.defaultSelectedEntries) + : new Set(), + { + equals: false, + } + ) + + const onSelect = (entry: TData) => { + setSelectedEntries((entries) => { + if (entries.has(entry)) { + entries.delete(entry) + } else { + entries.add(entry) + } + + return entries + }) + } + + const skip = createMemo(() => page() * rowsPerPage()) + + const onSearchChange = (newValue: string) => { + batch(() => { + setSearchText(newValue) + + clearTimeout(timeout) + timeout = window.setTimeout(() => { + setSearch(newValue) + }, config.searchDebounceMs ?? DEFAULT_DEBOUNCE_MS) + + setPage(0) + }) + } + + return { + order, + orderBy, + searchText, + search, + selectedEntries, + page, + rowsPerPage, + + skip, + + setOrder, + setOrderBy, + setSearch: onSearchChange, + setSelectedEntries, + setPage, + setRowsPerPage, + + onSelect, + } +} diff --git a/y-web/src/app/core/util/toast-utils.ts b/y-web/src/app/core/util/toast-utils.ts new file mode 100644 index 0000000..5d8fbc2 --- /dev/null +++ b/y-web/src/app/core/util/toast-utils.ts @@ -0,0 +1,15 @@ +import { unsafe_t } from "@/i18n" + +import { toastCtl } from "../toast" + +export const genericErrorToast = (error: unknown) => { + const { notify } = toastCtl + const errorCode = (error as { code: string }).code + + notify({ + title: "Error", + content: unsafe_t(`error.code.${errorCode}`) ?? errorCode, + severity: "error", + icon: "exclamation", + }) +} diff --git a/y-web/src/app/core/util/use-domain.ts b/y-web/src/app/core/util/use-domain.ts new file mode 100644 index 0000000..0cc19e1 --- /dev/null +++ b/y-web/src/app/core/util/use-domain.ts @@ -0,0 +1,25 @@ +import { createMemo } from "solid-js" + +import { useLocation } from "@solidjs/router" + +export const domainName = { + "": "Home", + admin: "Administration", +} as const + +export const domainIcon = { + "": "home", + admin: "handyman", +} as const + +export const useDomain = () => { + const location = useLocation() + + const domain = createMemo( + () => (location.pathname.split("/")[1] ?? "") as keyof typeof domainName + ) + + return { + domain, + } +} diff --git a/y-web/src/app/core/utils.ts b/y-web/src/app/core/utils.ts index 14a5844..77c8355 100644 --- a/y-web/src/app/core/utils.ts +++ b/y-web/src/app/core/utils.ts @@ -1,5 +1,11 @@ import { DEV } from "solid-js" +export const DEFAULT_DEBOUNCE_MS = 350 + +export type Tail = T extends [unknown, ...infer R] + ? R + : never + export const debug = (...args: unknown[]) => { if (DEV) console.debug(...args) } diff --git a/y-web/src/app/layout/app-aside.less b/y-web/src/app/layout/app-aside.less index b753bf3..c57c678 100644 --- a/y-web/src/app/layout/app-aside.less +++ b/y-web/src/app/layout/app-aside.less @@ -1,6 +1,202 @@ -#app-aside { - height: 100%; - min-width: 200px; - - border-right: 1px solid var(--color-border-15); -} +#app-aside { + display: flex; + flex-direction: column; + gap: 1em; + + box-sizing: border-box; + + padding: 1em; + + height: 100%; + min-width: 250px; + + border-right: 1px solid var(--color-border-15); + + .aside-section { + > .aside-section-title { + color: var(--color-text-grey-1); + font-weight: 500; + + margin-bottom: 1em; + } + + > .aside-section-content { + display: flex; + flex-direction: column; + gap: 0.5em; + + .aside-entry { + display: flex; + align-items: center; + justify-content: space-between; + + padding: 0.66em 0.75em; + + text-decoration: none; + color: var(--color-text); + + border-radius: 6px; + + transition: 0.15s ease-in-out; + + &.selected { + background-color: var(--color-primary-l-1-t-05) !important; + color: var(--color-primary-d-1); + } + + &:focus, + &:hover { + background-color: var(--color-grey-2); + } + + &:hover > .arrow { + opacity: 1; + transform: translateX(0) scale(1); + } + + > .content { + display: flex; + align-items: center; + + > .line { + display: flex; + align-self: flex-end; + + width: 4px; + + margin-right: 0.75em; + + border-radius: 2px; + background-color: var(--color-primary-d-1); + + // Hidden state + opacity: 0; + height: 0; + + transition: 0.2s ease-in-out; + } + + > .icon { + display: flex; + align-items: center; + justify-content: center; + + color: var(--color-text-grey-05); + + padding: 2px 0; + + margin-right: 1em; + + .ui-icon { + font-variation-settings: "FILL" 0, "wght" 400, "GRAD" -25, + "opsz" 18; + + transition: 0.25s ease-in-out; + } + } + + > .text { + font-weight: 550; + } + } + + &.selected > .content > .line { + height: 22px; + opacity: 1; + } + + &.selected > .content > .icon { + color: var(--color-primary-d-1); + + .ui-icon { + font-variation-settings: "FILL" 1, "wght" 800, "GRAD" 25, "opsz" 18 !important; + text-shadow: var(--color-primary-t-1) 2px 2px; + } + } + + > .arrow { + display: flex; + align-items: center; + justify-content: center; + + // Hidden state + opacity: 0; + transform: translateX(-0.25em) scale(0.9); + + transition: 0.15s ease-in-out; + } + + &.selected > .arrow { + opacity: 0 !important; + } + } + + .aside-entry:active > .arrow { + opacity: 1; + transform: translateX(0.25em) scale(1); + + transition: 0.025s; + } + + .aside-entry.group:active > .arrow { + opacity: 1; + transform: translateX(0) translateY(0.25em) scale(1); + + transition: 0.025s; + } + + .sub-entries-container { + display: flex; + gap: 0.5em; + + height: 100%; + + > .spacer { + display: flex; + + width: 4px; + + margin-left: calc(0.66em + 1px); + + border-radius: 4px; + + box-shadow: inset 0 0 0 1px var(--color-border-15); + } + + > .sub-entries { + display: flex; + flex-direction: column; + gap: 0.33em; + + width: 100%; + + > .sub-entry { + display: flex; + align-items: center; + + height: 100%; + + .aside-entry { + width: 100%; + + padding: 0.33em 0.66em; + + font-size: var(--text-sm); + + border-radius: 4px; + + > .content > .line { + width: 3px; + } + + &.selected > .content > .line { + height: 18px; + opacity: 1; + } + } + } + } + } + } + } +} diff --git a/y-web/src/app/layout/app-aside.tsx b/y-web/src/app/layout/app-aside.tsx index 2c72df2..7e49079 100644 --- a/y-web/src/app/layout/app-aside.tsx +++ b/y-web/src/app/layout/app-aside.tsx @@ -1,7 +1,7 @@ -import { Component } from "solid-js" +import { ComponentWithChildren } from "@/module" import "./app-aside.less" -export const AppAside: Component = () => { - return
+export const AppAside: ComponentWithChildren = (props) => { + return
{props.children}
} diff --git a/y-web/src/app/layout/app-content.less b/y-web/src/app/layout/app-content.less index 4f44b85..89a7b1a 100644 --- a/y-web/src/app/layout/app-content.less +++ b/y-web/src/app/layout/app-content.less @@ -1,2 +1,11 @@ #app-content { + display: flex; + + box-sizing: border-box; + + width: 100%; + height: 100%; + max-height: 100%; + + overflow-y: auto; } diff --git a/y-web/src/app/layout/app-content.tsx b/y-web/src/app/layout/app-content.tsx index 57dc4ea..79942d5 100644 --- a/y-web/src/app/layout/app-content.tsx +++ b/y-web/src/app/layout/app-content.tsx @@ -1,13 +1,7 @@ -import { Component } from "solid-js" - -import { AppRouter } from "@/app/app-router" +import { ComponentWithChildren } from "@/module" import "./app-content.less" -export const AppContent: Component = () => { - return ( -
- -
- ) +export const AppContent: ComponentWithChildren = (props) => { + return
{props.children}
} diff --git a/y-web/src/app/layout/app-menubar.less b/y-web/src/app/layout/app-menubar.less index 8053df8..d9b2f76 100644 --- a/y-web/src/app/layout/app-menubar.less +++ b/y-web/src/app/layout/app-menubar.less @@ -17,16 +17,28 @@ padding: 4px; + > .left { + margin: 0; + } + + > .right { + margin: 0.5em; + } + > div { display: flex; align-items: center; height: 100%; > .instance-info { + box-sizing: border-box; + display: flex; align-items: center; gap: 8px; + width: 250px; + > .instance-name { font-size: var(--text-body2); font-weight: 500; diff --git a/y-web/src/app/layout/app-menubar.tsx b/y-web/src/app/layout/app-menubar.tsx index 3d4256b..9f06a1d 100644 --- a/y-web/src/app/layout/app-menubar.tsx +++ b/y-web/src/app/layout/app-menubar.tsx @@ -3,6 +3,8 @@ import { Component } from "solid-js" import { InstanceLogo } from "@/app/layout/components/instance-logo" import "./app-menubar.less" +import { DomainSelector } from "./components/domain-selector" +import { UserIsland } from "./components/user-island" export const AppMenubar: Component = () => { return ( @@ -13,9 +15,12 @@ export const AppMenubar: Component = () => {
{"y"}
+
-
+
+ +
) diff --git a/y-web/src/app/layout/app-toasts.less b/y-web/src/app/layout/app-toasts.less new file mode 100644 index 0000000..65c71fa --- /dev/null +++ b/y-web/src/app/layout/app-toasts.less @@ -0,0 +1,94 @@ +@keyframes toast-enter { + 0% { + transform: scale(0.66) translateX(100%); + opacity: 0; + } + 100% { + transform: scale(1) translateX(0); + opacity: 1; + } +} + +#app-toasts-container { + position: fixed; + + bottom: 0; + right: 0; + + > .toasts { + display: flex; + flex-direction: column; + gap: 1em; + + padding: 1em; + + > .toast { + display: flex; + align-items: center; + gap: 0.75em; + + min-width: 200px; + max-width: 300px; + + padding: 0.5em 0.75em; + + cursor: pointer; + + border-radius: 6px; + border: 1px solid var(--color-border-1); + box-shadow: 0px 0px 10px 0px var(--color-grey-1); + background-color: white; + + animation: toast-enter 0.33s cubic-bezier(0.83, 0, 0.17, 1) 0s 1 forwards; + transition: 0.15s; + + &:hover { + background-color: var(--color-grey-1); + } + + &.severity-info > .icon-container { + color: var(--color-primary); + background-color: var(--color-primary-t-1); + } + + &.severity-success > .icon-container { + color: var(--color-success); + background-color: var(--color-success-t-1); + } + + &.severity-error > .icon-container { + color: var(--color-error); + background-color: var(--color-error-o-1); + } + + > .icon-container { + display: flex; + align-items: center; + justify-content: center; + + padding: 0.5em; + + border-radius: 6px; + } + + > .content-container { + > .content { + display: flex; + flex-direction: column; + gap: 0.25em; + + > .toast-title { + font-weight: 500; + font-size: var(--text-body); + } + + > .toast-content { + color: var(--color-text-grey-1); + font-weight: 400; + font-size: var(--text-sm); + } + } + } + } + } +} diff --git a/y-web/src/app/layout/app-toasts.tsx b/y-web/src/app/layout/app-toasts.tsx new file mode 100644 index 0000000..a54ec5d --- /dev/null +++ b/y-web/src/app/layout/app-toasts.tsx @@ -0,0 +1,48 @@ +import { For, Show } from "solid-js" + +import { Icon } from "../components/common/icon/icon" +import { toastCtl } from "../core/toast" +import "./app-toasts.less" + +export const AppToasts = () => { + const { toasts, remove } = toastCtl + + return ( +
+
+ + {(toast) => ( +
remove(toast.id)} + classList={{ + toast: true, + [`severity-${toast.severity ?? "info"}`]: true, + }} + > + +
+ +
+
+
+ +
+ +
+
+ +
+
{toast.title}
+ + +
{toast.content}
+
+
+
+
+ )} + +
+
+ ) +} diff --git a/y-web/src/app/layout/components/aside-entry.tsx b/y-web/src/app/layout/components/aside-entry.tsx new file mode 100644 index 0000000..8bf68a9 --- /dev/null +++ b/y-web/src/app/layout/components/aside-entry.tsx @@ -0,0 +1,82 @@ +import { For, Show, children, createMemo } from "solid-js" + +import { Link, useLocation } from "@solidjs/router" + +import { Icon } from "@/app/components/common/icon/icon" +import { ComponentWithChildren } from "@/module" + +const ICON_SIZE_NORMAL = 18 +const ICON_SIZE_SMALL = 14 + +export type AsideEnytryProps = { + to: string + + icon: string + title: string + + relatedPaths?: string[] + subEntry?: boolean +} + +export const AsideEntry: ComponentWithChildren = (props) => { + const location = useLocation() + + const subEntries = children(() => props.children).toArray() + + const isActive = createMemo(() => { + if (location.pathname.includes(props.to)) return true + + for (const path of props.relatedPaths ?? []) { + if (location.pathname.includes(path)) return true + } + }) + + const isSelected = createMemo(() => { + if (location.pathname.endsWith(props.to)) return true + for (const path of props.relatedPaths ?? []) { + if (location.pathname.endsWith(path)) return true + } + }) + + return ( + <> + 0, + }} + > +
+
+
+ +
+
{props.title}
+
+
+ +
+ + +
+
+
+ + {(entry) => entry &&
{entry}
} +
+
+
+ + + ) +} diff --git a/y-web/src/app/layout/components/aside-section.tsx b/y-web/src/app/layout/components/aside-section.tsx new file mode 100644 index 0000000..fae1c8a --- /dev/null +++ b/y-web/src/app/layout/components/aside-section.tsx @@ -0,0 +1,20 @@ +import { Show } from "solid-js" + +import { ComponentWithChildren } from "@/module" + +export type AsideSectionProps = { + title?: string +} + +export const AsideSection: ComponentWithChildren = ( + props +) => { + return ( +
+ +
{props.title}
+
+
{props.children}
+
+ ) +} diff --git a/y-web/src/app/layout/components/breadcrumbs.less b/y-web/src/app/layout/components/breadcrumbs.less new file mode 100644 index 0000000..475735f --- /dev/null +++ b/y-web/src/app/layout/components/breadcrumbs.less @@ -0,0 +1,34 @@ +.ui-breadcrumbs { + display: flex; + align-items: center; + gap: 0.33em; + + > .spacer { + display: flex; + align-items: center; + + color: var(--color-text-grey-1); + } +} + +.ui-breadcrumb { + text-decoration: none; + + > .breadcrumb { + display: flex; + + padding: 0.2em 0.4em; + + color: var(--color-text-grey-1); + font-size: var(--text-sm); + font-weight: 500; + + border-radius: 4px; + + transition: 0.15s; + + &:hover { + background-color: var(--color-grey-2); + } + } +} diff --git a/y-web/src/app/layout/components/breadcrumbs.tsx b/y-web/src/app/layout/components/breadcrumbs.tsx new file mode 100644 index 0000000..ec140b1 --- /dev/null +++ b/y-web/src/app/layout/components/breadcrumbs.tsx @@ -0,0 +1,47 @@ +import { For, JSX, Show, children } from "solid-js" + +import { Link } from "@solidjs/router" + +import { Icon } from "@/app/components/common/icon/icon" +import { ComponentWithChildren } from "@/module" + +import "./breadcrumbs.less" + +export type BreadcrumbProps = { + path: string +} + +export const Breadcrumb: ComponentWithChildren = (props) => { + return ( + + + + ) +} + +export type BreadcrumbsProps = { + style?: JSX.CSSProperties +} + +export const Breadcrumbs: ComponentWithChildren = (props) => { + const breadcrumbs = children(() => props.children).toArray() + + return ( +
+ + {(child, index) => ( + <> + +
+ +
+
+
{child}
+ + )} +
+
+ ) +} diff --git a/y-web/src/app/layout/components/domain-selector.less b/y-web/src/app/layout/components/domain-selector.less new file mode 100644 index 0000000..397ac25 --- /dev/null +++ b/y-web/src/app/layout/components/domain-selector.less @@ -0,0 +1,79 @@ +.ui-domain-selector { + .ui-popup-block { + .content { + padding: 0.66em !important; + gap: 0.66em !important; + } + } + + .domain-option { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1.5em; + + padding: 0.25em 0.25em; + + color: var(--color-text); + font-size: var(--text-body); + font-weight: 500; + text-decoration: none; + + border-radius: 4px; + + transition: 0.15s ease-in-out; + + &:hover, + &:focus { + background-color: var(--color-primary-o-1); + color: var(--color-primary-d-1); + } + + &:focus > .ui-icon, + &:hover > .ui-icon { + opacity: 1; + transform: translateX(0) scale(1); + } + + &:focus > .link > .ui-icon, + &:hover > .link > .ui-icon { + background-color: var(--color-primary-t-1); + + font-variation-settings: "FILL" 1, "wght" 500, "GRAD" 25, "opsz" 18 !important; + } + + > .link { + display: flex; + align-items: center; + + gap: 0.75em; + + flex-shrink: 0; + + > .ui-icon { + display: flex; + align-items: center; + justify-content: center; + + padding: 0.2em; + border-radius: 4px; + + transition: 0.15s ease-in-out; + } + } + + > .ui-icon { + display: flex; + align-items: center; + justify-content: center; + + color: var(--color-primary-d-1); + + // Hidden state + opacity: 0; + transform: translateX(-0.25em) scale(0.98); + + transition: 0.15s ease-in-out; + } + } +} diff --git a/y-web/src/app/layout/components/domain-selector.tsx b/y-web/src/app/layout/components/domain-selector.tsx new file mode 100644 index 0000000..b8a734b --- /dev/null +++ b/y-web/src/app/layout/components/domain-selector.tsx @@ -0,0 +1,70 @@ +import { Component, createSignal } from "solid-js" + +import { Link } from "@solidjs/router" + +import { Button } from "@/app/components/common/button/button" +import { Icon } from "@/app/components/common/icon/icon" +import { Popup } from "@/app/components/common/popup/popup" +import { PopupContainer } from "@/app/components/common/popup/popup-container" +import { domainIcon, domainName, useDomain } from "@/app/core/util/use-domain" + +import "./domain-selector.less" + +type DomainLinkProps = { + domain: keyof typeof domainName + to: string + + onHide: () => void +} + +const DomainLink: Component = (props) => { + return ( + + + + + + ) +} + +export const DomainSelector: Component = () => { + const { domain } = useDomain() + + const [popupShown, setPopupShown] = createSignal(false) + + const togglePopup = () => setPopupShown((shown) => !shown) + + return ( +
+ + + + + + + +
+ ) +} diff --git a/y-web/src/app/layout/components/user-island.tsx b/y-web/src/app/layout/components/user-island.tsx new file mode 100644 index 0000000..e9c2d28 --- /dev/null +++ b/y-web/src/app/layout/components/user-island.tsx @@ -0,0 +1,51 @@ +import { Component, Show, createSignal } from "solid-js" + +import { createMutation, useQueryClient } from "@tanstack/solid-query" + +import { Button } from "@/app/components/common/button/button" +import { Popup } from "@/app/components/common/popup/popup" +import { PopupContainer } from "@/app/components/common/popup/popup-container" +import { PopupEntry } from "@/app/components/common/popup/popup-entry" +import { logout } from "@/modules/core/auth/auth.api" +import { authKey, useAuth } from "@/modules/core/auth/auth.service" + +export const UserIsland: Component = () => { + const queryClient = useQueryClient() + + const $auth = useAuth() + const $logout = createMutation(logout) + + const [popupShown, setPopupShown] = createSignal(false) + + const togglePopup = () => setPopupShown((shown) => !shown) + + const performLogout = () => { + // eslint-disable-next-line no-undefined + $logout.mutate(undefined, { + onSuccess: () => { + void queryClient.invalidateQueries(authKey) + }, + }) + } + + return ( + + + + + + + + + ) +} diff --git a/y-web/src/app/routes.tsx b/y-web/src/app/routes.tsx new file mode 100644 index 0000000..10ae6a0 --- /dev/null +++ b/y-web/src/app/routes.tsx @@ -0,0 +1,9 @@ +export const routes = { + "/": "/", + "/login": "/login", + "/admin": "/admin", + "/admin/user-groups": "/admin/user-groups", + "/admin/user-groups/new": "/admin/user-groups/new", + "/admin/users": "/admin/users", + "/admin/users/new": "/admin/users/new", +} as const diff --git a/y-web/src/i18n.ts b/y-web/src/i18n.ts new file mode 100644 index 0000000..6b48564 --- /dev/null +++ b/y-web/src/i18n.ts @@ -0,0 +1,21 @@ +import { createSignal } from "solid-js" + +import { flatten, resolveTemplate, translator } from "@solid-primitives/i18n" + +import * as en from "../i18n/en.json" + +const dict = { + en, +} as const + +type Locale = keyof typeof dict + +export const [locale, setLocale] = createSignal("en") + +// eslint-disable-next-line solid/reactivity +const flatDict = flatten(dict[locale()]) + +export const t = translator(() => flatDict, resolveTemplate) + +// eslint-disable-next-line @typescript-eslint/naming-convention +export const unsafe_t = t as (key: string) => string | undefined diff --git a/y-web/src/index.css b/y-web/src/index.css index de18b41..183ddb7 100644 --- a/y-web/src/index.css +++ b/y-web/src/index.css @@ -2,7 +2,22 @@ font-family: "Inter"; src: url("/assets/fonts/Inter-VariableFont_slnt\,wght.ttf") format("truetype-variations"); - font-weight: 100 900; + font-weight: 1 999; +} + +@font-face { + font-family: "SourceCodePro"; + src: url("/assets/fonts/SourceCodePro-Italic-VariableFont_wght.ttf") + format("truetype-variations"); + font-weight: 1 999; + font-style: italic; +} + +@font-face { + font-family: "SourceCodePro"; + src: url("/assets/fonts/SourceCodePro-VariableFont_wght.ttf") + format("truetype-variations"); + font-weight: 1 999; } body { @@ -17,6 +32,17 @@ body { -moz-osx-font-smoothing: grayscale; } +code { + font-family: "SourceCodePro", monospace; +} + +hr { + margin: 0; + border: 0; + height: 1px; + background: var(--color-border-15); +} + :root { --text-sm: 12px; --text-body: 13px; @@ -25,27 +51,56 @@ body { --text-lg: 16px; --color-text: hsl(0, 0%, 5%); - --color-text-grey-05: hsl(0, 0%, 25%); - --color-text-grey-1: hsl(0, 0%, 50%); + --color-text-grey-025: hsl(234, 22%, 12.5%); + --color-text-grey-05: hsl(234, 20%, 25%); + --color-text-grey-075: hsl(234, 18%, 33%); + --color-text-grey-1: hsl(234, 16%, 50%); + + --color-text-header: hsl(234, 100%, 12%); --color-border-1: hsl(0, 0%, 95%); --color-border-15: hsl(0, 0%, 90%); --color-border-2: hsl(0, 0%, 80%); + --color-border-25: hsl(0, 0%, 70%); --color-border-3: hsl(0, 0%, 60%); --color-border-4: hsl(0, 0%, 40%); + --color-grey-05: hsl(0, 0%, 99%); --color-grey-1: hsl(0, 0%, 98%); --color-grey-15: hsl(0, 0%, 96%); --color-grey-2: hsl(0, 0%, 95%); - --color-primary: #1d1de9; + --color-primary: #0017e6; + --color-primary-l-1: hsl(234, 100%, 50%); + --color-primary-d-1: hsl(234, 100%, 33%); + + --color-primary-d-1-t-1: hsla(234, 100%, 33%, 0.1); + --color-primary-d-1-t-4: hsla(234, 100%, 33%, 0.4); + --color-primary-d-1-t-5: hsla(234, 100%, 33%, 0.5); + + --color-primary-l-1-t-1: hsla(234, 100%, 50%, 0.1); + --color-primary-l-1-t-05: hsla(234, 100%, 50%, 0.05); + --color-primary-l-1-t-025: hsla(234, 100%, 50%, 0.025); + + --color-primary-t-025: rgba(0, 23, 230, 0.025); + --color-primary-t-05: rgba(0, 23, 230, 0.05); + --color-primary-t-1: rgba(0, 23, 230, 0.1); + --color-primary-o-1: rgb(230, 232, 253); + + --color-primary-t-15: rgba(0, 23, 230, 0.15); + --color-primary-t-2: rgba(0, 23, 230, 0.2); + --color-primary-t-5: rgba(0, 23, 230, 0.5); - --color-primary-t-1: rgba(29, 29, 233, 0.1); - --color-primary-o-1: hsl(240deg 84% 95%); + --color-warning: #faf200; + --color-warning-d-1: hsl(36, 80%, 60%); + --color-warning-t-1: rgba(250, 242, 0, 0.1); + --color-warning-t-3: rgba(250, 242, 0, 0.3); - --color-primary-t-2: rgba(29, 29, 233, 0.2); + --color-error: #fc0015; + --color-error-o-1: rgb(255, 230, 232); + --color-error-o-2: rgb(254, 204, 208); - --color-error: #e91c1c; - --color-error-o-1: hsl(0, 82%, 95%); - --color-error-o-2: hsl(0, 82%, 90%); + --color-success: #22cc00; + --color-success-t-1: rgba(34, 204, 0, 0.1); + --color-success-t-3: rgba(34, 204, 0, 0.1); } diff --git a/y-web/src/modules/admin/components/user-group/components/groups-option-field.tsx b/y-web/src/modules/admin/components/user-group/components/groups-option-field.tsx new file mode 100644 index 0000000..53bed0e --- /dev/null +++ b/y-web/src/modules/admin/components/user-group/components/groups-option-field.tsx @@ -0,0 +1,49 @@ +import { Component, createMemo } from "solid-js" + +import { SelectField } from "@/app/components/common/select-field/select-field" +import { Form } from "@/app/core/use-form" +import { unsafe_t } from "@/i18n" +import { + UserGroupFieldValues, + UserGroupWatchedFields, +} from "@/modules/admin/pages/user-groups/[groupId]" +import { useUserGroups } from "@/modules/admin/user-groups/user-groups.service" +import { + IUserRight, + IUserRightOption, +} from "@/modules/admin/user-rights/user-rights.codecs" + +export type GroupsOptionFieldProps = { + right: IUserRight + option: IUserRightOption + form: Form + + disabled?: boolean +} + +export const UserGroupSelectField: Component = ( + props +) => { + const $userGroups = useUserGroups(() => ({})) + + const options = createMemo(() => $userGroups.data?.user_groups ?? []) + + // !TODO registerControlled's output is typed as any for now. + return ( + + ) +} diff --git a/y-web/src/modules/admin/components/user-group/user-group-right-category.tsx b/y-web/src/modules/admin/components/user-group/user-group-right-category.tsx new file mode 100644 index 0000000..582802c --- /dev/null +++ b/y-web/src/modules/admin/components/user-group/user-group-right-category.tsx @@ -0,0 +1,52 @@ +import { Card } from "@/app/components/common/card/card" +import { Stack } from "@/app/components/common/stack/stack" +import { Text } from "@/app/components/common/text/text" +import { unsafe_t } from "@/i18n" +import { ComponentWithChildren } from "@/module" + +import { IUserRightCategory } from "../../user-rights/user-rights.codecs" + +export type UserGroupRightCategoryProps = { + category: IUserRightCategory +} + +export const UserGroupRightCategory: ComponentWithChildren< + UserGroupRightCategoryProps +> = (props) => { + return ( + + + + + {unsafe_t(`main.userRightCategory.${props.category.name}.name`)} + + + {unsafe_t( + `main.userRightCategory.${props.category.name}.description` + )} + + + {props.children} + + + ) +} diff --git a/y-web/src/modules/admin/components/user-group/user-group-right-option.tsx b/y-web/src/modules/admin/components/user-group/user-group-right-option.tsx new file mode 100644 index 0000000..fca8035 --- /dev/null +++ b/y-web/src/modules/admin/components/user-group/user-group-right-option.tsx @@ -0,0 +1,57 @@ +import { Component, Match, Switch } from "solid-js" + +import { Checkbox } from "@/app/components/common/checkbox/checkbox" +import { Stack } from "@/app/components/common/stack/stack" +import { Text } from "@/app/components/common/text/text" +import { Form } from "@/app/core/use-form" +import { unsafe_t } from "@/i18n" + +import { + UserGroupFieldValues, + UserGroupWatchedFields, +} from "../../pages/user-groups/[groupId]" +import { + IUserRight, + IUserRightOption, +} from "../../user-rights/user-rights.codecs" +import { UserGroupSelectField } from "./components/groups-option-field" + +export type UserGroupRightOptionProps = { + right: IUserRight + option: IUserRightOption + form: Form + + disabled?: boolean +} + +export const UserGroupRightOption: Component = ( + props +) => { + return ( + + + + + + {unsafe_t( + `main.userRightOption.${props.right.name}.${props.option.name}.description` + )} + + + + + + + + + + + + ) +} diff --git a/y-web/src/modules/admin/components/user-group/user-group-right-tag.less b/y-web/src/modules/admin/components/user-group/user-group-right-tag.less new file mode 100644 index 0000000..0085934 --- /dev/null +++ b/y-web/src/modules/admin/components/user-group/user-group-right-tag.less @@ -0,0 +1,27 @@ +.user-group-right-tag { + display: flex; + align-items: center; + gap: 0.5em; + + padding: 0.4em 0.8em; + + cursor: help; + + border-radius: 5px; + + background-color: var(--color-grey-1); + + &.dangerous { + background-color: var(--color-error-o-1); + color: var(--color-error); + } + + &.administrative { + background-color: var(--color-error-o-1); + color: var(--color-error); + } + &.inherited { + background-color: var(--color-primary-t-05); + color: var(--color-primary); + } +} diff --git a/y-web/src/modules/admin/components/user-group/user-group-right-tag.tsx b/y-web/src/modules/admin/components/user-group/user-group-right-tag.tsx new file mode 100644 index 0000000..40c3766 --- /dev/null +++ b/y-web/src/modules/admin/components/user-group/user-group-right-tag.tsx @@ -0,0 +1,38 @@ +import { Component, Match, Switch } from "solid-js" + +import { Icon } from "@/app/components/common/icon/icon" +import { Text } from "@/app/components/common/text/text" +import { t } from "@/i18n" + +import { IUserRightTag } from "../../user-rights/user-rights.codecs" +import "./user-group-right-tag.less" + +export type UserGroupRightTagProps = { + tag: IUserRightTag | "inherited" +} + +export const UserGroupRightTag: Component = (props) => { + return ( +
+ + + + Dangerous + + + + Administrative + + + + + +
+ ) +} diff --git a/y-web/src/modules/admin/components/user-group/user-group-right.less b/y-web/src/modules/admin/components/user-group/user-group-right.less new file mode 100644 index 0000000..787a963 --- /dev/null +++ b/y-web/src/modules/admin/components/user-group/user-group-right.less @@ -0,0 +1,7 @@ +.user-group-right-options-section-left-floater { + width: 3px; + + border-radius: 3px; + + background-color: var(--color-border-15); +} diff --git a/y-web/src/modules/admin/components/user-group/user-group-right.tsx b/y-web/src/modules/admin/components/user-group/user-group-right.tsx new file mode 100644 index 0000000..113855b --- /dev/null +++ b/y-web/src/modules/admin/components/user-group/user-group-right.tsx @@ -0,0 +1,99 @@ +import { Component, For, Show } from "solid-js" + +import { Checkbox } from "@/app/components/common/checkbox/checkbox" +import { Stack } from "@/app/components/common/stack/stack" +import { Text } from "@/app/components/common/text/text" +import { Form } from "@/app/core/use-form" +import { unsafe_t } from "@/i18n" + +import { + UserGroupFieldValues, + UserGroupWatchedFields, +} from "../../pages/user-groups/[groupId]" +import { IUserRight } from "../../user-rights/user-rights.codecs" +import { UserGroupRightOption } from "./user-group-right-option" +import { UserGroupRightTag } from "./user-group-right-tag" +import "./user-group-right.less" + +export type UserGroupRightProps = { + right: IUserRight + form: Form + + disabled?: boolean +} + +export const UserGroupRight: Component = (props) => { + // eslint-disable-next-line solid/reactivity + const isAssigned = props.form.watch( + // eslint-disable-next-line solid/reactivity + `right:${props.right.name}` + ) + + return ( + + + + + + {unsafe_t(`main.userRight.${props.right.name}.name`)} + + + + + {(tag) => } + + + + + {unsafe_t(`main.userRight.${props.right.name}.description`)} + + 0}> + +
+ + + Right options + + + {(option) => ( + + )} + + + + + + ) +} diff --git a/y-web/src/modules/admin/components/user-groups-lits.tsx b/y-web/src/modules/admin/components/user-groups-lits.tsx new file mode 100644 index 0000000..97b98a3 --- /dev/null +++ b/y-web/src/modules/admin/components/user-groups-lits.tsx @@ -0,0 +1,177 @@ +import { Component, For, Match, Show, Switch, createMemo } from "solid-js" + +import { Icon } from "@/app/components/common/icon/icon" +import { InputField } from "@/app/components/common/input-field/input-field" +import { Note } from "@/app/components/common/note/note" +import { Pill } from "@/app/components/common/pill/pill" +import { Stack } from "@/app/components/common/stack/stack" +import { Text } from "@/app/components/common/text/text" +import { ListPageSwitcher } from "@/app/components/list-page-switcher/list-page-switcher" +import { ListEntryLink } from "@/app/components/list/components/list-entry-link" +import { + List, + ListEntries, + ListFooter, + ListHead, +} from "@/app/components/list/list" +import { useTableState } from "@/app/core/use-table-state" + +import { IUserGroupRow, userGroupType } from "../user-groups/user-groups.codecs" +import { useUserGroups } from "../user-groups/user-groups.service" + +export type UserGroupEntryProps = { + group: IUserGroupRow + onSelect: (entry: number) => void + selected: boolean +} + +const UserGroupEntry: Component = (props) => { + return ( +
+
+ + + {props.group.name} + + + + + + + + + + System group + + + + + + + + + + System group + + + + + + + + +
+
+ ) +} + +export const UserGroupsList: Component = () => { + const tableState = useTableState({ + defaultRowsPerPage: 500, + }) + + const $userGroups = useUserGroups( + () => ({ + search: tableState.search(), + limit: tableState.rowsPerPage(), + orderBy: tableState.orderBy(), + skip: tableState.skip(), + }), + { + refetchOnWindowFocus: true, + } + ) + + const userGroups = createMemo( + () => + // eslint-disable-next-line no-confusing-arrow + $userGroups.data?.user_groups.sort((group) => + group.group_type === null ? 1 : -1 + ) ?? [] + ) + + return ( + + + + + tableState.setSearch(event.currentTarget.value), + }} + /> + + + + + No user groups found. Try changing your search query. + + + + + + {(group) => ( + + )} + + + + tableState.rowsPerPage()} + > + + + + + + + ) +} diff --git a/y-web/src/modules/admin/components/user/update-user-password-modal.tsx b/y-web/src/modules/admin/components/user/update-user-password-modal.tsx new file mode 100644 index 0000000..3d1e11a --- /dev/null +++ b/y-web/src/modules/admin/components/user/update-user-password-modal.tsx @@ -0,0 +1,152 @@ +import { Component } from "solid-js" + +import { createMutation } from "@tanstack/solid-query" + +import { Button } from "@/app/components/common/button/button" +import { Icon } from "@/app/components/common/icon/icon" +import { InputField } from "@/app/components/common/input-field/input-field" +import { Modal } from "@/app/components/common/modal/modal" +import { Stack } from "@/app/components/common/stack/stack" +import { Text } from "@/app/components/common/text/text" +import { toastCtl } from "@/app/core/toast" +import { useForm } from "@/app/core/use-form" +import { genericErrorToast } from "@/app/core/util/toast-utils" +import { updateUserPassword } from "@/modules/admin/users/users.api" + +import { IUser } from "../../users/users.codecs" + +export type AdminUpdateUserPasswordFormValues = { + newPassword: string +} + +export type AdminUpdateUserPasswordModalProps = { + defaultValues?: AdminUpdateUserPasswordFormValues + + user: IUser | null + open: boolean + onClose: () => void +} + +export const AdminUpdateUserPasswordModal: Component< + AdminUpdateUserPasswordModalProps +> = (props) => { + const { notify } = toastCtl + + const $updatePassword = createMutation(updateUserPassword) + + const form = useForm({ + defaultValues: { + newPassword: "", + + ...props.defaultValues, + }, + disabled: () => $updatePassword.isLoading, + onSubmit: (values) => { + if (!props.user) return + + $updatePassword.mutate( + { + userId: props.user.id, + password: values.newPassword, + }, + { + onSuccess: () => { + notify({ + title: "Password updated", + content: "User's password was changed", + severity: "success", + icon: "check", + }) + + props.onClose() + }, + onError: (error) => genericErrorToast(error), + } + ) + }, + }) + + const { register, submit, errors } = form + + return ( + +
+ +
+ + + Update password + + + {props.user?.username} + + + + } + > + + + New password will not be sent to the user, don't forget to write it + down. + + +
+ + + + + + + + + +
+
+ ) +} diff --git a/y-web/src/modules/admin/components/users-lits.tsx b/y-web/src/modules/admin/components/users-lits.tsx new file mode 100644 index 0000000..a407ce3 --- /dev/null +++ b/y-web/src/modules/admin/components/users-lits.tsx @@ -0,0 +1,328 @@ +import { Component, For, Show, createMemo, createSignal } from "solid-js" + +import { createMutation, useQueryClient } from "@tanstack/solid-query" +import { format } from "date-fns" + +import { Button } from "@/app/components/common/button/button" +import { ExpandButton } from "@/app/components/common/expand-button/expand-button" +import { + ExpandButtonEntries, + ExpandButtonEntry, +} from "@/app/components/common/expand-button/expand-button-entry" +import { Icon } from "@/app/components/common/icon/icon" +import { InputField } from "@/app/components/common/input-field/input-field" +import { Modal } from "@/app/components/common/modal/modal" +import { Note } from "@/app/components/common/note/note" +import { Stack } from "@/app/components/common/stack/stack" +import { Text } from "@/app/components/common/text/text" +import { ListPageSwitcher } from "@/app/components/list-page-switcher/list-page-switcher" +import { ListEntryLink } from "@/app/components/list/components/list-entry-link" +import { + List, + ListEntries, + ListFooter, + ListHead, +} from "@/app/components/list/list" +import { toastCtl } from "@/app/core/toast" +import { useTableState } from "@/app/core/use-table-state" +import { genericErrorToast } from "@/app/core/util/toast-utils" +import { routes } from "@/app/routes" +import { IUser } from "@/modules/admin/users/users.codecs" +import { useUsers, usersKey } from "@/modules/admin/users/users.service" +import { useAuth } from "@/modules/core/auth/auth.service" + +import { deleteUsers } from "../users/users.api" + +export type UserEntryProps = { + user: IUser + onSelect: (entry: number) => void + selected: boolean +} + +const UserEntry: Component = (props) => { + return ( +
+
+ props.onSelect(props.user.id)} + /> +
+ + {props.user.username} + + + Joined {format(new Date(props.user.created_at), "dd.MM.yyyy")} + +
+
+
+ ) +} + +export const UsersList: Component = () => { + const $auth = useAuth() + const queryClient = useQueryClient() + const { notify } = toastCtl + + const tableState = useTableState({ + defaultRowsPerPage: 25, + }) + + const $users = useUsers( + () => ({ + search: tableState.search(), + limit: tableState.rowsPerPage(), + orderBy: tableState.orderBy(), + skip: tableState.skip(), + }), + { + refetchInterval: 60_000, + refetchOnWindowFocus: true, + } + ) + + const $deleteUsers = createMutation(deleteUsers) + + const deleteActionAllowed = createMemo( + () => + $auth.data?.user_rights.some( + (right) => right.right_name === "delete_user" + ) ?? false + ) + + const users = createMemo(() => $users.data?.users ?? []) + + const noneSelected = createMemo(() => tableState.selectedEntries().size === 0) + + const [deleteConfirmationModalOpen, setDeleteConfirmationModalOpen] = + createSignal(false) + + return ( + <> + setDeleteConfirmationModalOpen(false)} + style={{ + "max-width": "450px", + }} + header={ + +
+ +
+ + + Confirm action + + +
+ } + > + + + Are you sure you want to delete {tableState.selectedEntries().size}{" "} + user(s)? + + + + This action is irreversible. All data associated with the users will + be deleted forever. + + + + + + + +
+ + + +
+ + + + tableState.setSelectedEntries( + // eslint-disable-next-line solid/reactivity + () => new Set(users().map((group) => group.id)) + ) + } + icon="select_all" + > + Select all + + } + > + + tableState.setSelectedEntries(() => new Set()) + } + icon="select" + > + Deselect + + +
+ { + setDeleteConfirmationModalOpen(true) + }} + > + Delete + +
+
+
+
+ + tableState.setSearch(event.currentTarget.value), + }} + /> +
+
+ + + + No users found. Try changing your search query. + + + + + + {(user) => ( + + )} + + + + + + +
+
+ + ) +} diff --git a/y-web/src/modules/admin/layout/admin-layout.tsx b/y-web/src/modules/admin/layout/admin-layout.tsx new file mode 100644 index 0000000..338e157 --- /dev/null +++ b/y-web/src/modules/admin/layout/admin-layout.tsx @@ -0,0 +1,67 @@ +import { Component, lazy } from "solid-js" + +import { Route, Routes } from "@solidjs/router" + +import { AppAside } from "@/app/layout/app-aside" +import { AppContent } from "@/app/layout/app-content" +import { AsideEntry } from "@/app/layout/components/aside-entry" +import { AsideSection } from "@/app/layout/components/aside-section" + +import UserPage from "../pages/users/[userId]" + +const UsersListPage = lazy( + async () => import("@/modules/admin/pages/users-list") +) + +const NewUserPage = lazy(async () => import("@/modules/admin/pages/users/new")) + +const UserGroupsPage = lazy( + async () => import("@/modules/admin/pages/user-groups") +) + +const UserGroupPage = lazy( + async () => import("@/modules/admin/pages/user-groups/[groupId]") +) + +const NewUserGroupPage = lazy( + async () => import("@/modules/admin/pages/user-groups/new") +) + +const AdminLayout: Component = () => { + return ( + <> + + + + + + + + + + + + + + + + + + + + + + ) +} + +export default AdminLayout diff --git a/y-web/src/modules/admin/pages/user-groups/[groupId]/index.tsx b/y-web/src/modules/admin/pages/user-groups/[groupId]/index.tsx new file mode 100644 index 0000000..652366b --- /dev/null +++ b/y-web/src/modules/admin/pages/user-groups/[groupId]/index.tsx @@ -0,0 +1,552 @@ +import { + Component, + For, + Match, + Show, + Switch, + createEffect, + createMemo, + createSignal, + untrack, +} from "solid-js" + +import { useNavigate, useParams } from "@solidjs/router" +import { createMutation, useQueryClient } from "@tanstack/solid-query" + +import { Button } from "@/app/components/common/button/button" +import { Card } from "@/app/components/common/card/card" +import { ExpandButton } from "@/app/components/common/expand-button/expand-button" +import { + ExpandButtonEntries, + ExpandButtonEntry, +} from "@/app/components/common/expand-button/expand-button-entry" +import { Icon } from "@/app/components/common/icon/icon" +import { + KeyValue, + KeyValueFields, +} from "@/app/components/common/key-value/key-value" +import { Container } from "@/app/components/common/layout/container" +import { Modal } from "@/app/components/common/modal/modal" +import { Pill } from "@/app/components/common/pill/pill" +import { Stack } from "@/app/components/common/stack/stack" +import { Text } from "@/app/components/common/text/text" +import { toastCtl } from "@/app/core/toast" +import { useForm } from "@/app/core/use-form" +import { genericErrorToast } from "@/app/core/util/toast-utils" +import { Breadcrumb, Breadcrumbs } from "@/app/layout/components/breadcrumbs" +import { routes } from "@/app/routes" +import { + UpdateUserGroupInput, + deleteUserGroup, + updateUserGroup, +} from "@/modules/admin/user-groups/user-groups.api" +import { + IUserGroupRightOptionValue, + userGroupType, +} from "@/modules/admin/user-groups/user-groups.codecs" +import { + useUserGroup, + userGroupKey, + userGroupsKey, +} from "@/modules/admin/user-groups/user-groups.service" +import { useUserRights } from "@/modules/admin/user-rights/user-rights.service" +import { useAuth } from "@/modules/core/auth/auth.service" + +import { UserGroupRight } from "../../../components/user-group/user-group-right" +import { UserGroupRightCategory } from "../../../components/user-group/user-group-right-category" + +export type UserGroupFieldValues = { + [K in `right:${string}`]: boolean +} & { [K in `right_option:${string}`]: IUserGroupRightOptionValue } + +export type UserGroupWatchedFields = ["right:*", "right_option:*"] + +const USER_GROUPS_ROUTE = routes["/admin/user-groups"] + +const UserGroupPage: Component = () => { + const $auth = useAuth() + + const params = useParams() + const navigate = useNavigate() + const queryClient = useQueryClient() + const { notify } = toastCtl + + let formRef: HTMLFormElement | undefined + + const groupManagementPermissions = createMemo(() => { + let groupManagementAllowed = false + let groupDeletionAllowed = false + + for (const right of $auth.data?.user_rights ?? []) { + if (right.right_name === "manage_user_groups") { + groupManagementAllowed = true + + if ( + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + (right.right_options?.["allow_deleting_user_groups"] as unknown) === + true + ) { + groupDeletionAllowed = true + break + } + } + } + + return { groupManagementAllowed, groupDeletionAllowed } + }) + + const [updateConfirmationModalOpen, setUpdateConfirmationModalOpen] = + createSignal(false) + const [deleteConfirmationModalOpen, setDeleteConfirmationModalOpen] = + createSignal(false) + + const $updateUserGroup = createMutation(updateUserGroup) + const $deleteUserGroup = createMutation(deleteUserGroup) + + const $userRights = useUserRights() + const $userGroup = useUserGroup(() => ({ + userGroupId: Number.parseInt(params.groupId!, 10), + })) + + const userRights = createMemo( + () => + $userRights.data ?? { + categories: [], + } + ) + + createEffect(() => { + if ($userGroup.isError) { + genericErrorToast($userGroup.error) + navigate(USER_GROUPS_ROUTE) + } + }) + + const onSubmit = (values: UserGroupFieldValues) => { + if (!$userGroup.data?.id) return + + const rights: { + [key: string]: { + granted: boolean + options: Record + } + } = {} + + // ! TODO add support for non-flat FieldValues + for (const [field, value] of Object.entries(values)) { + if (field.startsWith("right:")) { + rights[field.replace("right:", "")] = { + granted: value as boolean, + options: {}, + } + // eslint-disable-next-line sonarjs/elseif-without-else + } else if (field.startsWith("right_option:")) { + const parts = field.split(":") + + const rightName = parts[1] as string + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + const optionName = parts[2] as string + + if (rights[rightName]) { + rights[rightName]!.options[optionName] = value + } + } + } + + void $updateUserGroup.mutate( + { + userGroupId: $userGroup.data.id, + rights, + }, + { + onSuccess: () => { + notify({ + title: "Changes saved", + content: "User group was updated", + severity: "success", + icon: "check", + }) + + void queryClient.invalidateQueries([userGroupsKey]) + void queryClient.invalidateQueries([userGroupKey, $userGroup.data.id]) + }, + onError: (error) => genericErrorToast(error), + onSettled: () => { + setUpdateConfirmationModalOpen(false) + }, + } + ) + } + + const form = useForm({ + defaultValues: {}, + disabled: () => + !$userGroup.data || !$userRights.data || $updateUserGroup.isLoading, + onSubmit, + watch: ["right:*", "right_option:*"], + }) + + const { setValue, submit } = form + + // ! TODO implement form.reset() + createEffect(() => { + if ($userGroup.data && $userRights.data) { + for (const right of $userGroup.data.rights) { + untrack(() => setValue(`right:${right.right_name}`, () => true)) + + for (const [name, value] of Object.entries(right.right_options)) { + untrack(() => + setValue(`right_option:${right.right_name}:${name}`, () => value) + ) + } + } + } + }) + + const saveKeyValue = (key: keyof UpdateUserGroupInput, value: string) => { + if (!$userGroup.data) return + + void $updateUserGroup.mutate( + { + userGroupId: $userGroup.data.id, + [key]: value, + }, + { + onSuccess: () => { + void queryClient.invalidateQueries([userGroupsKey]) + void queryClient.invalidateQueries([userGroupKey, $userGroup.data.id]) + }, + onError: (error) => genericErrorToast(error), + } + ) + } + + return ( + <> + setUpdateConfirmationModalOpen(false)} + style={{ + "max-width": "450px", + }} + header={ + +
+ +
+ + + Confirm changes + + +
+ } + > + + + Are you sure you want to commit your changes to this group? + + + + All users that are assigned to this group will be affected + immediately. + + + + + + + +
+ setDeleteConfirmationModalOpen(false)} + style={{ + "max-width": "450px", + }} + header={ + +
+ +
+ + + Confirm group deletion + + +
+ } + > + + Are you sure you want to delete this user group? + + + All users that are assigned to this group will unsassigned + automatically. + + + + + + + +
+ + + + Administration + Groups + + {$userGroup.data!.name} + + + + + + + + + {$userGroup.data?.name ?? ""} + + + + + + + + System group + + + + + + + + + + System group + + + + + + + + + + + setDeleteConfirmationModalOpen(true)} + > + Delete + + + + + + + + General Information + + + saveKeyValue("name", value)} + /> + + + + Group Rights + +
+ + + {(category) => ( + + + + {(right) => ( + <> +
+ + + )} +
+
+
+ )} +
+ + + + + + + + + + + +
+
+
+
+
+ + ) +} + +export default UserGroupPage diff --git a/y-web/src/modules/admin/pages/user-groups/index.tsx b/y-web/src/modules/admin/pages/user-groups/index.tsx new file mode 100644 index 0000000..3348044 --- /dev/null +++ b/y-web/src/modules/admin/pages/user-groups/index.tsx @@ -0,0 +1,69 @@ +import { Component, Show, createMemo } from "solid-js" + +import { useNavigate } from "@solidjs/router" + +import { Button } from "@/app/components/common/button/button" +import { Card } from "@/app/components/common/card/card" +import { Container } from "@/app/components/common/layout/container" +import { Stack } from "@/app/components/common/stack/stack" +import { Text } from "@/app/components/common/text/text" +import { Breadcrumb, Breadcrumbs } from "@/app/layout/components/breadcrumbs" +import { routes } from "@/app/routes" +import { useAuth } from "@/modules/core/auth/auth.service" + +import { UserGroupsList } from "../../components/user-groups-lits" + +const UserGroupsListPage: Component = () => { + const $auth = useAuth() + const navigate = useNavigate() + + const groupCreationAllowed = createMemo(() => + $auth.data?.user_rights.some( + (right) => + right.right_name === "manage_user_groups" && + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + (right.right_options["allow_creating_user_groups"] as unknown) === true + ) + ) + + return ( + + + + Administration + Groups + + + + + + + + + + New user group + + Create a new user group. + + + + + + + + ) +} + +export default UserGroupsListPage diff --git a/y-web/src/modules/admin/pages/user-groups/new/index.tsx b/y-web/src/modules/admin/pages/user-groups/new/index.tsx new file mode 100644 index 0000000..7abd445 --- /dev/null +++ b/y-web/src/modules/admin/pages/user-groups/new/index.tsx @@ -0,0 +1,138 @@ +import { Component, createEffect } from "solid-js" + +import { useNavigate } from "@solidjs/router" +import { createMutation, useQueryClient } from "@tanstack/solid-query" + +import { Button } from "@/app/components/common/button/button" +import { Icon } from "@/app/components/common/icon/icon" +import { InputField } from "@/app/components/common/input-field/input-field" +import { Container } from "@/app/components/common/layout/container" +import { Stack } from "@/app/components/common/stack/stack" +import { Text } from "@/app/components/common/text/text" +import { toastCtl } from "@/app/core/toast" +import { useForm } from "@/app/core/use-form" +import { genericErrorToast } from "@/app/core/util/toast-utils" +import { Breadcrumb, Breadcrumbs } from "@/app/layout/components/breadcrumbs" +import { routes } from "@/app/routes" +import { createUserGroup } from "@/modules/admin/user-groups/user-groups.api" +import { userGroupsKey } from "@/modules/admin/user-groups/user-groups.service" +import { useAuth } from "@/modules/core/auth/auth.service" + +const USER_GROUPS_ROUTE = routes["/admin/user-groups"] + +const NewUserGroupPage: Component = () => { + const $auth = useAuth() + + const navigate = useNavigate() + const queryClient = useQueryClient() + const { notify } = toastCtl + + createEffect(() => { + const groupCreationAllowed = $auth.data?.user_rights.some( + (right) => + right.right_name === "manage_user_groups" && + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + (right.right_options["allow_creating_user_groups"] as unknown) === true + ) + + if (!groupCreationAllowed) { + navigate(USER_GROUPS_ROUTE) + } + }) + const $createUserGroup = createMutation(createUserGroup) + + const form = useForm({ + defaultValues: { + name: "", + }, + disabled: () => $createUserGroup.isLoading, + onSubmit: (values) => { + $createUserGroup.mutate( + { + name: values.name, + }, + { + onSuccess: (response) => { + notify({ + title: "Group created", + content: "User group was created", + severity: "success", + icon: "check", + }) + + void queryClient.invalidateQueries([userGroupsKey]) + navigate(`/admin/user-groups/${response.id}`) + }, + onError: (error) => genericErrorToast(error), + } + ) + }, + }) + + const { register, submit, errors } = form + + return ( + + + Administration + Groups + new + + + + + + + Create a new user group + + + You will be able to set up user rights later. + + + +
+ + + + + + + +
+
+
+ ) +} + +export default NewUserGroupPage diff --git a/y-web/src/modules/admin/pages/users-list.tsx b/y-web/src/modules/admin/pages/users-list.tsx new file mode 100644 index 0000000..aaabb33 --- /dev/null +++ b/y-web/src/modules/admin/pages/users-list.tsx @@ -0,0 +1,82 @@ +import { Component, Show, createMemo } from "solid-js" + +import { useNavigate } from "@solidjs/router" + +import { Button } from "@/app/components/common/button/button" +import { Card } from "@/app/components/common/card/card" +import { Container } from "@/app/components/common/layout/container" +import { Stack } from "@/app/components/common/stack/stack" +import { Text } from "@/app/components/common/text/text" +import { Breadcrumb, Breadcrumbs } from "@/app/layout/components/breadcrumbs" +import { routes } from "@/app/routes" +import { useAuth } from "@/modules/core/auth/auth.service" + +import { UsersList } from "../components/users-lits" + +const UsersListPage: Component = () => { + const $auth = useAuth() + const navigate = useNavigate() + + const userCreationAllowed = createMemo(() => + $auth.data?.user_rights.some( + (right) => right.right_name === "create_account" + ) + ) + + return ( + + + + Administration + Users + + + + + + +
+
+ + New user + + + Manually create a new user account. You can set a temporary + password. + +
+ +
+
+
+
+
+ ) +} + +export default UsersListPage diff --git a/y-web/src/modules/admin/pages/users/[userId]/general/index.tsx b/y-web/src/modules/admin/pages/users/[userId]/general/index.tsx new file mode 100644 index 0000000..a3a56b7 --- /dev/null +++ b/y-web/src/modules/admin/pages/users/[userId]/general/index.tsx @@ -0,0 +1,174 @@ +import { Component, Show, createMemo, createSignal } from "solid-js" + +import { useNavigate } from "@solidjs/router" +import { createMutation, useQueryClient } from "@tanstack/solid-query" + +import { Button } from "@/app/components/common/button/button" +import { Card } from "@/app/components/common/card/card" +import { Icon } from "@/app/components/common/icon/icon" +import { Modal } from "@/app/components/common/modal/modal" +import { Stack } from "@/app/components/common/stack/stack" +import { Text } from "@/app/components/common/text/text" +import { toastCtl } from "@/app/core/toast" +import { genericErrorToast } from "@/app/core/util/toast-utils" +import { routes } from "@/app/routes" +import { AdminUpdateUserPasswordModal } from "@/modules/admin/components/user/update-user-password-modal" +import { IUser } from "@/modules/admin/user/user.codecs" +import { deleteUsers } from "@/modules/admin/users/users.api" +import { usersKey } from "@/modules/admin/users/users.service" +import { useAuth } from "@/modules/core/auth/auth.service" + +export type UserGeneralSubpageProps = { + user: IUser +} + +const UserGeneralSubpage: Component = (props) => { + const $auth = useAuth() + const queryClient = useQueryClient() + const navigate = useNavigate() + const { notify } = toastCtl + + const $deleteUsers = createMutation(deleteUsers) + + const passwordUpdateAllowed = createMemo( + () => + $auth.data?.user_rights.some( + (right) => right.right_name === "change_user_password" + ) ?? false + ) + + const deleteAllowed = createMemo( + () => + $auth.data?.user_rights.some( + (right) => right.right_name === "delete_user" + ) ?? false + ) + + const [updatePasswordModalOpen, setUpdatePasswordModalOpen] = + createSignal(false) + + const [deleteModalOpen, setDeleteModalOpen] = createSignal(false) + + return ( + <> + setDeleteModalOpen(false)} + style={{ + "max-width": "450px", + }} + header={ + +
+ +
+ + + Delete user + + + {props.user.username} + + +
+ } + > + + Are you sure you want to delete this user? + + + This action is irreversible. All data associated with the user will + be deleted forever. + + + + + + + +
+ + setUpdatePasswordModalOpen(false)} + /> + + + + + + + + + + + + + + + + + ) +} + +export default UserGeneralSubpage diff --git a/y-web/src/modules/admin/pages/users/[userId]/groups/index.tsx b/y-web/src/modules/admin/pages/users/[userId]/groups/index.tsx new file mode 100644 index 0000000..fec1faf --- /dev/null +++ b/y-web/src/modules/admin/pages/users/[userId]/groups/index.tsx @@ -0,0 +1,245 @@ +import { Component, For, createMemo, createSignal } from "solid-js" + +import { useParams } from "@solidjs/router" +import { createMutation, useQueryClient } from "@tanstack/solid-query" + +import { Button } from "@/app/components/common/button/button" +import { Card } from "@/app/components/common/card/card" +import { Checkbox } from "@/app/components/common/checkbox/checkbox" +import { Icon } from "@/app/components/common/icon/icon" +import { Link } from "@/app/components/common/link/link" +import { Modal } from "@/app/components/common/modal/modal" +import { Stack } from "@/app/components/common/stack/stack" +import { Text } from "@/app/components/common/text/text" +import { toastCtl } from "@/app/core/toast" +import { genericErrorToast } from "@/app/core/util/toast-utils" +import { routes } from "@/app/routes" +import { useUserGroups } from "@/modules/admin/user-groups/user-groups.service" +import { updateUserGroupMembership } from "@/modules/admin/user/user.api" +import { IUser } from "@/modules/admin/user/user.codecs" +import { useAuth } from "@/modules/core/auth/auth.service" + +import "./user-groups-subpage.less" + +export type UserGroupsSubpageProps = { + user: IUser +} + +const UserGroupsSubpage: Component = (props) => { + const $auth = useAuth() + + const params = useParams() + const queryClient = useQueryClient() + const { notify } = toastCtl + + const $updateUserGroupMembership = createMutation(updateUserGroupMembership) + + const clientPermissions = createMemo(() => { + let allowedGroups: number[] = [] + let anyGroupIsAllowed = false + + for (const right of $auth.data?.user_rights ?? []) { + if (right.right_name === "assign_user_groups") { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (right.right_options["allow_assigning_any_group"] === true) { + anyGroupIsAllowed = true + } + + const allowedGroupsIds = + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + right.right_options?.["assignable_user_groups"] as + | number[] + | undefined + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (allowedGroupsIds && Array.isArray(allowedGroupsIds)) { + allowedGroups = [...allowedGroups, ...allowedGroupsIds] + } + } + } + + return { allowedGroups, anyGroupIsAllowed } + }) + + const $userGroups = useUserGroups(() => ({})) + const userGroups = createMemo( + () => + $userGroups.data?.user_groups.filter((group) => !group.group_type) ?? [] + ) + + const [confirmationModalOpen, setConfirmationModalOpen] = createSignal(false) + const [selectedGroups, setSelectedGroups] = createSignal( + // eslint-disable-next-line solid/reactivity + props.user.user_groups + ) + + const updateSelection = (groupId: number, isSelected: boolean) => { + if (isSelected) { + setSelectedGroups((groups) => [...groups, groupId]) + } else { + setSelectedGroups((groups) => groups.filter((id) => id !== groupId)) + } + } + + const saveGroups = () => { + if (params.userId) { + $updateUserGroupMembership.mutate( + { + userId: Number.parseInt(params.userId, 10), + userGroups: selectedGroups(), + }, + { + onSettled: () => { + setConfirmationModalOpen(false) + }, + onSuccess: () => { + notify({ + title: "Changes saved", + content: "User's group membership was updated", + severity: "success", + icon: "check", + }) + + void queryClient.invalidateQueries(["user", params.userId]) + }, + onError: (error) => genericErrorToast(error), + } + ) + } + } + + return ( + <> + setConfirmationModalOpen(false)} + style={{ + "max-width": "450px", + }} + header={ + +
+ +
+ + + Confirm changes + + +
+ } + > + + + Are you sure you want to commit your changes to this user's groups? + + + + + + + +
+ + + + {(userGroup) => { + const selected = createMemo(() => + selectedGroups().includes(userGroup.id) + ) + + const isMutable = createMemo( + () => + clientPermissions().anyGroupIsAllowed || + clientPermissions().allowedGroups.includes(userGroup.id) + ) + + return ( +
+ + updateSelection(userGroup.id, checked) + } + disabled={!isMutable()} + /> + + {userGroup.name} + +
+ ) + }} +
+
+ + + + + User's permissions will be updated immediately after saving. + + + + +
+ + ) +} + +export default UserGroupsSubpage diff --git a/y-web/src/modules/admin/pages/users/[userId]/groups/user-groups-subpage.less b/y-web/src/modules/admin/pages/users/[userId]/groups/user-groups-subpage.less new file mode 100644 index 0000000..c956e15 --- /dev/null +++ b/y-web/src/modules/admin/pages/users/[userId]/groups/user-groups-subpage.less @@ -0,0 +1,27 @@ +#page-user-groups-subpage { + .group-option { + display: inline-flex; + align-items: center; + gap: 1em; + + width: fit-content; + + padding: 0.5em 0.66em; + + border-radius: 5px; + + transition: 0.15s; + + &.selected { + color: var(--color-primary-d-1); + + background-color: var(--color-primary-l-1-t-1); + box-shadow: inset 0 0 0 1px var(--color-primary-l-1-t-1); + + &.disabled { + box-shadow: none; + background-color: transparent; + } + } + } +} diff --git a/y-web/src/modules/admin/pages/users/[userId]/index.tsx b/y-web/src/modules/admin/pages/users/[userId]/index.tsx new file mode 100644 index 0000000..65bf1c8 --- /dev/null +++ b/y-web/src/modules/admin/pages/users/[userId]/index.tsx @@ -0,0 +1,127 @@ +import { Component, Show, createEffect, createMemo } from "solid-js" + +import { + Route, + Routes, + useLocation, + useNavigate, + useParams, +} from "@solidjs/router" +import { format } from "date-fns" + +import { Card } from "@/app/components/common/card/card" +import { Container } from "@/app/components/common/layout/container" +import { Stack } from "@/app/components/common/stack/stack" +import { Tab, TabsContainer } from "@/app/components/common/tab/tab" +import { Text } from "@/app/components/common/text/text" +import { genericErrorToast } from "@/app/core/util/toast-utils" +import { Breadcrumb, Breadcrumbs } from "@/app/layout/components/breadcrumbs" +import { routes } from "@/app/routes" +import { useUser } from "@/modules/admin/user/user.service" +import { useAuth } from "@/modules/core/auth/auth.service" + +import UserGeneralSubpage from "./general" +import UserGroupsSubpage from "./groups" + +const USERS_LIST_ROUTE = routes["/admin/users"] + +const UserPage: Component = () => { + const $auth = useAuth() + + const allowedTabs = createMemo(() => { + const groupsTabAllowed = $auth.data?.user_rights.some( + (right) => right.right_name === "manage_user_groups" + ) + + return { groupsTabAllowed } + }) + + const navigate = useNavigate() + const location = useLocation() + const params = useParams() + + // eslint-disable-next-line no-confusing-arrow + const userId = createMemo(() => params.userId as string) + + const $user = useUser(() => ({ + userId: userId(), + })) + + createEffect(() => { + if ($user.isError) { + genericErrorToast($user.error) + navigate(USERS_LIST_ROUTE) + } + }) + + const currentSubpage = createMemo(() => { + const locationParts = location.pathname.split("/") + + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + return (locationParts.length > 3 && locationParts[4]) || "general" + }) + + const navigateToSubpage = (subpage: string) => + userId() && navigate(`${USERS_LIST_ROUTE}/${userId()}/${subpage}`) + + return ( + + + Administration + Users + + {$user.data?.username ?? ""} + + + + + + + {$user.data!.username} + + Joined {format(new Date($user.data!.created_at), "dd.MM.yyyy")} + + + + + + + navigateToSubpage("")} + selected={currentSubpage() === "general"} + /> + + navigateToSubpage("groups")} + /> + + + + + + + } + /> + + } + /> + + + + + + ) +} + +export default UserPage diff --git a/y-web/src/modules/admin/pages/users/new/index.tsx b/y-web/src/modules/admin/pages/users/new/index.tsx new file mode 100644 index 0000000..e00449b --- /dev/null +++ b/y-web/src/modules/admin/pages/users/new/index.tsx @@ -0,0 +1,146 @@ +import { Component, createEffect } from "solid-js" + +import { useNavigate } from "@solidjs/router" +import { createMutation, useQueryClient } from "@tanstack/solid-query" + +import { Button } from "@/app/components/common/button/button" +import { Icon } from "@/app/components/common/icon/icon" +import { InputField } from "@/app/components/common/input-field/input-field" +import { Container } from "@/app/components/common/layout/container" +import { Stack } from "@/app/components/common/stack/stack" +import { Text } from "@/app/components/common/text/text" +import { toastCtl } from "@/app/core/toast" +import { useForm } from "@/app/core/use-form" +import { genericErrorToast } from "@/app/core/util/toast-utils" +import { Breadcrumb, Breadcrumbs } from "@/app/layout/components/breadcrumbs" +import { routes } from "@/app/routes" +import { createUser } from "@/modules/admin/users/users.api" +import { usersKey } from "@/modules/admin/users/users.service" +import { useAuth } from "@/modules/core/auth/auth.service" + +const USERS_ROUTE = routes["/admin/users"] + +const NewUserPage: Component = () => { + const $auth = useAuth() + + const navigate = useNavigate() + const queryClient = useQueryClient() + const { notify } = toastCtl + + const $createUser = createMutation(createUser) + + createEffect(() => { + const userCreationAllowed = $auth.data?.user_rights.some( + (right) => right.right_name === "create_account" + ) + + if (!userCreationAllowed) { + navigate(USERS_ROUTE) + } + }) + + const form = useForm({ + defaultValues: { + username: "", + password: "", + }, + disabled: () => $createUser.isLoading, + onSubmit: (values) => { + $createUser.mutate( + { + username: values.username, + password: values.password, + }, + { + onSuccess: (response) => { + notify({ + title: "User created", + content: "A new user was successfully created.", + severity: "success", + icon: "check", + }) + + void queryClient.invalidateQueries([usersKey]) + navigate(`${USERS_ROUTE}/${response.id}`) + }, + onError: (error) => genericErrorToast(error), + } + ) + }, + }) + + const { register, submit, errors } = form + + return ( + + + Administration + Users + new + + + + + + + Create a new user + + + +
+ + + + + + + + + + +
+
+
+ ) +} + +export default NewUserPage diff --git a/y-web/src/modules/admin/user-groups/user-groups.api.ts b/y-web/src/modules/admin/user-groups/user-groups.api.ts new file mode 100644 index 0000000..f680d93 --- /dev/null +++ b/y-web/src/modules/admin/user-groups/user-groups.api.ts @@ -0,0 +1,75 @@ +import { del, get, patch, post } from "@/app/core/request" +import { TableInput, appendTableInput } from "@/app/core/request.utils" + +import { + TCreateUserGroup, + TGetUserGroups, + TUserGroupDetails, +} from "./user-groups.codecs" + +export const apiUserGroups = "/admin/user-groups" + +export type UserGroupsInput = TableInput + +export const userGroups = async (input: UserGroupsInput) => { + const query = new URLSearchParams() + + appendTableInput(query, input) + + return get(apiUserGroups, { + query, + }).then((data) => TGetUserGroups.parse(data)) +} + +export type CreateUserGruopInput = { + name: string +} + +export const createUserGroup = async (input: CreateUserGruopInput) => { + return post(apiUserGroups, { + body: { + name: input.name, + }, + }).then((data) => TCreateUserGroup.parse(data)) +} + +export type GetUserGroupInput = { + userGroupId: number +} + +export const userGroup = async (input: GetUserGroupInput) => { + return get(`${apiUserGroups}/${input.userGroupId}`).then((data) => + TUserGroupDetails.parse(data) + ) +} + +export type UpdateUserGroupInput = { + userGroupId: number + + name?: string + + rights?: Record< + string, + { + granted: boolean + options: Record + } + > +} + +export const updateUserGroup = async (input: UpdateUserGroupInput) => { + return patch(`${apiUserGroups}/${input.userGroupId}`, { + body: { + name: input.name?.trim(), + rights: input.rights, + }, + }) +} + +export type DeleteUserGruopInput = { + userGroupId: number +} + +export const deleteUserGroup = async (input: DeleteUserGruopInput) => { + return del(`${apiUserGroups}/${input.userGroupId}`) +} diff --git a/y-web/src/modules/admin/user-groups/user-groups.codecs.ts b/y-web/src/modules/admin/user-groups/user-groups.codecs.ts new file mode 100644 index 0000000..c4c60d6 --- /dev/null +++ b/y-web/src/modules/admin/user-groups/user-groups.codecs.ts @@ -0,0 +1,47 @@ +import { z } from "zod" + +export const userGroupType = { user: "user", everyone: "everyone" } as const + +export const TUserGroupRow = z.object({ + id: z.number(), + name: z.string(), + group_type: z.nullable(z.string()), +}) + +export type IUserGroupRow = z.infer + +export const TUserGroupRows = z.array(TUserGroupRow) + +export const TGetUserGroups = z.object({ + user_groups: TUserGroupRows, + total_count: z.number(), +}) + +export const TCreateUserGroup = z.object({ + id: z.number(), +}) + +export const TUserGroupRightOptionValue = z.union([ + z.string(), + z.number(), + z.boolean(), + z.array(z.string()), + z.array(z.number()), + z.null(), +]) + +export type IUserGroupRightOptionValue = z.infer< + typeof TUserGroupRightOptionValue +> + +export const TUserGroupRight = z.object({ + right_name: z.string(), + right_options: z.record(TUserGroupRightOptionValue), +}) + +export const TUserGroupDetails = z.object({ + id: z.number(), + name: z.string(), + rights: z.array(TUserGroupRight), + group_type: z.nullable(z.string()), +}) diff --git a/y-web/src/modules/admin/user-groups/user-groups.service.ts b/y-web/src/modules/admin/user-groups/user-groups.service.ts new file mode 100644 index 0000000..918fdd2 --- /dev/null +++ b/y-web/src/modules/admin/user-groups/user-groups.service.ts @@ -0,0 +1,32 @@ +import { createQuery } from "@tanstack/solid-query" + +import { + GetUserGroupInput, + UserGroupsInput, + userGroup, + userGroups, +} from "./user-groups.api" + +export const userGroupsKey = "users-groups" as const +export const userGroupKey = "users-group" as const + +export const useUserGroups = ( + input: () => UserGroupsInput, + options: { + refetchOnWindowFocus?: boolean + refetchInterval?: number + } = {} +) => { + return createQuery( + () => [userGroupsKey, input()], + async () => userGroups(input()), + options + ) +} + +export const useUserGroup = (input: () => GetUserGroupInput) => { + return createQuery( + () => [userGroupKey, input().userGroupId], + async () => userGroup(input()) + ) +} diff --git a/y-web/src/modules/admin/user-rights/user-rights.api.ts b/y-web/src/modules/admin/user-rights/user-rights.api.ts new file mode 100644 index 0000000..c8939ef --- /dev/null +++ b/y-web/src/modules/admin/user-rights/user-rights.api.ts @@ -0,0 +1,9 @@ +import { get } from "@/app/core/request" + +import { TGetUserRights } from "./user-rights.codecs" + +export const apiUserRights = "/user-rights" + +export const userRights = async () => { + return get(apiUserRights).then((data) => TGetUserRights.parse(data)) +} diff --git a/y-web/src/modules/admin/user-rights/user-rights.codecs.ts b/y-web/src/modules/admin/user-rights/user-rights.codecs.ts new file mode 100644 index 0000000..92c384d --- /dev/null +++ b/y-web/src/modules/admin/user-rights/user-rights.codecs.ts @@ -0,0 +1,42 @@ +import { z } from "zod" + +export const TUserRightOptionValueType = z.union([ + z.literal("boolean"), + z.literal("number"), + z.literal("string"), + z.literal("string_array"), +]) + +export const TUserRightTag = z.union([ + z.literal("dangerous"), + z.literal("administrative"), +]) + +export type IUserRightTag = z.infer + +export const TUserRightOption = z.object({ + name: z.string(), + value_type: TUserRightOptionValueType, + value_source: z.string().nullable(), +}) + +export type IUserRightOption = z.infer + +export const TUserRight = z.object({ + name: z.string(), + options: z.array(TUserRightOption), + tags: z.array(TUserRightTag), +}) + +export type IUserRight = z.infer + +export const TUserRightCategory = z.object({ + name: z.string(), + rights: z.array(TUserRight), +}) + +export type IUserRightCategory = z.infer + +export const TGetUserRights = z.object({ + categories: z.array(TUserRightCategory), +}) diff --git a/y-web/src/modules/admin/user-rights/user-rights.service.ts b/y-web/src/modules/admin/user-rights/user-rights.service.ts new file mode 100644 index 0000000..289d7f2 --- /dev/null +++ b/y-web/src/modules/admin/user-rights/user-rights.service.ts @@ -0,0 +1,12 @@ +import { createQuery } from "@tanstack/solid-query" + +import { userRights } from "./user-rights.api" + +export const userRightsKey = "userRights" as const + +export const useUserRights = () => { + return createQuery( + () => [userRightsKey], + async () => userRights() + ) +} diff --git a/y-web/src/modules/admin/user/user.api.ts b/y-web/src/modules/admin/user/user.api.ts new file mode 100644 index 0000000..5b2e143 --- /dev/null +++ b/y-web/src/modules/admin/user/user.api.ts @@ -0,0 +1,28 @@ +import { get, patch } from "@/app/core/request" + +import { TUser } from "./user.codecs" + +export const apiUsers = "/admin/users" + +export type UpdateUserGruopMembershipInput = { + userId: number + userGroups: number[] +} + +export const updateUserGroupMembership = async ( + input: UpdateUserGruopMembershipInput +) => { + return patch(`${apiUsers}/${input.userId}/groups`, { + body: { + user_groups: input.userGroups, + }, + }) +} + +export type GetUserInput = { + userId: number | string +} + +export const user = async (input: GetUserInput) => { + return get(`${apiUsers}/${input.userId}`).then((data) => TUser.parse(data)) +} diff --git a/y-web/src/modules/admin/user/user.codecs.ts b/y-web/src/modules/admin/user/user.codecs.ts new file mode 100644 index 0000000..e14c365 --- /dev/null +++ b/y-web/src/modules/admin/user/user.codecs.ts @@ -0,0 +1,10 @@ +import { z } from "zod" + +export const TUser = z.object({ + id: z.number(), + username: z.string(), + created_at: z.string().datetime(), + user_groups: z.array(z.number()), +}) + +export type IUser = z.infer diff --git a/y-web/src/modules/admin/user/user.service.ts b/y-web/src/modules/admin/user/user.service.ts new file mode 100644 index 0000000..1981129 --- /dev/null +++ b/y-web/src/modules/admin/user/user.service.ts @@ -0,0 +1,19 @@ +import { createQuery } from "@tanstack/solid-query" + +import { GetUserInput, user } from "./user.api" + +export const userKey = "user" as const + +export const useUser = ( + input: () => GetUserInput, + options: { + refetchOnWindowFocus?: boolean + refetchInterval?: number + } = {} +) => { + return createQuery( + () => [userKey, input().userId], + async () => user(input()), + options + ) +} diff --git a/y-web/src/modules/admin/users/users.api.ts b/y-web/src/modules/admin/users/users.api.ts new file mode 100644 index 0000000..25c2105 --- /dev/null +++ b/y-web/src/modules/admin/users/users.api.ts @@ -0,0 +1,57 @@ +import { del, get, post, put } from "@/app/core/request" +import { TableInput, appendTableInput } from "@/app/core/request.utils" + +import { TCreateUser, TGetUsers } from "./users.codecs" + +export const apiUsers = "/admin/users" + +export type UsersInput = TableInput + +export const users = async (input: UsersInput) => { + const query = new URLSearchParams() + + appendTableInput(query, input) + + return get(apiUsers, { + query, + }).then((data) => TGetUsers.parse(data)) +} + +export type UpdateUserPasswordInput = { + userId: number + password: string +} + +export const updateUserPassword = async (input: UpdateUserPasswordInput) => { + return put(`${apiUsers}/${input.userId}/password`, { + body: { + password: input.password, + }, + }) +} + +export type CreateUserInput = { + username: string + password: string +} + +export const createUser = async (input: CreateUserInput) => { + return post(apiUsers, { + body: { + username: input.username.trim(), + password: input.password, + }, + }).then((data) => TCreateUser.parse(data)) +} + +export type DeleteUsersInput = { + userIds: number[] +} + +export const deleteUsers = async (input: DeleteUsersInput) => { + return del(apiUsers, { + body: { + user_ids: input.userIds, + }, + }) +} diff --git a/y-web/src/modules/admin/users/users.codecs.ts b/y-web/src/modules/admin/users/users.codecs.ts new file mode 100644 index 0000000..6e036e1 --- /dev/null +++ b/y-web/src/modules/admin/users/users.codecs.ts @@ -0,0 +1,22 @@ +import { z } from "zod" + +export const TUser = z.object({ + id: z.number(), + username: z.string(), + created_at: z.string().datetime(), +}) + +export type IUser = z.infer + +export const TUsers = z.array(TUser) + +export type IUsers = z.infer + +export const TGetUsers = z.object({ + users: TUsers, + total_count: z.number(), +}) + +export const TCreateUser = z.object({ + id: z.number(), +}) diff --git a/y-web/src/modules/admin/users/users.service.ts b/y-web/src/modules/admin/users/users.service.ts new file mode 100644 index 0000000..bc55ca7 --- /dev/null +++ b/y-web/src/modules/admin/users/users.service.ts @@ -0,0 +1,19 @@ +import { createQuery } from "@tanstack/solid-query" + +import { UsersInput, users } from "./users.api" + +export const usersKey = "users" as const + +export const useUsers = ( + input: () => UsersInput, + options: { + refetchOnWindowFocus?: boolean + refetchInterval?: number + } = {} +) => { + return createQuery( + () => [usersKey, input()], + async () => users(input()), + options + ) +} diff --git a/y-web/src/modules/core/auth/auth.api.ts b/y-web/src/modules/core/auth/auth.api.ts new file mode 100644 index 0000000..8a6a292 --- /dev/null +++ b/y-web/src/modules/core/auth/auth.api.ts @@ -0,0 +1,26 @@ +import { get, post } from "@/app/core/request" + +import { TMe } from "./auth.codecs" + +const apiLogin = "/auth/login" +const apiLogout = "/auth/logout" +const apiMe = "/auth/me" + +export type LoginInput = { + username: string + password: string +} + +export const login = async (input: LoginInput) => { + return post(apiLogin, { + body: input, + }) +} + +export const logout = async () => { + return post(apiLogout) +} + +export const getMe = async () => { + return get(apiMe).then((data) => TMe.parse(data)) +} diff --git a/y-web/src/modules/core/auth/auth.codecs.ts b/y-web/src/modules/core/auth/auth.codecs.ts new file mode 100644 index 0000000..bae5577 --- /dev/null +++ b/y-web/src/modules/core/auth/auth.codecs.ts @@ -0,0 +1,14 @@ +import z from "zod" + +export const TMe = z.object({ + id: z.number(), + username: z.string(), + user_rights: z.array( + z.object({ + right_name: z.string(), + right_options: z.any(), + }) + ), +}) + +export type IMe = z.infer diff --git a/y-web/src/modules/core/auth/auth.service.ts b/y-web/src/modules/core/auth/auth.service.ts new file mode 100644 index 0000000..26b8b8f --- /dev/null +++ b/y-web/src/modules/core/auth/auth.service.ts @@ -0,0 +1,11 @@ +import { createQuery } from "@tanstack/solid-query" + +import { getMe } from "./auth.api" + +export const authKey = ["auth-me"] as const + +export const useAuth = () => { + return createQuery(() => authKey, getMe, { + refetchOnMount: false, + }) +} diff --git a/y-web/src/modules/core/pages/login/login.less b/y-web/src/modules/core/pages/login/login.less new file mode 100644 index 0000000..9626b3a --- /dev/null +++ b/y-web/src/modules/core/pages/login/login.less @@ -0,0 +1,31 @@ +#page-login { + height: 100vh; + + > .login-container { + height: 100%; + + display: flex; + align-items: center; + justify-content: center; + + > .form-container { + > .instance-name { + width: 100%; + + margin-bottom: 2em; + + font-weight: 500; + + text-align: center; + } + + > .login-form { + display: flex; + flex-direction: column; + gap: 1em; + + width: 250px; + } + } + } +} diff --git a/y-web/src/modules/core/pages/login/login.tsx b/y-web/src/modules/core/pages/login/login.tsx new file mode 100644 index 0000000..1457ece --- /dev/null +++ b/y-web/src/modules/core/pages/login/login.tsx @@ -0,0 +1,169 @@ +import { Component, Show, createSignal } from "solid-js" + +import { useNavigate } from "@solidjs/router" +import { createMutation, useQueryClient } from "@tanstack/solid-query" + +import { Button } from "@/app/components/common/button/button" +import { InputError } from "@/app/components/common/input-error/input-error" +import { InputField } from "@/app/components/common/input-field/input-field" +import { Stack } from "@/app/components/common/stack/stack" +import { Text } from "@/app/components/common/text/text" +import { ResponseError } from "@/app/core/request" +import { toastCtl } from "@/app/core/toast" +import { useForm } from "@/app/core/use-form" +import { routes } from "@/app/routes" +import { unsafe_t } from "@/i18n" +import { login, logout } from "@/modules/core/auth/auth.api" +import { authKey, useAuth } from "@/modules/core/auth/auth.service" + +import "./login.less" + +const LoginPage: Component = () => { + const { notify } = toastCtl + const navigate = useNavigate() + const queryClient = useQueryClient() + + const $auth = useAuth() + + const $login = createMutation(login) + const $logout = createMutation(logout) + + const performLogout = () => { + // eslint-disable-next-line no-undefined + $logout.mutate(undefined, { + onSuccess: () => { + notify({ + title: "Logged out", + content: "See ya!", + severity: "success", + icon: "waving_hand", + }) + + void queryClient.invalidateQueries(authKey) + }, + }) + } + + const [error, setError] = createSignal() + + const form = useForm({ + defaultValues: { + username: "", + password: "", + }, + onSubmit: (values) => { + $login.mutate(values, { + onSuccess: () => { + notify({ + title: "Welcome back!", + severity: "success", + icon: "waving_hand", + }) + + void queryClient.invalidateQueries(authKey) + + const urlParameters = new URLSearchParams(window.location.search) + + if (urlParameters.has("return")) { + const to = urlParameters.get("return") + + if (to && !to.includes("/login")) { + return void navigate(to) + } + } + + navigate(routes["/"]) + }, + onError: (requestError) => { + setError((requestError as ResponseError).code) + }, + }) + }, + }) + + const { register, submit, errors } = form + + return ( +
+ +
+ ) +} + +export default LoginPage diff --git a/y-web/tsconfig.json b/y-web/tsconfig.json index 1cc0452..58b37df 100644 --- a/y-web/tsconfig.json +++ b/y-web/tsconfig.json @@ -17,6 +17,8 @@ "strict": true, "allowSyntheticDefaultImports": true, - "noUncheckedIndexedAccess": true + "noUncheckedIndexedAccess": true, + + "resolveJsonModule": true } } diff --git a/y-web/vite.config.ts b/y-web/vite.config.ts index 60fb17f..a5cc72e 100644 --- a/y-web/vite.config.ts +++ b/y-web/vite.config.ts @@ -5,6 +5,14 @@ export default defineConfig({ plugins: [solidPlugin()], server: { port: 3000, + proxy: { + "/api": { + target: "http://localhost:8080", + changeOrigin: true, + secure: false, + ws: true, + }, + }, }, build: { target: "esnext",