From 70a93aeb0b5dfde84fec9eb801cc600e243e0c73 Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Sun, 18 Jun 2023 18:17:28 -0500 Subject: [PATCH] init --- .env | 1 + .gitignore | 4 + Cargo.lock | 2698 +++++++++++++++++ Cargo.toml | 52 + config.yaml | 45 + diesel.toml | 9 + migrations/.keep | 0 .../2023-06-07-213214_session_store/down.sql | 2 + .../2023-06-07-213214_session_store/up.sql | 6 + .../2023-06-13-192016_med_management/down.sql | 3 + .../2023-06-13-192016_med_management/up.sql | 27 + scripts/deploy.fish | 25 + scripts/dev.fish | 16 + src/apps/auth/middleware.rs | 115 + src/apps/auth/mod.rs | 170 ++ src/apps/auth/password.rs | 54 + src/apps/canvas_lms/comm.rs | 71 + src/apps/canvas_lms/grading.rs | 127 + src/apps/canvas_lms/graph.rs | 132 + src/apps/canvas_lms/mod.rs | 220 ++ src/apps/med/directive.rs | 176 ++ src/apps/med/log.rs | 270 ++ src/apps/med/mod.rs | 84 + src/apps/mod.rs | 27 + src/apps/server_info/mod.rs | 51 + src/apps/webcheck/driver/chrome.rs | 67 + src/apps/webcheck/driver/mod.rs | 1 + src/apps/webcheck/mod.rs | 199 ++ src/apps/webcheck/utd_app.rs | 75 + src/bin/server.rs | 41 + src/comm/email.rs | 46 + src/comm/gotify.rs | 73 + src/comm/mod.rs | 99 + src/config/comm.rs | 24 + src/config/mod.rs | 79 + src/http.rs | 108 + src/lib.rs | 134 + src/models/med.rs | 182 ++ src/models/med_test.rs | 74 + src/models/mod.rs | 4 + src/models/session.rs | 9 + src/schema.rs | 44 + src/session/mod.rs | 71 + src/session/wrap.rs | 60 + src/ui.rs | 103 + templates/grades.html | 28 + ui/.eslintrc.cjs | 14 + ui/.gitignore | 24 + ui/.server_pid | 1 + ui/index.html | 16 + ui/package.json | 35 + ui/public/favicon.ico | Bin 0 -> 1406 bytes ui/src/App.css | 42 + ui/src/App.tsx | 83 + ui/src/PageBase.tsx | 175 ++ ui/src/api/auth.ts | 42 + ui/src/api/canvas_lms.ts | 41 + ui/src/api/med_directive.ts | 55 + ui/src/api/med_log.ts | 61 + ui/src/api/request.ts | 23 + ui/src/api/server_info.ts | 11 + ui/src/api/time.ts | 12 + ui/src/assets/react.svg | 1 + ui/src/components/EnsureRole.tsx | 18 + ui/src/components/TimeBody.tsx | 65 + ui/src/context/LoginContext.ts | 8 + ui/src/index.css | 13 + ui/src/main.tsx | 10 + ui/src/pages/FramePage.tsx | 21 + ui/src/pages/GradesPage.tsx | 195 ++ ui/src/pages/HomePage.tsx | 117 + ui/src/pages/LoginDialog.tsx | 89 + ui/src/pages/MedsPage.tsx | 273 ++ ui/src/theme.ts | 11 + ui/src/vite-env.d.ts | 1 + ui/tsconfig.json | 25 + ui/tsconfig.node.json | 10 + ui/vite.config.ts | 15 + ui/yarn.lock | 1983 ++++++++++++ 79 files changed, 9396 insertions(+) create mode 100644 .env create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 config.yaml create mode 100644 diesel.toml create mode 100644 migrations/.keep create mode 100644 migrations/2023-06-07-213214_session_store/down.sql create mode 100644 migrations/2023-06-07-213214_session_store/up.sql create mode 100644 migrations/2023-06-13-192016_med_management/down.sql create mode 100644 migrations/2023-06-13-192016_med_management/up.sql create mode 100755 scripts/deploy.fish create mode 100755 scripts/dev.fish create mode 100644 src/apps/auth/middleware.rs create mode 100644 src/apps/auth/mod.rs create mode 100644 src/apps/auth/password.rs create mode 100644 src/apps/canvas_lms/comm.rs create mode 100644 src/apps/canvas_lms/grading.rs create mode 100644 src/apps/canvas_lms/graph.rs create mode 100644 src/apps/canvas_lms/mod.rs create mode 100644 src/apps/med/directive.rs create mode 100644 src/apps/med/log.rs create mode 100644 src/apps/med/mod.rs create mode 100644 src/apps/mod.rs create mode 100644 src/apps/server_info/mod.rs create mode 100644 src/apps/webcheck/driver/chrome.rs create mode 100644 src/apps/webcheck/driver/mod.rs create mode 100644 src/apps/webcheck/mod.rs create mode 100644 src/apps/webcheck/utd_app.rs create mode 100644 src/bin/server.rs create mode 100644 src/comm/email.rs create mode 100644 src/comm/gotify.rs create mode 100644 src/comm/mod.rs create mode 100644 src/config/comm.rs create mode 100644 src/config/mod.rs create mode 100644 src/http.rs create mode 100644 src/lib.rs create mode 100644 src/models/med.rs create mode 100644 src/models/med_test.rs create mode 100644 src/models/mod.rs create mode 100644 src/models/session.rs create mode 100644 src/schema.rs create mode 100644 src/session/mod.rs create mode 100644 src/session/wrap.rs create mode 100644 src/ui.rs create mode 100644 templates/grades.html create mode 100644 ui/.eslintrc.cjs create mode 100644 ui/.gitignore create mode 100644 ui/.server_pid create mode 100644 ui/index.html create mode 100644 ui/package.json create mode 100644 ui/public/favicon.ico create mode 100644 ui/src/App.css create mode 100644 ui/src/App.tsx create mode 100644 ui/src/PageBase.tsx create mode 100644 ui/src/api/auth.ts create mode 100644 ui/src/api/canvas_lms.ts create mode 100644 ui/src/api/med_directive.ts create mode 100644 ui/src/api/med_log.ts create mode 100644 ui/src/api/request.ts create mode 100644 ui/src/api/server_info.ts create mode 100644 ui/src/api/time.ts create mode 100644 ui/src/assets/react.svg create mode 100644 ui/src/components/EnsureRole.tsx create mode 100644 ui/src/components/TimeBody.tsx create mode 100644 ui/src/context/LoginContext.ts create mode 100644 ui/src/index.css create mode 100644 ui/src/main.tsx create mode 100644 ui/src/pages/FramePage.tsx create mode 100644 ui/src/pages/GradesPage.tsx create mode 100644 ui/src/pages/HomePage.tsx create mode 100644 ui/src/pages/LoginDialog.tsx create mode 100644 ui/src/pages/MedsPage.tsx create mode 100644 ui/src/theme.ts create mode 100644 ui/src/vite-env.d.ts create mode 100644 ui/tsconfig.json create mode 100644 ui/tsconfig.node.json create mode 100644 ui/vite.config.ts create mode 100644 ui/yarn.lock diff --git a/.env b/.env new file mode 100644 index 0000000..6a786e4 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +DATABASE_URL=sqlite://data/development.db \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..670732e --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +data +target +config-dev.yaml +config-prod.yaml diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..ec7f2de --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2698 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "433cfd6710c9986c576a25ca913c39d66a6474107b406f34f91d4a8923395241" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "209b47e8954a928e1d72e86eca7000ebb6655fe1436d33eefc2201cad027e237" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "aho-corasick" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +dependencies = [ + "memchr", +] + +[[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 = "anstream" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is-terminal", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" + +[[package]] +name = "anstyle-parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "anstyle-wincon" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +dependencies = [ + "anstyle", + "windows-sys 0.48.0", +] + +[[package]] +name = "anyhow" +version = "1.0.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" + +[[package]] +name = "arc-swap" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" + +[[package]] +name = "argon2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95c2fcf79ad1932ac6269a738109997a83c227c09b75842ae564dc8ede6a861c" +dependencies = [ + "base64ct", + "blake2", + "password-hash", +] + +[[package]] +name = "askama" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47cbc3cf73fa8d9833727bbee4835ba5c421a0d65b72daf9a7b5d0e0f9cfb57e" +dependencies = [ + "askama_derive", + "askama_escape", + "humansize", + "num-traits", + "percent-encoding", +] + +[[package]] +name = "askama_derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c22fbe0413545c098358e56966ff22cdd039e10215ae213cfbd65032b119fc94" +dependencies = [ + "basic-toml", + "mime", + "mime_guess", + "nom", + "proc-macro2", + "quote", + "serde", + "syn 2.0.18", +] + +[[package]] +name = "askama_escape" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" + +[[package]] +name = "async-trait" +version = "0.1.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "axum" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8175979259124331c1d7bf6586ee7e0da434155e4b2d48ec2c8386281d8df39" +dependencies = [ + "async-trait", + "axum-core", + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "mime", + "rustversion", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-server" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "447f28c85900215cc1bea282f32d4a2f22d55c5a300afdfbc661c8d6a632e063" +dependencies = [ + "arc-swap", + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "rustls 0.21.1", + "rustls-pemfile", + "tokio", + "tokio-rustls 0.24.0", + "tower-service", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[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 = "basic-toml" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c0de75129aa8d0cceaf750b89013f0e08804d6ec61416da787b35ad0d7cddf1" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[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 = "bumpalo" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" + +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + +[[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 = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clap" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "401a4694d2bf92537b6867d94de48c4842089645fdcdf6c71865b175d836e9c2" +dependencies = [ + "clap_builder", + "clap_derive", + "once_cell", +] + +[[package]] +name = "clap_builder" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72394f3339a76daf211e57d4bcb374410f3965dcc606dd0e03738c7888766980" +dependencies = [ + "anstream", + "anstyle", + "bitflags", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8cd2b2a819ad6eec39e8f1d6b53001af1e5469f8c177579cdaeb313115b825f" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "clap_lex" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "colored" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" +dependencies = [ + "atty", + "lazy_static", + "winapi", +] + +[[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.21", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[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.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +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", + "rand_core", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "diesel" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7a532c1f99a0f596f6960a60d1e119e91582b24b39e2d83a190e61262c3ef0c" +dependencies = [ + "chrono", + "diesel_derives", + "libsqlite3-sys", + "time 0.3.21", +] + +[[package]] +name = "diesel_derives" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74398b79d81e52e130d991afeed9c86034bb1b7735f46d2f5bf7deb261d80303" +dependencies = [ + "diesel_table_macro_syntax", + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "diesel_migrations" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6036b3f0120c5961381b570ee20a02432d7e2d27ea60de9578799cf9156914ac" +dependencies = [ + "diesel", + "migrations_internals", + "migrations_macros", +] + +[[package]] +name = "diesel_table_macro_syntax" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5" +dependencies = [ + "syn 2.0.18", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "email-encoding" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbfb21b9878cf7a348dcb8559109aabc0ec40d69924bd706fa5149846c4fef75" +dependencies = [ + "base64 0.21.2", + "memchr", +] + +[[package]] +name = "email_address" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2153bd83ebc09db15bcbdc3e2194d901804952e3dc96967e1cd3b0c5c32d112" + +[[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 = "errno" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "fantoccini" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65f0fbe245d714b596ba5802b46f937f5ce68dcae0f32f9a70b5c3b04d3c6f64" +dependencies = [ + "base64 0.13.1", + "cookie", + "futures-core", + "futures-util", + "http", + "hyper", + "hyper-rustls", + "mime", + "serde", + "serde_json", + "time 0.3.21", + "tokio", + "url", + "webdriver", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[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 = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[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-io" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + +[[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.18", +] + +[[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 = "ghash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "h2" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d357c7ae988e7d2182f7d7871d0b963962420b0678b0997ce7de72001aeab782" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "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 = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi", +] + +[[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 = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "http-range-header" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29" + +[[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 = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + +[[package]] +name = "hyper" +version = "0.14.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c" +dependencies = [ + "http", + "hyper", + "log", + "rustls 0.20.8", + "rustls-native-certs", + "tokio", + "tokio-rustls 0.23.4", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[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 = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[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", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + +[[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 0.3.1", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "ipnet" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f" + +[[package]] +name = "is-terminal" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" +dependencies = [ + "hermit-abi 0.3.1", + "io-lifetimes", + "rustix", + "windows-sys 0.48.0", +] + +[[package]] +name = "itoa" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" + +[[package]] +name = "js-sys" +version = "0.3.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f37a4a5928311ac501dee68b3c7613a1037d0edb30c8e5427bd832d55d1b790" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lettre" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76bd09637ae3ec7bd605b8e135e757980b3968430ff2b1a4a94fb7769e50166d" +dependencies = [ + "base64 0.21.2", + "email-encoding", + "email_address", + "fastrand", + "futures-util", + "hostname", + "httpdate", + "idna 0.3.0", + "mime", + "native-tls", + "nom", + "once_cell", + "quoted_printable", + "socket2", + "tokio", +] + +[[package]] +name = "libc" +version = "0.2.146" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b" + +[[package]] +name = "libm" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" + +[[package]] +name = "libsqlite3-sys" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326" +dependencies = [ + "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 = "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.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de" + +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + +[[package]] +name = "matchit" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b87248edafb776e59e6ee64a79086f65890d3510f2c656c000bf2a7e8a0aea40" + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "migrations_internals" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f23f71580015254b020e856feac3df5878c2c7a8812297edd6c0a485ac9dada" +dependencies = [ + "serde", + "toml", +] + +[[package]] +name = "migrations_macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cce3325ac70e67bbab5bd837a31cae01f1a6db64e0e744a33cb03a543469ef08" +dependencies = [ + "migrations_internals", + "proc-macro2", + "quote", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + +[[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", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.48.0", +] + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[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-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +dependencies = [ + "hermit-abi 0.2.6", + "libc", +] + +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "openssl" +version = "0.10.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b3f656a17a6cbc115b5c7a40c616947d213ba182135b014d6051b73ab6f019" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2ce0f250f34a308dcfdbb351f511359857d4ed2134ba715a4eadd46e1ffd617" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[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 = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "pin-project" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c95a7476719eab1e366eaf73d0260af3021184f18177925b07f54b30089ceead" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39407670928234ebc5e6e580247dd567ad73a3578460c5990f9503df207e8f07" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "polyval" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef234e08c11dfcb2e56f79fd70f6f2eb7f025c0ce2333e82f4f0518ecad30c6" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "quoted_printable" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3866219251662ec3b26fc217e3e05bf9c4f84325234dfb96bf0bf840889e49" + +[[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", +] + +[[package]] +name = "regex" +version = "1.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" + +[[package]] +name = "reqwest" +version = "0.11.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" +dependencies = [ + "base64 0.21.2", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] +name = "rust-embed" +version = "6.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73e721f488c353141288f223b599b4ae9303ecf3e62923f40a492f0634a4dc3" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "6.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22ce362f5561923889196595504317a4372b84210e6e335da529a65ea5452b5" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn 2.0.18", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "7.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512b0ab6853f7e14e3c8754acb43d6f748bb9ced66aa5915a6553ac8213f7731" +dependencies = [ + "sha2", + "walkdir", +] + +[[package]] +name = "rustix" +version = "0.37.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustls" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" +dependencies = [ + "log", + "ring", + "sct", + "webpki", +] + +[[package]] +name = "rustls" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c911ba11bc8433e811ce56fde130ccf32f5127cab0e0194e9c68c5a5b671791e" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" +dependencies = [ + "base64 0.21.2", +] + +[[package]] +name = "rustls-webpki" +version = "0.100.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6207cd5ed3d8dca7816f8f3725513a34609c0c765bf652b8c3cb4cfd87db46b" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06" + +[[package]] +name = "ryu" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" +dependencies = [ + "windows-sys 0.42.0", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "security-framework" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.163" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.163" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "serde_json" +version = "1.0.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7f05c1d5476066defcdfacce1f52fc3cae3af1d3089727100c02ae92e5abbe0" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "serde_spanned" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93107647184f6027e3b7dcb2e11034cf95ffa1e3a682c67951963ac69c1c007d" +dependencies = [ + "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_yaml" +version = "0.9.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9d684e3ec7de3bf5466b32bd75303ac16f0736426e5a4e0d6e489559ce1249c" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "simple_logger" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78beb34673091ccf96a8816fce8bfd30d1292c7621ca2bcb5f2ba0fae4f558d" +dependencies = [ + "atty", + "colored", + "log", + "time 0.3.21", + "windows-sys 0.42.0", +] + +[[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.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[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 = "stringmatch" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aadc0801d92f0cdc26127c67c4b8766284f52a5ba22894f285e3101fa57d05d" +dependencies = [ + "regex", +] + +[[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.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "tempfile" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" +dependencies = [ + "autocfg", + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys 0.48.0", +] + +[[package]] +name = "thirtyfour" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72fc70ad9624071cdd96d034676b84b504bfeb4bee1580df1324c99373ea0ca7" +dependencies = [ + "async-trait", + "base64 0.13.1", + "chrono", + "cookie", + "fantoccini", + "futures", + "http", + "log", + "parking_lot", + "serde", + "serde_json", + "serde_repr", + "stringmatch", + "thirtyfour-macros", + "thiserror", + "tokio", + "url", + "urlparse", +] + +[[package]] +name = "thirtyfour-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cae91d1c7c61ec65817f1064954640ee350a50ae6548ff9a1bdd2489d6ffbb0" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "thiserror" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[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.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc" +dependencies = [ + "itoa", + "libc", + "num_threads", + "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.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" +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.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94d7b1cfd2aa4011f2de74c2c4c63665e27a71006b0a192dcd2710272e73dfa2" +dependencies = [ + "autocfg", + "bytes", + "libc", + "mio", + "num_cpus", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +dependencies = [ + "rustls 0.20.8", + "tokio", + "webpki", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0d409377ff5b1e3ca6437aa86c1eb7d40c134bfec254e44c830defa92669db5" +dependencies = [ + "rustls 0.21.1", + "tokio", +] + +[[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 = "toml" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6135d499e69981f9ff0ef2167955a5333c35e36f6937d382974566b3d5b94ec" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380d56e8670370eee6566b0bfd4265f65b3f432e8c6d85623f728d4fa31f739" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d1d42a9b3f3ec46ba828e8d376aec14592ea199f70a06a548587ecd1c4ab658" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-range-header", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[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-core", +] + +[[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 = "try-lock" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + +[[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.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" + +[[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 = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "unsafe-libyaml" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1865806a559042e51ab5414598446a5871b561d21b6764f2eabb0dd481d880a6" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "url" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +dependencies = [ + "form_urlencoded", + "idna 0.4.0", + "percent-encoding", +] + +[[package]] +name = "urlparse" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "110352d4e9076c67839003c7788d8604e24dcded13e0b375af3efaa8cf468517" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "uuid" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2" +dependencies = [ + "getrandom", +] + +[[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 = "walkdir" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[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.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bba0e8cb82ba49ff4e229459ff22a191bbe9a1cb3a341610c9c33efc27ddf73" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b04bc93f9d6bdee709f6bd2118f57dd6679cf1176a1af464fca3ab0d66d8fb" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.18", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d1985d03709c53167ce907ff394f5316aa22cb4e12761295c5dc57dacb6297e" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14d6b024f1a526bb0234f52840389927257beb670610081360e5a03c5df9c258" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93" + +[[package]] +name = "web-sys" +version = "0.3.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bdd9ef4e984da1187bf8110c5cf5b845fbc87a23602cdf912386a76fcd3a7c2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webdriver" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9973cb72c8587d5ad5efdb91e663d36177dc37725e6c90ca86c626b0cc45c93f" +dependencies = [ + "base64 0.13.1", + "bytes", + "cookie", + "http", + "log", + "serde", + "serde_derive", + "serde_json", + "time 0.3.21", + "unicode-segmentation", + "url", +] + +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + +[[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-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[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.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +dependencies = [ + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[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.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[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.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[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.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[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.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[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.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[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.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "winnow" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61de7bac303dc551fe038e2b3cef0f571087a47571ea6e79a87692ac99b99699" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + +[[package]] +name = "yoake" +version = "0.1.0" +dependencies = [ + "aes-gcm", + "anyhow", + "argon2", + "askama", + "async-trait", + "axum", + "axum-server", + "base64 0.21.2", + "chrono", + "clap", + "diesel", + "diesel_migrations", + "flate2", + "hyper", + "lazy_static", + "lettre", + "log", + "rand_core", + "regex", + "reqwest", + "rust-embed", + "rustls-pemfile", + "serde", + "serde_json", + "serde_yaml", + "simple_logger", + "tempfile", + "thirtyfour", + "tokio", + "tokio-rustls 0.24.0", + "tower", + "tower-http", + "uuid", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..48b1dbf --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,52 @@ +[package] +name = "yoake" +version = "0.1.0" +edition = "2021" +authors = ["Yumechi "] +description = "Yoake is Yumechi's Personal Information Management System." + + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +aes-gcm = "0.10.2" +anyhow = "1.0.71" +argon2 = "0.5.0" +askama = { version = "0.12.0" } +async-trait = "0.1.68" +axum = { version = "0.6.18", features = ["http2"] } +axum-server = { version = "0.5.1", features = ["rustls", "rustls-pemfile", "tls-rustls"] } +base64 = "0.21.2" +chrono = { version = "0.4.26", features = ["serde"] } +clap = { version = "4.3.2", features = ["derive"] } +diesel = { version = "2.1.0", features = ["sqlite", "chrono"] } +diesel_migrations = { version = "2.1.0", features = ["sqlite"] } +flate2 = "1.0.26" +hyper = "0.14.26" +lazy_static = "1.4.0" +lettre = "0.10.4" +log = "0.4.18" +rand_core = { version = "0.6.4", features = ["getrandom"] } +regex = "1.8.4" +reqwest = { version = "0.11.18", features = ["json", "blocking"] } +rust-embed = "6.7.0" +rustls-pemfile = "1.0.2" +serde = { version = "1.0.163", features = ["derive"] } +serde_json = "1.0.96" +serde_yaml = "0.9.21" +simple_logger = "4.1.0" +tempfile = "3.6.0" +thirtyfour = "0.31.0" +tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread", "time"] } +tokio-rustls = "0.24.0" +tower = { version = "0.4.13", features = ["limit", "timeout", "buffer"] } +tower-http = { version = "0.4.0", features = ["timeout", "limit", "cors"] } +uuid = { version = "1.3.3", features = ["v4"] } + + +[profile.release] +lto = true + +[[bin]] +name = "yoake_server" +path = "src/bin/server.rs" diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..bc87205 --- /dev/null +++ b/config.yaml @@ -0,0 +1,45 @@ + +listen: + addr: 127.0.0.1:3000 + +db: + url: sqlite://data/development.db + +canvas_lms: + token: "1017~xxxxxxxxxxxx" + endpoint: "https://xxx.instructure.com/api/graphql" + refresh_interval: 300 + +comm: + gotify: + url: https://gotify.yumechi.jp + token: Axxxxxx + email: + from: "Yoake " + to: "yoake@yumechi.jp" + host: "smtp.example.com" + port: 587 + username: "yume@yumechi.jp" + password: "xxxx" + default_subject: "[ohime]お姫様のご注意" + +auth: + users: + yume: + roles: + - Admin + - User + password: $argon2id$v=19$m=19456,t=2,p=1$U7zg/pa1Wf9Hi9NM+ns9aA$tivXyIMw+wo9ZZoz0I+6yLm7+1SfkW9fF5hONy/qq1Y + test: + roles: + - User + password: $argon2id$v=19$m=19456,t=2,p=1$U7zg/pa1Wf9Hi9NM+ns9aA$tivXyIMw+wo9ZZoz0I+6yLm7+1SfkW9fF5hONy/qq1Y + +session: + secret: I3fEE8L65Ldk+BrjwPoQQu+skB7k3PlDg5uVx8EB + +webcheck: + utd_app: + username: user + password: xxxx + interval: 600 \ No newline at end of file diff --git a/diesel.toml b/diesel.toml new file mode 100644 index 0000000..c028f4a --- /dev/null +++ b/diesel.toml @@ -0,0 +1,9 @@ +# For documentation on how to configure this file, +# see https://diesel.rs/guides/configuring-diesel-cli + +[print_schema] +file = "src/schema.rs" +custom_type_derives = ["diesel::query_builder::QueryId"] + +[migrations_directory] +dir = "migrations" diff --git a/migrations/.keep b/migrations/.keep new file mode 100644 index 0000000..e69de29 diff --git a/migrations/2023-06-07-213214_session_store/down.sql b/migrations/2023-06-07-213214_session_store/down.sql new file mode 100644 index 0000000..8e8e82c --- /dev/null +++ b/migrations/2023-06-07-213214_session_store/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +drop table sessions; \ No newline at end of file diff --git a/migrations/2023-06-07-213214_session_store/up.sql b/migrations/2023-06-07-213214_session_store/up.sql new file mode 100644 index 0000000..90bd174 --- /dev/null +++ b/migrations/2023-06-07-213214_session_store/up.sql @@ -0,0 +1,6 @@ +-- Your SQL goes here +create table sessions ( + uuid text primary key not null, + expiry datetime not null, + content text not null +); diff --git a/migrations/2023-06-13-192016_med_management/down.sql b/migrations/2023-06-13-192016_med_management/down.sql new file mode 100644 index 0000000..c1a147d --- /dev/null +++ b/migrations/2023-06-13-192016_med_management/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +drop table medications; +drop table medication_logs; diff --git a/migrations/2023-06-13-192016_med_management/up.sql b/migrations/2023-06-13-192016_med_management/up.sql new file mode 100644 index 0000000..f95b6f1 --- /dev/null +++ b/migrations/2023-06-13-192016_med_management/up.sql @@ -0,0 +1,27 @@ +-- Your SQL goes here +create table medications ( + uuid text primary key not null, + name text not null, + dosage integer not null, + dosage_unit text not null, + period_hours integer not null, + flags text not null, + options text not null, + + created datetime not null, + updated datetime not null +); + +create table medication_logs ( + uuid text primary key not null, + med_uuid text not null, + dosage integer not null, + time_actual datetime not null, + time_expected datetime not null, + dose_offset real not null, + + created datetime not null, + updated datetime not null, + + FOREIGN KEY(med_uuid) REFERENCES medications(uuid) +) \ No newline at end of file diff --git a/scripts/deploy.fish b/scripts/deploy.fish new file mode 100755 index 0000000..96f315b --- /dev/null +++ b/scripts/deploy.fish @@ -0,0 +1,25 @@ +#!/usr/bin/env fish + +echo "Building UI..." +cd ui && yarn build && cd .. \ + || exit 1 + +echo "Building server..." +cargo build --release \ + || exit 1 + +echo "Copying files..." +scp target/release/yoake_server config-prod.yaml yoake: \ + || exit 1 + +echo "Deploying..." +ssh yoake " + echo 'Stopping server...' + sudo systemctl stop yoake-server + sudo mv yoake_server /var/lib/caddy/yoake/ + sudo mv config-prod.yaml /var/lib/caddy/yoake/config.yaml + sudo chown -R caddy:caddy /var/lib/caddy/yoake/ + echo 'Starting server...' + sudo systemctl start yoake-server + " \ + || exit 1 \ No newline at end of file diff --git a/scripts/dev.fish b/scripts/dev.fish new file mode 100755 index 0000000..1b8ae23 --- /dev/null +++ b/scripts/dev.fish @@ -0,0 +1,16 @@ +#!/usr/bin/env fish + +echo "Starting Server..." + +trap "echo 'Stopping Server...'; kill (cat .server_pid) && rm .server_pid; exit 0" SIGINT SIGTERM + +while true + + if [ -f .server_pid ] + kill (cat .server_pid) + end + + cargo run --bin yoake_server -- -c config-dev.yaml & echo $last_pid > .server_pid + inotifywait -e modify -e move -e create -e delete -r src \ + && kill (cat .server_pid) +end \ No newline at end of file diff --git a/src/apps/auth/middleware.rs b/src/apps/auth/middleware.rs new file mode 100644 index 0000000..8f8588f --- /dev/null +++ b/src/apps/auth/middleware.rs @@ -0,0 +1,115 @@ +use std::convert::Infallible; + +use axum::{async_trait, extract::FromRequestParts}; +use hyper::http::request::Parts; +use serde::{Deserialize, Serialize}; + +use crate::{http::ApiResponse, session::SessionStore}; + +use super::Role; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AuthInfo { + valid: bool, + user: String, + display_name: String, + roles: Vec, +} + +impl AuthInfo { + pub fn is_valid(&self) -> bool { + self.valid + } + pub fn user(&self) -> &str { + &self.user + } + pub fn display_name(&self) -> &str { + &self.display_name + } + pub fn roles(&self) -> &[Role] { + &self.roles + } + pub fn has_any_role(&self, roles: &[Role]) -> bool { + for role in roles { + if self.roles.contains(role) { + return true; + } + } + false + } + pub fn check_for_any_role(&self, roles: &[Role]) -> Result<(), ApiResponse<()>> { + if self.has_any_role(roles) { + Ok(()) + } else { + Err(ApiResponse::unauthorized("Unauthorized".to_string(), None)) + } + } +} + +impl Default for AuthInfo { + fn default() -> Self { + Self { + valid: false, + user: String::new(), + display_name: "anonymous".to_string(), + roles: Vec::new(), + } + } +} +#[async_trait] +impl FromRequestParts for AuthInfo +where + S: Send + Sync, +{ + type Rejection = Infallible; + + async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { + let reject = AuthInfo::default(); + + let Some(session) = parts.extensions.get::() else { + return Ok(reject); + }; + + if session.auth.user.is_empty() { + return Ok(reject); + } + let now = chrono::Utc::now(); + if session.auth.expire < now { + return Ok(reject); + } + + let mut res = AuthInfo::default(); + res.valid = true; + res.user = session.auth.user.clone(); + res.display_name = session.auth.user.clone(); + res.roles = session.auth.roles.clone(); + + Ok(res) + } +} + +/* +#[macro_export] +macro_rules! require_role { + ($auth:ident, $roles:expr) => { + if !$auth.has_any_role(&[$roles]) { + return ApiResponse::<()>::error( + format!("You do not have permission to access this resource. Acceptable role: {:?}, you have: {:?}", $roles, $auth.roles()), + 403, + None, + ) + .into_response(); + } + }; + ($auth:ident, [$($roles:expr),*]) => { + if !$auth.has_any_role(&[$($roles),*]) { + return ApiResponse::<()>::error( + format!("You do not have permission to access this resource. Acceptable roles: {:?}, you have: {:?}", [$($roles),*], $auth.roles()), + 403, + None, + ) + .into_response(); + } + }; +} +*/ diff --git a/src/apps/auth/mod.rs b/src/apps/auth/mod.rs new file mode 100644 index 0000000..9724b84 --- /dev/null +++ b/src/apps/auth/mod.rs @@ -0,0 +1,170 @@ +use std::{ + future::Future, + pin::Pin, + sync::{Arc, Mutex}, +}; + +use axum::{ + body::HttpBody, + error_handling::HandleErrorLayer, + http::Request, + response::{IntoResponse, Response}, + routing::{get, post}, + BoxError, Extension, Router, +}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use tower::ServiceBuilder; + +use crate::{ + apps::App, + config::Config, + http::{ApiResponse, JsonApiForm}, + session::SessionStore, + AppState, +}; + +use self::middleware::AuthInfo; + +pub mod middleware; +mod password; + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct SessionAuth { + pub user: String, + pub expire: DateTime, + pub roles: Vec, +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub enum Role { + Admin, + User, + Unknown, +} + +impl From<&str> for Role { + fn from(s: &str) -> Self { + match s { + "Admin" => Self::Admin, + "User" => Self::User, + _ => Self::Unknown, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LoginForm { + pub username: String, + pub password: String, +} + +pub async fn route_login( + app: Extension>, + Extension(mut session_store): Extension, + JsonApiForm(form): JsonApiForm, +) -> Result<(Extension, ApiResponse<()>), ApiResponse<()>> { + let failed_response = ApiResponse::<()>::error("Invalid credentials".to_string(), 401, None); + + let state = app.state.lock().unwrap(); + let state = state.as_ref().unwrap(); + + let Some(user) = state.config.auth.users.get(&form.username) else { + const DUMMY_HASH: &str = "$argon2id$v=19$m=19456,t=2,p=1$U7zg/pa1Wf9Hi9NM+ns9aA$tivXyIMw+wo9ZZoz0I+6yLm7+1SfkW9fF5hONy/qq1Y"; + password::verify_password(DUMMY_HASH, form.password.as_str()); + return Err(failed_response); + }; + + let hash = user.password.as_str(); + if !password::verify_password(hash, form.password.as_str()) { + return Err(failed_response); + } + + session_store.auth.user = form.username.to_string(); + session_store.auth.expire = Utc::now() + chrono::Duration::days(7); + session_store.auth.roles = user.roles.iter().map(|r| r.as_str().into()).collect(); + + Ok(( + Extension(session_store), + ApiResponse::ok("Login successful".to_string(), None), + )) +} + +pub async fn route_logout( + Extension(mut session_store): Extension, +) -> Result<(Extension, ApiResponse<()>), ApiResponse<()>> { + session_store.auth.user = String::new(); + + Ok(( + Extension(session_store), + ApiResponse::ok("Logout successful".to_string(), None), + )) +} + +pub async fn route_self(auth: AuthInfo, _req: Request) -> Response +where + ::Error: std::fmt::Debug, +{ + ApiResponse::ok("".to_string(), Some(auth)).into_response() +} + +pub struct AuthApp { + state: Mutex>, +} + +pub struct AuthAppState { + config: &'static Config, + _app_state: Arc>, +} + +impl AuthApp { + pub fn new() -> Self { + Self { + state: Mutex::new(None), + } + } +} + +impl App for AuthApp { + fn initialize( + self: Arc, + config: &'static Config, + app_state: Arc>, + ) -> Pin>> { + let mut state = self.state.lock().unwrap(); + *state = Some(AuthAppState { + config, + _app_state: app_state, + }); + + Box::pin(async {}) + } + + fn api_routes(self: Arc) -> Router { + let rate_limiter = tower::limit::RateLimitLayer::new(1, std::time::Duration::from_secs(1)); + Router::new() + .route( + "/auth/hash_password", + post(password::route_hash_password) + // rate limit + .layer( + ServiceBuilder::new() + .layer(HandleErrorLayer::new(|err: BoxError| async move { + log::error!("Error: {:?}", err); + ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + "Internal Server Error".to_string(), + ) + })) + .buffer(64) + .concurrency_limit(1) + .rate_limit(1, std::time::Duration::from_secs(1)) + .layer(rate_limiter), + ), + ) + .route("/auth/logout", post(route_logout)) + .route("/auth/login", post(route_login)) + .route("/auth/self", get(route_self)) + .layer(Extension(self.clone())) + } +} diff --git a/src/apps/auth/password.rs b/src/apps/auth/password.rs new file mode 100644 index 0000000..67aefcb --- /dev/null +++ b/src/apps/auth/password.rs @@ -0,0 +1,54 @@ +use argon2::{ + password_hash::{rand_core::OsRng, SaltString}, + Argon2, PasswordHasher, PasswordVerifier, +}; +use serde::{Deserialize, Serialize}; + +use crate::http::{ApiResponse, ApiResult, JsonApiForm}; + +pub fn hash_password(password: &str) -> String { + let salt = SaltString::generate(&mut OsRng); + + let argon2 = Argon2::default(); + argon2 + .hash_password(password.as_bytes(), &salt) + .unwrap() + .to_string() +} + +pub fn verify_password(hash: &str, password: &str) -> bool { + let argon2 = Argon2::default(); + let hash = argon2::PasswordHash::new(hash).unwrap(); + argon2.verify_password(password.as_bytes(), &hash).is_ok() +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct HashPasswordForm { + password: String, +} + +pub async fn route_hash_password( + JsonApiForm(form): JsonApiForm, +) -> ApiResult { + let hash = hash_password(&form.password); + + Ok(ApiResponse::ok("".to_string(), Some(hash))) +} +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_hash_password() { + let password = "password"; + let hash = hash_password(password); + assert!(verify_password(&hash, password)); + } + + #[test] + fn test_hash_invalid_password() { + let password = "password"; + let hash = hash_password(password); + assert!(!verify_password(&hash, "invalid")); + } +} diff --git a/src/apps/canvas_lms/comm.rs b/src/apps/canvas_lms/comm.rs new file mode 100644 index 0000000..523ad93 --- /dev/null +++ b/src/apps/canvas_lms/comm.rs @@ -0,0 +1,71 @@ +use askama::Template; +use chrono::{Local, TimeZone}; + +use super::grading::Grading; + +#[derive(Template)] +#[template(path = "grades.html")] +pub struct GradesTemplate<'a> { + pub grades: &'a [TemplateGrade], +} + +pub struct TemplateGrade { + pub submission_username: String, + pub course_code: String, + pub assignment_url: String, + pub name: String, + pub due: String, + pub grade: String, + pub graded_at: String, + pub posted_at: String, +} + +impl From<&Grading> for TemplateGrade { + fn from(grading: &Grading) -> Self { + Self { + submission_username: grading.submission_username.clone(), + course_code: grading.course_code.clone(), + assignment_url: grading.assignment_url.clone(), + name: grading.name.clone(), + due: grading + .due_at + .map(|d| { + Local + .from_utc_datetime(&d.naive_utc()) + .format("%Y-%m-%d %H:%M") + .to_string() + }) + .unwrap_or_else(|| "N/A".to_string()), + grade: if grading.grade_hidden { + "Hidden".to_string() + } else if grading.score.is_none() { + "Not Graded".to_string() + } else { + format!( + "{:.2} ({})/ {:.2}", + grading.score.unwrap(), + grading.grade, + grading.possible_points + ) + }, + graded_at: grading + .graded_at + .map(|d| { + Local + .from_utc_datetime(&d.naive_utc()) + .format("%Y-%m-%d %H:%M") + .to_string() + }) + .unwrap_or_else(|| "N/A".to_string()), + posted_at: grading + .posted_at + .map(|d| { + Local + .from_utc_datetime(&d.naive_utc()) + .format("%Y-%m-%d %H:%M") + .to_string() + }) + .unwrap_or_else(|| "N/A".to_string()), + } + } +} diff --git a/src/apps/canvas_lms/grading.rs b/src/apps/canvas_lms/grading.rs new file mode 100644 index 0000000..b56b301 --- /dev/null +++ b/src/apps/canvas_lms/grading.rs @@ -0,0 +1,127 @@ +use chrono::{DateTime, Utc}; +use serde::Serialize; + +#[derive(Debug, Clone, Serialize)] +pub struct Grading { + pub name: String, + pub course_name: String, + pub course_code: String, + + pub submission_username: String, + + pub assignment_id: String, + pub assignment_legacy_id: String, + pub assignment_url: String, + pub submission_id: String, + pub submission_legacy_id: String, + pub course_id: String, + pub course_legacy_id: String, + + pub due_at: Option>, + pub state: String, + pub score: Option, + pub entered_score: Option, + pub possible_points: f64, + pub grade_hidden: bool, + pub grade: String, + pub entered_grade: String, + + pub graded_at: Option>, + pub posted_at: Option>, +} + +impl Grading { + pub fn last_updated(&self) -> Option> { + [self.graded_at, self.posted_at] + .iter() + .filter_map(|d| *d) + .max() + } +} + +impl Grading { + pub fn find_updates<'a>(before: &[Grading], after: &'a [Grading]) -> Vec<&'a Grading> { + let mut updates = Vec::new(); + for new in after { + if let Some(old) = before + .iter() + .find(|old| old.submission_id == new.submission_id) + { + if old.last_updated() < new.last_updated() { + updates.push(new); + } + } else { + updates.push(new); + } + } + updates + } +} + +impl PartialEq for Grading { + fn eq(&self, other: &Self) -> bool { + self.submission_id == other.submission_id && self.last_updated() == other.last_updated() + } +} + +impl PartialOrd for Grading { + fn partial_cmp(&self, other: &Self) -> Option { + if self == other { + Some(std::cmp::Ordering::Equal) + } else { + self.last_updated().partial_cmp(&other.last_updated()) + } + } +} + +impl Eq for Grading { + fn assert_receiver_is_total_eq(&self) {} +} + +impl Ord for Grading { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.partial_cmp(other).unwrap() + } +} + +pub struct CanvasGradingData(pub super::graph::AllCourseData); + +impl IntoIterator for CanvasGradingData { + type Item = Grading; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + let mut vec = Vec::new(); + for course in self.0.all_courses { + for submission in course.submissions_connection.nodes { + vec.push(Grading { + name: submission.assignment.name, + course_name: course.name.clone(), + course_code: course.course_code.clone(), + due_at: submission.assignment.due_at.map(|d| d.into()), + + submission_username: submission.user.name.clone(), + + assignment_id: submission.assignment.id.clone(), + assignment_legacy_id: submission.assignment.id_legacy.clone(), + assignment_url: submission.assignment.html_url.clone(), + submission_id: submission.id.clone(), + submission_legacy_id: submission.id_legacy.clone(), + course_id: course.id.clone(), + course_legacy_id: course.id_legacy.clone(), + + state: course.state.clone(), + score: submission.score, + entered_score: submission.entered_score, + possible_points: submission.assignment.points_possible, + grade_hidden: submission.grade_hidden, + grade: submission.grade.unwrap_or_else(|| "".to_string()), + entered_grade: submission.entered_grade.unwrap_or_else(|| "".to_string()), + graded_at: submission.graded_at.map(|d| d.into()), + posted_at: submission.posted_at.map(|d| d.into()), + }); + } + } + vec.into_iter() + } +} diff --git a/src/apps/canvas_lms/graph.rs b/src/apps/canvas_lms/graph.rs new file mode 100644 index 0000000..b151cd7 --- /dev/null +++ b/src/apps/canvas_lms/graph.rs @@ -0,0 +1,132 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize)] +pub struct GraphQuery { + pub query: String, + #[serde(rename = "operationName")] + pub operation_name: String, + pub variables: T, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct GraphNodes { + pub nodes: Vec, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct GraphResponse { + pub data: T, +} + +pub const ALL_COURSES_QUERY: &str = "query gradeQuery { + allCourses { + _id + id + name + state + courseCode + submissionsConnection(first: $maxn$, orderBy: {field: gradedAt, direction: descending}) { + nodes { + _id + id + assignment { + _id + id + name + dueAt + gradingType + pointsPossible + htmlUrl + } + score + enteredScore + grade + enteredGrade + gradingStatus + gradeHidden + gradedAt + posted + postedAt + state + user { + _id + id + name + sisId + email + } + } + } + } + }"; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct AllCourseData { + #[serde(rename = "allCourses")] + pub all_courses: Vec, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct GraphCourse { + #[serde(rename = "_id")] + pub id_legacy: String, + pub id: String, + pub name: String, + pub state: String, + #[serde(rename = "courseCode")] + pub course_code: String, + #[serde(rename = "submissionsConnection")] + pub submissions_connection: GraphNodes, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct GraphSubmission { + #[serde(rename = "_id")] + pub id_legacy: String, + pub id: String, + pub assignment: GraphAssignment, + pub score: Option, + #[serde(rename = "enteredScore")] + pub entered_score: Option, + pub grade: Option, + #[serde(rename = "enteredGrade")] + pub entered_grade: Option, + #[serde(rename = "gradingStatus")] + pub grading_status: Option, + #[serde(rename = "gradeHidden")] + pub grade_hidden: bool, + #[serde(rename = "gradedAt")] + pub graded_at: Option>, + pub posted: bool, + #[serde(rename = "postedAt")] + pub posted_at: Option>, + pub state: String, + pub user: GraphUser, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct GraphAssignment { + #[serde(rename = "_id")] + pub id_legacy: String, + pub id: String, + pub name: String, + #[serde(rename = "dueAt")] + pub due_at: Option>, + #[serde(rename = "gradingType")] + pub grading_type: String, + #[serde(rename = "pointsPossible")] + pub points_possible: f64, + #[serde(rename = "htmlUrl")] + pub html_url: String, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct GraphUser { + #[serde(rename = "_id")] + pub id_legacy: String, + pub id: String, + #[serde(rename = "sisId")] + pub sis_id: Option, + pub name: String, + pub email: Option, +} diff --git a/src/apps/canvas_lms/mod.rs b/src/apps/canvas_lms/mod.rs new file mode 100644 index 0000000..52337c2 --- /dev/null +++ b/src/apps/canvas_lms/mod.rs @@ -0,0 +1,220 @@ +use std::{ + future::Future, + pin::Pin, + sync::{Arc, Mutex}, +}; + +use askama::Template; +use axum::{body::HttpBody, extract::Query, http::Request, routing::get, Extension, Router}; +use log::{debug, info}; +use serde::{Deserialize, Serialize}; +use tokio::sync::Mutex as AsyncMutex; + +use crate::{ + apps::{ + auth::Role, + canvas_lms::{ + comm::{GradesTemplate, TemplateGrade}, + grading::CanvasGradingData, + }, + }, + comm::{Communicator, Message}, + config::Config, + AppState, +}; + +use self::{ + grading::Grading, + graph::{AllCourseData, GraphQuery, GraphResponse, ALL_COURSES_QUERY}, +}; + +use crate::{ + apps::{auth::middleware::AuthInfo, App}, + http::{ApiResponse, ApiResult}, +}; + +mod comm; +pub mod grading; +mod graph; + +pub async fn query_grades( + endpoint: &str, + token: &str, + maxn: i32, +) -> Result, reqwest::Error> { + let client = reqwest::Client::new(); + let query = GraphQuery { + query: ALL_COURSES_QUERY + .to_string() + .replace("$maxn$", &maxn.to_string()), + operation_name: "gradeQuery".to_string(), + variables: (), + }; + let res = client + .post(endpoint) + .bearer_auth(token) + .json(&query) + .send() + .await? + .json::>() + .await?; + Ok(res) +} + +pub struct CanvasLMSApp { + state: AsyncMutex, +} + +struct CanvasLMSAppState { + config: Option<&'static Config>, + grade_cache: GradeCache, + global_app_state: Option>>, +} + +#[derive(Debug, Clone, Serialize)] +struct GradeCache { + last_updated: chrono::DateTime, + response: Option>, +} + +#[derive(Debug, Deserialize)] +struct GetGradesOptions { + force_refresh: Option, +} + +async fn route_get_grades( + auth: AuthInfo, + app: Extension>, + Query(query): Query, + _req: Request, +) -> ApiResult +where + ::Error: std::fmt::Debug, +{ + auth.check_for_any_role(&[Role::Admin])?; + + if Some(true) == query.force_refresh || !app.grade_loaded().await { + app.refresh_grades().await; + } + let state = app.state.lock().await; + let grade_cache = state.grade_cache.to_owned(); + if grade_cache.response.is_none() { + return Err(ApiResponse::<()>::error( + "Grades not available yet".to_string(), + 503, + None, + )); + } + Ok(ApiResponse::ok( + "Grades retrieved successfully".to_string(), + Some(grade_cache), + )) +} + +impl CanvasLMSApp { + pub fn new() -> Self { + Self { + state: AsyncMutex::new(CanvasLMSAppState { + config: None, + grade_cache: GradeCache { + last_updated: chrono::Local::now(), + response: None, + }, + global_app_state: None, + }), + } + } + pub(crate) async fn grade_loaded(&self) -> bool { + let state: tokio::sync::MutexGuard = self.state.lock().await; + state.grade_cache.response.is_some() + } + pub(crate) async fn refresh_grades(&self) { + let mut state = self.state.lock().await; + let config = state.config.unwrap(); + let res = query_grades(&config.canvas_lms.endpoint, &config.canvas_lms.token, 50).await; + match res { + Ok(res) => { + let mut res_generalized = + CanvasGradingData(res.data).into_iter().collect::>(); + res_generalized.sort_unstable(); + res_generalized.reverse(); + debug!("Finished refreshing grades"); + if let Some(old_grades) = &state.grade_cache.response { + let updates = Grading::find_updates(old_grades, &res_generalized); + if !updates.is_empty() { + let templated_grades: Vec = + res_generalized.iter().map(|g| g.into()).collect(); + let template_ctx = GradesTemplate { + grades: templated_grades.as_ref(), + }; + let template_rendered = template_ctx.render().unwrap(); + let global_app_state = + state.global_app_state.as_ref().unwrap().lock().unwrap(); + let email_result = global_app_state.comm.send_message(&Message { + subject: "New grades available".to_string(), + body: template_rendered, + mime: "text/html", + ..Default::default() + }); + match email_result { + Ok(_) => { + info!("Sent email notification for new grades"); + } + Err(e) => { + log::error!("Error sending email notification: {}", e); + } + } + } + } + state.grade_cache.last_updated = chrono::Local::now(); + state.grade_cache.response = Some(res_generalized); + } + Err(e) => { + log::error!("Error querying Canvas LMS: {}", e); + } + } + } +} + +impl App for CanvasLMSApp { + fn initialize( + self: Arc, + config: &'static Config, + app_state: Arc>, + ) -> Pin>> { + let self_clone = self.clone(); + + let refresh_interval = config.canvas_lms.refresh_interval; + if refresh_interval == 0 { + panic!("Canvas LMS refresh interval cannot be 0"); + } + + let init_async = Box::pin(async move { + let mut state = self.state.lock().await; + state.global_app_state = Some(app_state); + state.config = Some(config); + state.grade_cache = GradeCache { + last_updated: chrono::Local::now(), + response: None, + }; + + tokio::spawn(async move { + let mut ticker = + tokio::time::interval(std::time::Duration::from_secs(refresh_interval)); + ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); + loop { + self_clone.refresh_grades().await; + ticker.tick().await; + } + }); + }); + + init_async + } + + fn api_routes(self: Arc) -> Router { + Router::new() + .route("/canvas_lms/grades", get(route_get_grades)) + .layer(Extension(self.clone())) + } +} diff --git a/src/apps/med/directive.rs b/src/apps/med/directive.rs new file mode 100644 index 0000000..c2f2612 --- /dev/null +++ b/src/apps/med/directive.rs @@ -0,0 +1,176 @@ +use std::sync::Arc; + +use crate::{ + apps::auth::{middleware::AuthInfo, Role}, + http::{ApiResponse, ApiResult, JsonApiForm}, + models::med::Medication, +}; + +use axum::{extract::Path, Extension}; +use log::error; +use serde::{Deserialize, Serialize}; + +use super::MedManagementApp; + +#[derive(Debug, Serialize, Deserialize)] +pub struct ParseShorthandForm { + pub shorthand: String, +} + +pub async fn route_parse_shorthand( + JsonApiForm(form): JsonApiForm, +) -> ApiResult { + let res = form.shorthand.parse::().map_err(|e| { + error!("Failed to parse shorthand: {}", e); + ApiResponse::bad_request("Failed to parse".to_string(), None) + })?; + Ok(ApiResponse::ok( + "Parsed successfully".to_string(), + Some(res), + )) +} + +pub async fn route_format_shorthand( + JsonApiForm(form): JsonApiForm, +) -> ApiResult { + let shorthand: String = form.into(); + Ok(ApiResponse::ok( + "Formatted successfully".to_string(), + Some(shorthand), + )) +} + +pub async fn route_get_directive( + auth: AuthInfo, + app: Extension>, +) -> ApiResult> { + auth.check_for_any_role(&[Role::Admin])?; + + let state = app.state.lock().await; + let state = state.as_ref().unwrap(); + let mut global_app_state = state.global_app_state.lock().unwrap(); + + let meds = { + use crate::schema::medications::dsl::*; + use diesel::prelude::*; + + medications + .load::(&mut global_app_state.db) + .map_err(|e| { + error!("Failed to load meds: {:?}", e); + ApiResponse::error("Database error".to_string(), 500, None) + })? + }; + + Ok(ApiResponse::ok( + "Directives retrieved".to_string(), + Some(meds), + )) +} + +pub async fn route_post_directive( + auth: AuthInfo, + app: Extension>, + JsonApiForm(mut form): JsonApiForm, +) -> ApiResult { + auth.check_for_any_role(&[Role::Admin])?; + + let state = app.state.lock().await; + let state = state.as_ref().unwrap(); + let mut global_app_state = state.global_app_state.lock().unwrap(); + + form.uuid = uuid::Uuid::new_v4().to_string(); + form.created = chrono::Utc::now().naive_local(); + form.updated = chrono::Utc::now().naive_local(); + + let res = { + use crate::schema::medications; + use crate::schema::medications::dsl::*; + use diesel::prelude::*; + + diesel::insert_into(medications::table) + .values(&form) + .execute(&mut global_app_state.db) + .unwrap(); + + medications + .filter(medications::uuid.eq(&form.uuid)) + .first::(&mut global_app_state.db) + .unwrap() + }; + + Ok(ApiResponse::ok("Directives posted".to_string(), Some(res))) +} + +pub async fn route_patch_directive( + auth: AuthInfo, + app: Extension>, + JsonApiForm(form): JsonApiForm, +) -> ApiResult { + auth.check_for_any_role(&[Role::Admin])?; + + let state = app.state.lock().await; + let state = state.as_ref().unwrap(); + let mut global_app_state = state.global_app_state.lock().unwrap(); + + let res = { + use crate::schema::medications; + use crate::schema::medications::dsl::*; + use diesel::prelude::*; + + diesel::update(medications.filter(medications::uuid.eq(&form.uuid))) + .set(( + name.eq(&form.name), + dosage.eq(&form.dosage), + dosage_unit.eq(&form.dosage_unit), + period_hours.eq(&form.period_hours), + flags.eq(&form.flags), + options.eq(&form.options), + updated.eq(chrono::Utc::now().naive_local()), + )) + .execute(&mut global_app_state.db) + .map_err(|e| { + error!("Failed to update med: {:?}", e); + ApiResponse::error("Database error".to_string(), 500, None) + })?; + + medications + .filter(medications::uuid.eq(&form.uuid)) + .first::(&mut global_app_state.db) + .map_err(|e| { + error!("Failed to load med: {:?}", e); + ApiResponse::error("Database error".to_string(), 500, None) + })? + }; + + Ok(ApiResponse::ok("Directives updated".to_string(), Some(res))) +} + +pub async fn route_delete_directive( + auth: AuthInfo, + app: Extension>, + Path(med_uuid): Path, +) -> ApiResult<()> { + auth.check_for_any_role(&[Role::Admin])?; + + let state = app.state.lock().await; + let state = state.as_ref().unwrap(); + let mut global_app_state = state.global_app_state.lock().unwrap(); + + { + use crate::schema::medications::dsl::medications; + use diesel::prelude::*; + + diesel::delete(medications.filter(crate::schema::medications::uuid.eq(&med_uuid))) + .execute(&mut global_app_state.db) + .map_err(|e| { + error!("Failed to delete med: {:?}", e); + ApiResponse::error("Database error".to_string(), 500, None) + })?; + }; + + Ok(ApiResponse::<()>::ok( + "Directives deleted".to_string(), + None, + )) +} diff --git a/src/apps/med/log.rs b/src/apps/med/log.rs new file mode 100644 index 0000000..ca3762c --- /dev/null +++ b/src/apps/med/log.rs @@ -0,0 +1,270 @@ +use std::sync::Arc; + +use crate::{ + apps::auth::{middleware::AuthInfo, Role}, + http::{ApiResponse, ApiResult, JsonApiForm}, + models::med::{Medication, MedicationLog}, +}; + +use axum::{ + extract::{Path, Query}, + Extension, +}; +use chrono::{Duration, TimeZone, Utc}; +use log::error; +use serde::{Deserialize, Serialize}; + +use super::MedManagementApp; + +pub fn effective_last_dose( + med: &Medication, + mut med_logs: Vec, +) -> Option { + let mut remaining_dose = med.dosage; + med_logs.sort_by(|a, b| a.time_actual.cmp(&b.time_actual)); + med_logs.reverse(); + + for log in med_logs { + if log.dosage >= remaining_dose { + return Some(log); + } else { + remaining_dose -= log.dosage; + } + } + + None +} + +pub fn project_next_dose(med: &Medication, med_logs: Vec) -> MedicationLog { + let effective_last = effective_last_dose(med, med_logs); + + let now = Utc::now().naive_utc(); + + let next_time = match effective_last { + Some(last) => last.time_actual + Duration::hours(med.period_hours as i64), + None => now, + }; + + let offset = (now.timestamp_millis() - next_time.timestamp_millis()) as f64 + / (med.period_hours * 60 * 60 * 1000) as f64; + + MedicationLog { + uuid: uuid::Uuid::new_v4().to_string(), + med_uuid: med.uuid.clone(), + dosage: med.dosage, + time_actual: now, + time_expected: next_time, + dose_offset: offset as f32, + created: now, + updated: now, + } +} + +pub async fn route_project_next_dose( + auth: AuthInfo, + app: Extension>, + Path(med_uuid_form): Path, +) -> ApiResult { + auth.check_for_any_role(&[Role::Admin])?; + + let state = app.state.lock().await; + let state = state.as_ref().unwrap(); + let mut global_app_state = state.global_app_state.lock().unwrap(); + + let med = { + use crate::schema::medications::dsl::*; + use diesel::prelude::*; + + medications + .filter(uuid.eq(&med_uuid_form)) + .first::(&mut global_app_state.db) + .map_err(|e| { + error!("Failed to get med: {}", e); + ApiResponse::error("Failed to get med".to_string(), 500, None) + })? + }; + + let med_logs = { + use crate::schema::medication_logs::dsl::*; + use diesel::prelude::*; + + medication_logs + .order_by(time_actual.desc()) + .limit(10) + .filter(med_uuid.eq(&med_uuid_form)) + .load::(&mut global_app_state.db) + .map_err(|e| { + error!("Failed to get med logs: {}", e); + ApiResponse::error("Failed to get med logs".to_string(), 500, None) + })? + }; + + let next_dose = project_next_dose(&med, med_logs); + Ok(ApiResponse::ok( + "Next dose projected successfully".to_string(), + Some(next_dose), + )) +} + +#[derive(Serialize, Deserialize)] +pub struct GetLogOptions { + until: Option, + limit: i64, +} + +pub async fn route_get_log( + auth: AuthInfo, + app: Extension>, + Path(med_uuid_form): Path, + Query(GetLogOptions { until, limit }): Query, +) -> ApiResult> { + auth.check_for_any_role(&[Role::Admin])?; + + let state = app.state.lock().await; + let state = state.as_ref().unwrap(); + let mut global_app_state = state.global_app_state.lock().unwrap(); + + let med_logs = { + use crate::schema::medication_logs::dsl::*; + use diesel::prelude::*; + + match until { + None => medication_logs + .limit(limit) + .order_by(time_actual.desc()) + .filter(med_uuid.eq(&med_uuid_form)) + .load::(&mut global_app_state.db) + .map_err(|e| { + error!("Failed to get med logs: {}", e); + ApiResponse::error("Failed to get med logs".to_string(), 500, None) + })?, + Some(until) => { + let until = Utc.timestamp_millis_opt(until).unwrap().naive_utc(); + medication_logs + .limit(limit) + .order_by(time_actual.desc()) + .filter(med_uuid.eq(&med_uuid_form).and(time_actual.lt(until))) + .load::(&mut global_app_state.db) + .map_err(|e| { + error!("Failed to get med logs: {}", e); + ApiResponse::error("Failed to get med logs".to_string(), 500, None) + })? + } + } + }; + + Ok(ApiResponse::ok( + "Med logs retrieved successfully".to_string(), + Some(med_logs), + )) +} + +pub async fn route_post_log( + auth: AuthInfo, + app: Extension>, + Path(med_uuid_form): Path, + JsonApiForm(form): JsonApiForm, +) -> ApiResult { + auth.check_for_any_role(&[Role::Admin])?; + + let state = app.state.lock().await; + let state = state.as_ref().unwrap(); + let mut global_app_state = state.global_app_state.lock().unwrap(); + + let med = { + use crate::schema::medications::dsl::*; + use diesel::prelude::*; + + medications + .filter(uuid.eq(&med_uuid_form)) + .first::(&mut global_app_state.db) + .map_err(|e| { + error!("Failed to get med: {}", e); + ApiResponse::error("Failed to get med".to_string(), 500, None) + })? + }; + + let med_logs = { + use crate::schema::medication_logs::dsl::*; + use diesel::prelude::*; + + medication_logs + .order_by(time_actual.desc()) + .limit(10) + .filter( + med_uuid + .eq(&med_uuid_form) + .and(time_actual.lt(form.time_actual)), + ) + .load::(&mut global_app_state.db) + .map_err(|e| { + error!("Failed to get med logs: {}", e); + ApiResponse::error("Failed to get med logs".to_string(), 500, None) + })? + }; + + let projected_next_dose = project_next_dose(&med, med_logs); + let mut form = form; + + form.med_uuid = med_uuid_form; + form.time_expected = projected_next_dose.time_expected; + form.dose_offset = projected_next_dose.dose_offset; + let now = Utc::now().naive_utc(); + form.created = now; + form.updated = now; + + let log = { + use crate::schema::medication_logs::dsl::*; + use diesel::prelude::*; + + diesel::insert_into(medication_logs) + .values(form.clone()) + .execute(&mut global_app_state.db) + .map_err(|e| { + error!("Failed to insert log: {}", e); + ApiResponse::error("Failed to insert log".to_string(), 500, None) + })?; + + medication_logs + .filter(uuid.eq(form.uuid)) + .first::(&mut global_app_state.db) + .map_err(|e| { + error!("Failed to get log: {}", e); + ApiResponse::error("Failed to get log".to_string(), 500, None) + })? + }; + + Ok(ApiResponse::ok( + "Log inserted successfully".to_string(), + Some(log), + )) +} + +pub async fn route_delete_log( + auth: AuthInfo, + app: Extension>, + Path((_med_uuid, log_uuid)): Path<(String, String)>, +) -> ApiResult<()> { + auth.check_for_any_role(&[Role::Admin])?; + + let state = app.state.lock().await; + let state = state.as_ref().unwrap(); + let mut global_app_state = state.global_app_state.lock().unwrap(); + + { + use crate::schema::medication_logs::dsl::*; + use diesel::prelude::*; + + diesel::delete(medication_logs.filter(uuid.eq(log_uuid))) + .execute(&mut global_app_state.db) + .map_err(|e| { + error!("Failed to delete log: {}", e); + ApiResponse::error("Failed to delete log".to_string(), 500, None) + })?; + } + + Ok(ApiResponse::ok( + "Log deleted successfully".to_string(), + None, + )) +} diff --git a/src/apps/med/mod.rs b/src/apps/med/mod.rs new file mode 100644 index 0000000..9ad1079 --- /dev/null +++ b/src/apps/med/mod.rs @@ -0,0 +1,84 @@ +use std::{ + future::Future, + pin::Pin, + sync::{Arc, Mutex}, +}; + +use crate::{config::Config, AppState}; + +use super::App; + +use axum::{ + routing::{delete, get, post}, + Extension, Router, +}; +use tokio::sync::Mutex as AsyncMutex; + +mod directive; +mod log; + +pub struct MedManagementApp { + state: AsyncMutex>, +} + +struct MedManagementAppState { + global_app_state: Arc>, +} + +impl MedManagementApp { + pub fn new() -> Self { + Self { + state: AsyncMutex::new(None), + } + } +} + +impl App for MedManagementApp { + fn initialize( + self: Arc, + _config: &'static Config, + app_state: Arc>, + ) -> Pin>> { + Box::pin(async move { + let mut state = self.state.lock().await; + *state = Some(MedManagementAppState { + global_app_state: app_state, + }); + }) + } + + fn api_routes(self: Arc) -> Router { + Router::new() + .route( + "/med/parse_shorthand", + post(directive::route_parse_shorthand), + ) + .route( + "/med/format_shorthand", + post(directive::route_format_shorthand), + ) + .route( + "/med/directive", + get(directive::route_get_directive) + .post(directive::route_post_directive) + .patch(directive::route_patch_directive), + ) + .route( + "/med/directive/:med_uuid", + delete(directive::route_delete_directive), + ) + .route( + "/med/directive/:med_uuid/project_next_dose", + get(log::route_project_next_dose), + ) + .route( + "/med/directive/:med_uuid/log", + get(log::route_get_log).post(log::route_post_log), + ) + .route( + "/med/directive/:med_uuid/log/:log_uuid", + delete(log::route_delete_log), + ) + .layer(Extension(self.clone())) + } +} diff --git a/src/apps/mod.rs b/src/apps/mod.rs new file mode 100644 index 0000000..f47c788 --- /dev/null +++ b/src/apps/mod.rs @@ -0,0 +1,27 @@ +use crate::{config::Config, AppState}; +use axum::Router; + +use std::{ + future::Future, + pin::Pin, + sync::{Arc, Mutex}, +}; + +pub trait App { + fn initialize( + self: Arc, + _config: &'static Config, + _app_state: Arc>, + ) -> Pin>> { + Box::pin(async {}) + } + fn api_routes(self: Arc) -> Router { + Router::new() + } +} + +pub mod auth; +pub mod canvas_lms; +pub mod med; +pub mod server_info; +pub mod webcheck; diff --git a/src/apps/server_info/mod.rs b/src/apps/server_info/mod.rs new file mode 100644 index 0000000..060a936 --- /dev/null +++ b/src/apps/server_info/mod.rs @@ -0,0 +1,51 @@ +use std::sync::Arc; + +use crate::{apps::App, http::ApiResponse}; +use axum::{ + body::HttpBody, + http::Request, + response::{IntoResponse, Response}, + routing::get, + Router, +}; +use serde::{Deserialize, Serialize}; + +pub struct ServerInfoApp {} + +#[cfg(debug_assertions)] +const PROFILE: &str = "debug"; +#[cfg(not(debug_assertions))] +const PROFILE: &str = "release"; + +#[derive(Debug, Serialize, Deserialize)] +pub struct ServerInfo { + version: String, + profile: String, +} + +async fn get_server_info(_req: Request) -> Response +where + ::Error: std::fmt::Debug, +{ + let server_info = ServerInfo { + version: env!("CARGO_PKG_VERSION").to_string(), + profile: PROFILE.to_string(), + }; + ApiResponse::ok( + "Server info retrieved successfully".to_string(), + Some(server_info), + ) + .into_response() +} + +impl ServerInfoApp { + pub fn new() -> Self { + Self {} + } +} + +impl App for ServerInfoApp { + fn api_routes(self: Arc) -> Router { + Router::new().route("/server_info", get(get_server_info)) + } +} diff --git a/src/apps/webcheck/driver/chrome.rs b/src/apps/webcheck/driver/chrome.rs new file mode 100644 index 0000000..98cb916 --- /dev/null +++ b/src/apps/webcheck/driver/chrome.rs @@ -0,0 +1,67 @@ +use rand_core::RngCore; +use std::process::{Child, Command}; +use thirtyfour::{prelude::WebDriverResult, ChromeCapabilities, WebDriver}; +pub struct ChromeDriver { + child: Option, + port: u16, + tmp_dir: Option, +} + +impl ChromeDriver { + pub fn new() -> Self { + let port = rand_core::OsRng.next_u32() as u16 + 10000; + Self::new_port(port) + } + pub fn new_port(port: u16) -> Self { + Self { + child: None, + port, + tmp_dir: None, + } + } + + pub fn spawn(&mut self, args: &[&str]) -> anyhow::Result<()> { + let tmp_dir = tempfile::tempdir()?; + std::fs::create_dir(tmp_dir.path().join("data"))?; + std::fs::create_dir(tmp_dir.path().join("crashpad"))?; + self.tmp_dir = Some(tmp_dir); + + let mut cmd = Command::new("chromedriver"); + cmd.arg(format!("--port={}", self.port)); + cmd.args(args); + + let child = cmd.spawn()?; + + self.child = Some(child); + + Ok(()) + } + + pub async fn connect(&mut self, mut caps: ChromeCapabilities) -> WebDriverResult { + let temp_dir = self.tmp_dir.as_ref().unwrap(); + + caps.add_chrome_arg( + format!("--user-data-dir={}", temp_dir.path().join("data").display()).as_str(), + )?; + caps.add_chrome_arg( + format!( + "--crash-dumps-dir={}", + temp_dir.path().join("crashpad").display() + ) + .as_str(), + )?; + + let addr = format!("http://localhost:{}/", self.port); + WebDriver::new(addr.as_str(), caps).await + } +} + +impl Drop for ChromeDriver { + fn drop(&mut self) { + if let Some(child) = &mut self.child { + if let Err(e) = child.kill() { + log::error!("Error killing chrome driver: {}", e); + } + } + } +} diff --git a/src/apps/webcheck/driver/mod.rs b/src/apps/webcheck/driver/mod.rs new file mode 100644 index 0000000..ed64a8d --- /dev/null +++ b/src/apps/webcheck/driver/mod.rs @@ -0,0 +1 @@ +pub mod chrome; diff --git a/src/apps/webcheck/mod.rs b/src/apps/webcheck/mod.rs new file mode 100644 index 0000000..64a0065 --- /dev/null +++ b/src/apps/webcheck/mod.rs @@ -0,0 +1,199 @@ +use std::{ + collections::HashMap, + future::Future, + pin::Pin, + sync::{Arc, Mutex}, +}; + +use async_trait::async_trait; +use axum::{routing::get, Extension, Router}; +use chrono::DateTime; +use log::info; +use serde::{Deserialize, Serialize}; +use thirtyfour::{DesiredCapabilities, WebDriver}; +use tokio::sync::Mutex as AsyncMutex; + +use crate::{ + comm::{Communicator, Message}, + config::Config, + http::{ApiResponse, ApiResult}, + AppState, +}; + +use super::{ + auth::{middleware::AuthInfo, Role}, + App, +}; + +mod driver; +mod utd_app; + +pub struct WebcheckApp { + state: AsyncMutex, +} + +struct WebcheckAppState { + config: Option<&'static Config>, + global_app_state: Option>>, + last_response: HashMap, + checkers: HashMap>, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +struct LastResponse { + pub response: String, + pub timestamp: DateTime, +} + +async fn route_get_results( + auth: AuthInfo, + app: Extension>, +) -> ApiResult> { + auth.check_for_any_role(&[Role::Admin])?; + + let state = app.state.lock().await; + + Ok(ApiResponse::ok( + "Results retrieved successfully".to_string(), + Some(state.last_response.to_owned()), + )) +} + +#[async_trait] +pub trait WebDriverChecker { + fn init(&mut self, config: &HashMap) -> anyhow::Result<()>; + fn interval(&self) -> u64; + async fn check(&self, driver: &WebDriver) -> anyhow::Result; +} + +impl WebcheckApp { + pub fn new() -> Self { + Self { + state: AsyncMutex::new(WebcheckAppState { + config: None, + global_app_state: None, + last_response: HashMap::new(), + checkers: HashMap::new(), + }), + } + } + + pub async fn run_single_check(self: &Self, key: &str) -> anyhow::Result<()> { + let mut state = self.state.lock().await; + + let checker = state.checkers.get_mut(key).unwrap(); + + let mut driver = driver::chrome::ChromeDriver::new(); + driver.spawn(&["--enable-chrome-logs"])?; + tokio::time::sleep(std::time::Duration::from_secs(5)).await; + + let mut caps = DesiredCapabilities::chrome(); + caps.set_headless().unwrap(); + caps.set_disable_gpu().unwrap(); + + let driver = driver.connect(caps).await?; + let response = checker.check(&driver).await?; + + let new_response = LastResponse { + response: response.clone(), + timestamp: chrono::Utc::now(), + }; + + let last_response = state.last_response.get(key); + + match last_response { + Some(last_response) => { + if last_response.response != response { + state + .global_app_state + .as_ref() + .unwrap() + .lock() + .unwrap() + .comm + .send_message(&Message { + subject: format!("webcheck {} changed", key), + body: format!("{} changed to {}", key, response), + mime: "text/plain", + priority: 0, + })?; + } + } + None => {} + } + + state.last_response.insert(key.to_string(), new_response); + + Ok(()) + } + + pub async fn run_check_loops(self: Arc) { + let self_clone = self.clone(); + + let state = self.state.lock().await; + for key in state.checkers.keys() { + let key = key.clone(); + let self_clone = self_clone.clone(); + tokio::spawn(async move { + let interval = { + let state = self_clone.state.lock().await; + let checker = state.checkers.get(key.as_str()).unwrap(); + checker.interval() + }; + + let mut ticker = tokio::time::interval(std::time::Duration::from_secs(interval)); + + loop { + ticker.tick().await; + + info!("Running webcheck for {}", key); + self_clone + .run_single_check(key.as_str()) + .await + .map_err(|e| { + log::error!("Error running webcheck for {}: {}", key, e); + }) + .ok(); + } + }); + } + } +} + +impl App for WebcheckApp { + fn initialize( + self: Arc, + config: &'static Config, + app_state: Arc>, + ) -> Pin>> { + Box::pin(async move { + let mut state = self.state.lock().await; + state.config = Some(config); + state.global_app_state = Some(app_state); + + let Some(ref config) = config.webcheck else { + return; + }; + + config.keys().for_each(|key| match key.as_str() { + "utd_app" => { + let mut checker = utd_app::UTDAppChecker::new(); + checker + .init(config.get(key).unwrap()) + .expect("Failed to initialize UTDAppChecker"); + state.checkers.insert(key.clone(), Box::new(checker)); + } + _ => panic!("Invalid key in webcheck config: {}", key), + }); + + let self_clone = self.clone(); + tokio::spawn(self_clone.run_check_loops()); + }) + } + + fn api_routes(self: Arc) -> Router { + Router::new() + .route("/webcheck/results", get(route_get_results)) + .layer(Extension(self.clone())) + } +} diff --git a/src/apps/webcheck/utd_app.rs b/src/apps/webcheck/utd_app.rs new file mode 100644 index 0000000..5646f8f --- /dev/null +++ b/src/apps/webcheck/utd_app.rs @@ -0,0 +1,75 @@ +use std::collections::HashMap; + +use anyhow::Result; +use async_trait::async_trait; +use thirtyfour::prelude::*; +use tokio::time; + +use super::WebDriverChecker; + +pub struct UTDAppChecker { + config: HashMap, +} + +impl UTDAppChecker { + pub fn new() -> Self { + Self { + config: HashMap::new(), + } + } +} + +#[async_trait] +impl WebDriverChecker for UTDAppChecker { + fn init(&mut self, config: &HashMap) -> Result<()> { + if config.get("username").is_none() || config.get("password").is_none() { + return Err(anyhow::anyhow!("username or password not set")); + } + + self.config = config.clone(); + Ok(()) + } + fn interval(&self) -> u64 { + let default_interval = "3600".to_string(); + let interval = self.config.get("interval").unwrap_or(&default_interval); + interval.parse::().unwrap() + } + + async fn check(&self, driver: &WebDriver) -> Result { + let username = self.config.get("username").unwrap(); + let password = self.config.get("password").unwrap(); + + driver + .goto("https://utdallas.my.site.com/TX_SiteLogin?startURL=%2FTargetX_Portal__PB") + .await?; + + let input_els = driver.find_all(By::Css("input[type='text']")).await?; + + for input_el in input_els { + let name_attr = input_el.attr("name").await?; + if name_attr.is_some() && name_attr.unwrap().ends_with(":username") { + input_el.send_keys(username).await?; + } + } + + let password_el = driver.find(By::Css("input[type='password']")).await?; + password_el.send_keys(password).await?; + + let submit_el = driver.find(By::Css("a.targetx-button")).await?; + submit_el.click().await?; + + time::sleep(time::Duration::from_secs(10)).await; + + let mut checklist_item_text = Vec::new(); + let checklist_item_text_els = driver.find_all(By::Css("p.checklist-item-text")).await?; + + for checklist_item_text_el in checklist_item_text_els { + let text = checklist_item_text_el.text().await?; + checklist_item_text.push(text); + } + + checklist_item_text.sort(); + + Ok(checklist_item_text.join("\n")) + } +} diff --git a/src/bin/server.rs b/src/bin/server.rs new file mode 100644 index 0000000..2918628 --- /dev/null +++ b/src/bin/server.rs @@ -0,0 +1,41 @@ +use clap::Parser; +use simple_logger::SimpleLogger; +use yoake::config; + +#[derive(Debug, Parser)] +#[clap(name = "yoake_server", version, author, about)] +struct Cli { + #[arg(short, long, default_value = "config.yaml")] + config: String, + #[arg(short, long)] + dev: bool, +} + +#[tokio::main] +async fn main() { + let args = Cli::parse(); + + SimpleLogger::new() + .with_module_level( + "yoake", + if args.dev { + log::LevelFilter::Debug + } else { + log::LevelFilter::Info + }, + ) + .with_level(if args.dev { + log::LevelFilter::Info + } else { + log::LevelFilter::Warn + }) + .init() + .unwrap(); + + let config = config::Config::load_yaml_file(args.config); + unsafe { + config::set_config(config); + } + + yoake::main_server(args.dev).await; +} diff --git a/src/comm/email.rs b/src/comm/email.rs new file mode 100644 index 0000000..22b6af6 --- /dev/null +++ b/src/comm/email.rs @@ -0,0 +1,46 @@ +use lettre::{ + message::header::ContentType, transport::smtp::authentication::Credentials, Transport, +}; + +use crate::config::{comm::EmailConfig, Config}; + +use super::Communicator; + +pub struct EmailCommunicator { + config: &'static EmailConfig, +} +impl EmailCommunicator { + pub fn new(config: &'static Config) -> Self { + Self { + config: &config.comm.email, + } + } +} + +impl Communicator for EmailCommunicator { + fn name(&self) -> &'static str { + "email" + } + fn supported_mimes(&self) -> Vec<&'static str> { + vec!["text/plain", "text/html"] + } + fn send_message(&self, message: &super::Message) -> anyhow::Result<()> { + let mailer = lettre::SmtpTransport::relay(&self.config.host)? + .credentials(Credentials::new( + self.config.username.clone(), + self.config.password.clone(), + )) + .build(); + + let email = lettre::Message::builder() + .from(self.config.from.parse()?) + .to(self.config.to.parse()?) + .subject(message.subject.clone()) + .header(ContentType::parse(message.mime)?) + .body(message.body.clone())?; + + mailer.send(&email)?; + + Ok(()) + } +} diff --git a/src/comm/gotify.rs b/src/comm/gotify.rs new file mode 100644 index 0000000..68d096b --- /dev/null +++ b/src/comm/gotify.rs @@ -0,0 +1,73 @@ +use super::Communicator; +use crate::config::{comm::GotifyConfig, Config}; +use serde::Serialize; + +pub struct GotifyCommunicator { + config: &'static GotifyConfig, +} + +impl GotifyCommunicator { + pub fn new(config: &'static Config) -> Self { + Self { + config: &config.comm.gotify, + } + } +} + +#[derive(Serialize)] +struct GotifyMessage { + title: String, + message: String, + priority: i8, + extras: GotifyMessageExtras, +} + +impl Into for &super::Message { + fn into(self) -> GotifyMessage { + GotifyMessage { + title: self.subject.clone(), + message: self.body.clone(), + priority: self.priority, + extras: GotifyMessageExtras { + client_display: GotifyMessageExtrasClientDisplay { + content_type: self.mime.clone().to_string(), + }, + }, + } + } +} + +#[derive(Serialize)] +struct GotifyMessageExtras { + #[serde(rename = "client::display")] + client_display: GotifyMessageExtrasClientDisplay, +} + +#[derive(Serialize)] +struct GotifyMessageExtrasClientDisplay { + #[serde(rename = "contentType")] + content_type: String, +} + +impl Communicator for GotifyCommunicator { + fn name(&self) -> &'static str { + "gotify" + } + fn supported_mimes(&self) -> Vec<&'static str> { + vec!["text/plain", "text/markdown"] + } + fn send_message(&self, message: &super::Message) -> anyhow::Result<()> { + let client = reqwest::blocking::Client::new(); + let response = client + .post(&format!("{}/message", self.config.url)) + .header("X-Gotify-Key", &self.config.token) + .json::(&message.into()) + .send()?; + + if !response.status().is_success() { + anyhow::bail!("Gotify returned an error: {:?}", response); + } + + Ok(()) + } +} diff --git a/src/comm/mod.rs b/src/comm/mod.rs new file mode 100644 index 0000000..f12b8b1 --- /dev/null +++ b/src/comm/mod.rs @@ -0,0 +1,99 @@ +use std::{collections::HashMap, sync::Arc}; + +use anyhow::Result; +use log::{error, warn}; + +pub mod email; +pub mod gotify; + +#[derive(Clone, Debug)] +pub struct Message { + pub subject: String, + pub body: String, + pub mime: &'static str, + pub priority: i8, +} + +impl Default for Message { + fn default() -> Self { + Self { + subject: String::new(), + body: String::new(), + mime: MIME_PLAIN, + priority: 0, + } + } +} + +pub const MIME_PLAIN: &'static str = "text/plain"; +pub const MIME_HTML: &'static str = "text/html"; + +pub trait Communicator { + fn name(&self) -> &'static str; + fn supported_mimes(&self) -> Vec<&'static str>; + fn send_message(&self, message: &Message) -> Result<()>; +} + +pub struct GlobalCommunicator { + communicators: HashMap<&'static str, Vec>>, +} + +impl GlobalCommunicator { + pub fn new() -> Self { + Self { + communicators: HashMap::new(), + } + } + pub fn add_communicator(&mut self, communicator: Arc) { + for mime in communicator.supported_mimes() { + if !self.communicators.contains_key(mime) { + self.communicators.insert(mime, Vec::new()); + } + self.communicators + .get_mut(mime) + .unwrap() + .push(communicator.clone()); + } + } + pub fn by_mime(&self, mime: &'static str) -> Option<&Vec>> { + self.communicators.get(mime) + } + pub fn by_name(&self, name: &'static str) -> Option<&Arc> { + for communicators in self.communicators.values() { + for communicator in communicators { + if communicator.name() == name { + return Some(communicator); + } + } + } + None + } +} + +impl Communicator for GlobalCommunicator { + fn name(&self) -> &'static str { + "global" + } + + fn supported_mimes(&self) -> Vec<&'static str> { + self.communicators.keys().map(|k| *k).collect() + } + + fn send_message(&self, message: &Message) -> Result<()> { + let mime = message.mime; + if let Some(communicators) = self.communicators.get(mime) { + for communicator in communicators { + if let Err(e) = communicator.send_message(message) { + warn!("Failed to send message with {}: {}", communicator.name(), e); + continue; + } + return Ok(()); + } + } + error!("No communicators available for mime {}", mime); + Err(anyhow::anyhow!( + "No communicators available for mime {}", + mime + )) + } +} diff --git a/src/config/comm.rs b/src/config/comm.rs new file mode 100644 index 0000000..c16e08a --- /dev/null +++ b/src/config/comm.rs @@ -0,0 +1,24 @@ +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct Config { + pub email: EmailConfig, + pub gotify: GotifyConfig, +} + +#[derive(Debug, Deserialize)] +pub struct EmailConfig { + pub from: String, + pub to: String, + pub host: String, + pub port: u16, + pub username: String, + pub password: String, + pub default_subject: String, +} + +#[derive(Debug, Deserialize)] +pub struct GotifyConfig { + pub url: String, + pub token: String, +} diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 0000000..3c87107 --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1,79 @@ +use std::collections::HashMap; + +use serde::Deserialize; + +pub mod comm; + +#[derive(Debug, Deserialize)] +pub struct Config { + pub listen: ListenConfig, + pub db: DbConfig, + + pub canvas_lms: CanvasLmsConfig, + + pub auth: AuthConfig, + + pub session: SessionConfig, + + pub webcheck: Option>>, + + pub comm: comm::Config, +} + +impl Config { + pub fn load_yaml(reader: R) -> Self { + let config = serde_yaml::from_reader(reader).expect("Failed to parse config"); + config + } + pub fn load_yaml_file>(path: P) -> Self { + let file = std::fs::File::open(path).expect("Failed to open config file"); + Self::load_yaml(file) + } +} + +#[derive(Debug, Deserialize)] +pub struct ListenConfig { + pub addr: String, + pub cert: Option, + pub key: Option, +} + +#[derive(Debug, Deserialize)] +pub struct DbConfig { + pub url: String, +} + +#[derive(Debug, Deserialize)] +pub struct CanvasLmsConfig { + pub token: String, + pub endpoint: String, + pub refresh_interval: u64, +} + +#[derive(Debug, Deserialize)] +pub struct AuthConfig { + pub users: HashMap, +} + +#[derive(Debug, Deserialize)] +pub struct SessionConfig { + pub secret: String, +} + +#[derive(Debug, Deserialize)] +pub struct AuthUser { + pub password: String, + pub roles: Vec, +} + +static mut CURRENT_CONFIG: Option = None; + +pub unsafe fn set_config(config: Config) { + unsafe { + CURRENT_CONFIG = Some(config); + } +} + +pub fn get_config() -> &'static Config { + unsafe { CURRENT_CONFIG.as_ref().expect("Config not initialized") } +} diff --git a/src/http.rs b/src/http.rs new file mode 100644 index 0000000..71e75af --- /dev/null +++ b/src/http.rs @@ -0,0 +1,108 @@ +use axum::{ + async_trait, + body::{Bytes, HttpBody}, + extract::FromRequest, + http::Request, + response::{IntoResponse, Response}, + BoxError, Json, +}; +use hyper::StatusCode; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; + +pub struct JsonApiForm(pub T); + +#[async_trait] +impl FromRequest for JsonApiForm +where + T: DeserializeOwned, + S: Send + Sync, + B: HttpBody + Send + 'static, + B::Data: Send, + B::Error: Into, +{ + type Rejection = ApiResponse<()>; + + async fn from_request(req: Request, state: &S) -> Result { + if req.headers().get("content-type") != Some(&"application/json".parse().unwrap()) { + return Err(ApiResponse::error( + "invalid content-type".to_string(), + 400, + None, + )); + } + let bytes = Bytes::from_request(req, state) + .await + .map_err(|_| ApiResponse::error("failed reading request".to_string(), 400, None))?; + + let des = &mut serde_json::Deserializer::from_slice(&bytes); + + Ok(JsonApiForm(T::deserialize(des).map_err(|e| { + ApiResponse::error(format!("failed parsing json: {}", e), 400, None) + })?)) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ApiResponse { + pub code: u16, + pub status: ApiStatus, + pub message: String, + pub data: Option, +} + +pub type ApiResult = Result, ApiResponse<()>>; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum ApiStatus { + Ok, + Error, +} + +impl ApiResponse { + pub const fn ok(message: String, data: Option) -> Self { + Self { + code: 200, + status: ApiStatus::Ok, + message, + data, + } + } + + pub const fn error(message: String, code: u16, data: Option) -> Self { + Self { + code, + status: ApiStatus::Error, + message, + data, + } + } + + pub const fn unauthorized(message: String, data: Option) -> Self { + Self { + code: 401, + status: ApiStatus::Error, + message, + data, + } + } + + pub const fn bad_request(message: String, data: Option) -> Self { + Self { + code: 400, + status: ApiStatus::Error, + message, + data, + } + } +} + +impl IntoResponse for ApiResponse +where + T: Serialize, +{ + fn into_response(self) -> Response { + let resp = (StatusCode::from_u16(self.code).unwrap(), Json(self)).into_response(); + + resp + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..f52bb76 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,134 @@ +use std::sync::{Arc, Mutex}; + +use apps::{med, webcheck}; +use axum::{http::Request, middleware::Next, response::Response, routing::get, Extension, Router}; +use axum_server::tls_rustls::RustlsConfig; +use diesel::{sqlite, Connection}; +use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; +use hyper::Method; +use log::info; +use tower_http::cors::{self, CorsLayer}; + +use crate::{ + apps::{auth, canvas_lms, server_info, App}, + comm::GlobalCommunicator, +}; + +pub mod apps; +pub mod config; + +pub mod session; + +pub mod models; +pub mod schema; + +pub mod http; + +pub mod comm; + +pub mod ui; + +async fn log_middleware(req: Request, next: Next) -> Response { + info!("Request: {} {}", req.method(), req.uri()); + let res = next.run(req).await; + info!("Response: {:?}", res); + res +} + +const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations"); + +pub struct AppState { + pub db: sqlite::SqliteConnection, + pub comm: comm::GlobalCommunicator, +} + +pub fn establish_db_connection() -> sqlite::SqliteConnection { + let db_url = &config::get_config().db.url; + + let mut conn = sqlite::SqliteConnection::establish(db_url) + .unwrap_or_else(|_| panic!("Error connecting to {}", db_url)); + + conn.run_pending_migrations(MIGRATIONS).unwrap(); + + conn +} + +pub async fn server_listen(router: Router) { + let config = config::get_config(); + + let listen_addr = config + .listen + .addr + .parse() + .expect("Failed to parse listen address"); + if config.listen.cert.is_some() && config.listen.key.is_some() { + let tls_config = RustlsConfig::from_pem_file( + config.listen.cert.as_ref().unwrap(), + config.listen.key.as_ref().unwrap(), + ) + .await + .expect("Failed to load TLS certificate and key"); + info!("Listening on https://{}", config.listen.addr); + axum_server::bind_rustls(listen_addr, tls_config) + .serve(router.into_make_service()) + .await + .unwrap(); + } else { + info!("Listening on http://{}", config.listen.addr); + axum_server::bind(listen_addr) + .serve(router.into_make_service()) + .await + .unwrap(); + } +} + +pub async fn main_server(dev: bool) { + let config = config::get_config(); + + let apps: &mut [Arc] = &mut [ + Arc::new(server_info::ServerInfoApp::new()), + Arc::new(auth::AuthApp::new()), + Arc::new(canvas_lms::CanvasLMSApp::new()), + Arc::new(med::MedManagementApp::new()), + Arc::new(webcheck::WebcheckApp::new()), + ]; + + let mut comm = GlobalCommunicator::new(); + comm.add_communicator(Arc::new(comm::gotify::GotifyCommunicator::new(config))); + comm.add_communicator(Arc::new(comm::email::EmailCommunicator::new(config))); + let app_state = Arc::new(Mutex::new(AppState { + db: establish_db_connection(), + comm: comm, + })); + + for app in &mut *apps { + app.clone().initialize(config, app_state.clone()).await; + } + + let mut api_router = axum::Router::new(); + for app in apps { + api_router = api_router.merge(app.clone().api_routes()); + } + + let router = axum::Router::new() + .nest("/api", api_router) + .route("/", get(ui::redirect_to_ui)) + .route("/ui/", get(ui::ui_index_handler)) + .route("/ui/*path", get(ui::ui_path_handler)) + .layer(axum::middleware::from_fn(session::middleware)) + .layer(Extension(config)) + .layer(Extension(app_state)) + .layer(axum::middleware::from_fn(log_middleware)); + + let router = if dev { + router.layer( + CorsLayer::new() + .allow_methods([Method::GET, Method::POST]) + .allow_origin(cors::Any), + ) + } else { + router + }; + + server_listen(router).await; +} diff --git a/src/models/med.rs b/src/models/med.rs new file mode 100644 index 0000000..925d1df --- /dev/null +++ b/src/models/med.rs @@ -0,0 +1,182 @@ +use std::str::FromStr; + +use diesel::prelude::*; +use lazy_static::lazy_static; +use regex::Regex; +use serde::{Deserialize, Serialize}; + +#[derive(Queryable, Selectable, Insertable, Serialize, Deserialize, Debug)] +#[diesel(table_name = crate::schema::medications)] +pub struct Medication { + pub uuid: String, + pub name: String, + pub dosage: i32, + pub dosage_unit: String, + pub period_hours: i32, + pub flags: String, + pub options: String, + pub created: chrono::NaiveDateTime, + pub updated: chrono::NaiveDateTime, +} + +impl Medication { + pub fn flags_split(&self) -> Vec { + self.flags.split(' ').map(|s| s.to_string()).collect() + } + pub fn options_split(&self) -> Vec<(String, String)> { + lazy_static! { + static ref REGEX_MED_OPTION: Regex = Regex::new(r"^([a-zA-Z]+)\((\w+)\)$").unwrap(); + }; + self.options + .split(' ') + .map(|s| { + let caps = REGEX_MED_OPTION.captures(s).unwrap(); + ( + caps.get(1).unwrap().as_str().to_string(), + caps.get(2).unwrap().as_str().to_string(), + ) + }) + .collect() + } +} + +pub const FLAGS_WITH_IMPLIED_FREQ: [&str; 2] = ["qhs", "qam"]; + +impl Into for Medication { + fn into(self) -> String { + let mut output = String::new(); + + output.push_str(&self.name); + output.push(' '); + + output.push_str(&self.dosage.to_string()); + output.push_str(&self.dosage_unit); + output.push(' '); + + if !FLAGS_WITH_IMPLIED_FREQ.contains(&self.flags.as_str()) { + match self.period_hours { + 6 => output.push_str("qid"), + 8 => output.push_str("tid"), + 12 => output.push_str("bid"), + 24 => output.push_str("qd"), + _ => { + if self.period_hours % 24 == 0 { + output.push_str(&format!("q{}d", self.period_hours / 24)); + } else { + output.push_str(&format!("q{}h", self.period_hours)); + } + } + } + output.push(' '); + } + + output.push_str(&self.flags); + output.push(' '); + output.push_str(&self.options); + + output.trim().to_string() + } +} + +impl FromStr for Medication { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + lazy_static! { + static ref REGEX_NUMBER: Regex = Regex::new(r"^\d+$").unwrap(); + static ref REGEX_NUMBER_WITH_UNIT: Regex = Regex::new(r"^(\d+)(\w+)$").unwrap(); + static ref REGEX_MED_OPTION: Regex = Regex::new(r"^([a-zA-Z]+)\((\w+)\)$").unwrap(); + } + let mut parts = s.split(' '); + + let mut name = String::new(); + let mut flags = Vec::new(); + let mut options = Vec::new(); + let mut dosage = 0; + let mut dosage_unit = None; + for part in parts.by_ref() { + if REGEX_NUMBER.is_match(part) { + dosage = part.parse()?; + break; + } else if let Some(caps) = REGEX_NUMBER_WITH_UNIT.captures(part) { + dosage = caps.get(1).unwrap().as_str().parse()?; + dosage_unit = Some(caps.get(2).unwrap().as_str().to_string()); + break; + } else { + name.push_str(part); + name.push(' '); + } + } + if dosage_unit.is_none() { + dosage_unit = parts.next().map(|s| s.to_string()); + } + let period_spec = parts + .next() + .ok_or(anyhow::anyhow!("missing period spec"))? + .to_lowercase(); + let period_hours = match period_spec.as_str() { + "qd" => 24, + "bid" => 12, + "tid" => 8, + "qid" => 6, + "qhs" => { + flags.push("qhs".to_string()); + 24 + } + "qam" => { + flags.push("qam".to_string()); + 24 + } + _ => { + if period_spec.starts_with("q") { + let period_unit = period_spec.chars().last().unwrap(); + let period_duration = period_spec[1..period_spec.len() - 1].parse()?; + match period_unit { + 'h' => period_duration, + 'd' => period_duration * 24, + _ => return Err(anyhow::anyhow!("invalid period spec")), + } + } else { + return Err(anyhow::anyhow!("invalid period spec")); + } + } + }; + for part in parts { + if let Some(caps) = REGEX_MED_OPTION.captures(part) { + let opt_name = caps.get(1).unwrap().as_str(); + let opt_value = caps.get(2).unwrap().as_str(); + options.push((opt_name.to_string(), opt_value.to_string())); + } else { + flags.push(part.to_string()); + } + } + Ok(Self { + uuid: uuid::Uuid::new_v4().to_string(), + name: name.trim().to_string(), + dosage, + dosage_unit: dosage_unit.unwrap(), + period_hours, + flags: flags.join(" "), + options: options + .iter() + .map(|(k, v)| format!("{}({})", k, v)) + .collect::>() + .join(" "), + created: chrono::Utc::now().naive_utc(), + updated: chrono::Utc::now().naive_utc(), + }) + } +} + +#[derive(Queryable, Selectable, Insertable, Serialize, Deserialize, Debug, Clone)] +#[diesel(table_name = crate::schema::medication_logs)] +pub struct MedicationLog { + pub uuid: String, + pub med_uuid: String, + pub dosage: i32, + pub time_actual: chrono::NaiveDateTime, + pub time_expected: chrono::NaiveDateTime, + pub dose_offset: f32, + pub created: chrono::NaiveDateTime, + pub updated: chrono::NaiveDateTime, +} diff --git a/src/models/med_test.rs b/src/models/med_test.rs new file mode 100644 index 0000000..cf7207d --- /dev/null +++ b/src/models/med_test.rs @@ -0,0 +1,74 @@ +use super::med::*; + +#[test] +pub fn test_parse_med() { + let uuid_stub = "".to_string(); + let time_stub = chrono::Utc::now().naive_utc(); + let cases = vec![ + ( + "Atorvastatin 10mg QD", + Medication { + uuid: uuid_stub.clone(), + name: "Atorvastatin".to_string(), + dosage: 10, + dosage_unit: "mg".to_string(), + period_hours: 24, + flags: "".to_string(), + options: "".to_string(), + created: time_stub.clone(), + updated: time_stub.clone(), + }, + ), + ( + "Something 10mg tid adlib", + Medication { + uuid: uuid_stub.clone(), + name: "Something".to_string(), + dosage: 10, + dosage_unit: "mg".to_string(), + period_hours: 8, + flags: "adlib".to_string(), + options: "".to_string(), + created: time_stub.clone(), + updated: time_stub.clone(), + }, + ), + ( + "Metformin 500mg qHS", + Medication { + uuid: uuid_stub.clone(), + name: "Metformin".to_string(), + dosage: 500, + dosage_unit: "mg".to_string(), + period_hours: 24, + flags: "qhs".to_string(), + options: "".to_string(), + created: time_stub.clone(), + updated: time_stub.clone(), + }, + ), + ( + "Hydroxyzine 50mg qid prn sched(whole)", + Medication { + uuid: uuid_stub.clone(), + name: "Hydroxyzine".to_string(), + dosage: 50, + dosage_unit: "mg".to_string(), + period_hours: 6, + flags: "prn".to_string(), + options: "sched(whole)".to_string(), + created: time_stub.clone(), + updated: time_stub.clone(), + }, + ), + ]; + for (input, expected) in cases { + let actual = input.parse::().unwrap(); + assert_eq!(actual.name, expected.name); + assert_eq!(actual.dosage, expected.dosage); + assert_eq!(actual.dosage_unit, expected.dosage_unit); + assert_eq!(actual.period_hours, expected.period_hours); + assert_eq!(actual.flags, expected.flags); + assert_eq!(actual.options, expected.options); + } +} diff --git a/src/models/mod.rs b/src/models/mod.rs new file mode 100644 index 0000000..00affdc --- /dev/null +++ b/src/models/mod.rs @@ -0,0 +1,4 @@ +pub mod med; +#[cfg(test)] +mod med_test; +pub mod session; diff --git a/src/models/session.rs b/src/models/session.rs new file mode 100644 index 0000000..e836756 --- /dev/null +++ b/src/models/session.rs @@ -0,0 +1,9 @@ +use diesel::prelude::*; + +#[derive(Queryable, Selectable, Insertable)] +#[diesel(table_name = crate::schema::sessions)] +pub struct Session { + pub uuid: String, + pub expiry: chrono::NaiveDateTime, + pub content: String, +} diff --git a/src/schema.rs b/src/schema.rs new file mode 100644 index 0000000..27a7da8 --- /dev/null +++ b/src/schema.rs @@ -0,0 +1,44 @@ +// @generated automatically by Diesel CLI. + +diesel::table! { + medication_logs (uuid) { + uuid -> Text, + med_uuid -> Text, + dosage -> Integer, + time_actual -> Timestamp, + time_expected -> Timestamp, + dose_offset -> Float, + created -> Timestamp, + updated -> Timestamp, + } +} + +diesel::table! { + medications (uuid) { + uuid -> Text, + name -> Text, + dosage -> Integer, + dosage_unit -> Text, + period_hours -> Integer, + flags -> Text, + options -> Text, + created -> Timestamp, + updated -> Timestamp, + } +} + +diesel::table! { + sessions (uuid) { + uuid -> Text, + expiry -> Timestamp, + content -> Text, + } +} + +diesel::joinable!(medication_logs -> medications (med_uuid)); + +diesel::allow_tables_to_appear_in_same_query!( + medication_logs, + medications, + sessions, +); diff --git a/src/session/mod.rs b/src/session/mod.rs new file mode 100644 index 0000000..1af8387 --- /dev/null +++ b/src/session/mod.rs @@ -0,0 +1,71 @@ +use axum::{http::Request, middleware::Next, response::Response, Extension}; +use base64::{engine::general_purpose, Engine}; +use serde::{Deserialize, Serialize}; + +use crate::{apps::auth::SessionAuth, config::Config}; + +mod wrap; + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct SessionStore { + pub auth: SessionAuth, +} + +const SESSION_COOKIE_NAME: &str = "session_state"; + +pub async fn middleware( + Extension(config): Extension<&Config>, + mut req: Request, + next: Next, +) -> Response { + let mut session_state = SessionStore::default(); + + let mut key = [0u8; 32]; + general_purpose::STANDARD + .decode_slice(config.session.secret.as_bytes(), &mut key) + .expect("Failed to decode session secret"); + + { + let cookies = req + .headers() + .get("Cookie") + .map(|c| c.to_str().unwrap_or("")) + .unwrap_or("") + .split("; ") + .map(|c| { + let mut parts = c.splitn(2, '='); + ( + parts.next().unwrap_or("").to_string(), + parts.next().unwrap_or("").to_string(), + ) + }); + + for (name, value) in cookies { + if name == SESSION_COOKIE_NAME { + if let Some(store) = wrap::unwrap_json::(&value, key.as_ref()) { + session_state = store; + } + } + } + } + + req.extensions_mut().insert(session_state); + + let mut resp = next.run(req).await; + + if let Some(new_store) = resp.extensions().get::() { + let wrapped = wrap::wrap_json(new_store, key.as_ref()); + + resp.headers_mut().insert( + "Set-Cookie", + format!( + "{}={}; Path=/; HttpOnly; Max-Age=31536000", + SESSION_COOKIE_NAME, wrapped + ) + .parse() + .unwrap(), + ); + } + + resp +} diff --git a/src/session/wrap.rs b/src/session/wrap.rs new file mode 100644 index 0000000..b7ccc8b --- /dev/null +++ b/src/session/wrap.rs @@ -0,0 +1,60 @@ +use aes_gcm::{ + aead::{Aead, OsRng}, + AeadCore, Aes256Gcm, Key, KeyInit, Nonce, +}; +use base64::{engine::general_purpose, Engine}; +use serde::{de::DeserializeOwned, Serialize}; + +pub fn wrap_json(data: T, key: &[u8]) -> String { + let nonce = Aes256Gcm::generate_nonce(&mut OsRng); + let key = Key::::from_slice(key); + let cipher = Aes256Gcm::new(&key); + + let b64_engine = general_purpose::STANDARD; + + let plaintext = serde_json::to_string(&data).unwrap(); + let ciphertext = cipher + .encrypt(&nonce, plaintext.as_bytes()) + .expect("Failed to encrypt"); + + let mut output = String::new(); + + output.push_str(&b64_engine.encode(&nonce)); + output.push_str(":"); + b64_engine.encode_string(&ciphertext, &mut output); + + output +} + +pub fn unwrap_json(data: &str, key: &[u8]) -> Option { + let data = data.splitn(2, ':').collect::>(); + let nonce_b64 = data.get(0)?; + let ciphertext_b64 = data.get(1)?; + + let nonce = general_purpose::STANDARD.decode(nonce_b64).ok()?; + let ciphertext = general_purpose::STANDARD.decode(ciphertext_b64).ok()?; + + let cipher = Aes256Gcm::new(Key::::from_slice(key)); + let nonce = Nonce::from_slice(&nonce); + let plaintext = cipher + .decrypt(&nonce, ciphertext.as_slice()) + .expect("Failed to decrypt"); + + let plaintext_utf8 = String::from_utf8(plaintext).ok()?; + + serde_json::from_str(&plaintext_utf8).ok() +} + +#[cfg(test)] +mod tests { + #[test] + fn test_wrap_json() { + let data = "test"; + let key: [u8; 32] = [0; 32]; + + let wrapped = super::wrap_json(data, key.as_ref()); + let unwrapped = super::unwrap_json::(&wrapped, key.as_ref()).unwrap(); + + assert_eq!(data, unwrapped); + } +} diff --git a/src/ui.rs b/src/ui.rs new file mode 100644 index 0000000..cd6fd3c --- /dev/null +++ b/src/ui.rs @@ -0,0 +1,103 @@ +use std::io::Write; + +use axum::{ + body::HttpBody, extract::Path, http::Request, response::IntoResponse, response::Response, + routing::get, Router, +}; +use flate2::write::GzEncoder; +use hyper::{header, StatusCode}; +use rust_embed::RustEmbed; + +#[derive(RustEmbed)] +#[folder = "ui/dist"] +struct UiAssets; + +pub async fn serve_file(path: &str, accepted_encodings: Vec<&str>) -> Response { + let ext = path.split('.').last().unwrap_or(""); + let content_type = match ext { + "html" => "text/html", + "css" => "text/css", + "js" => "text/javascript", + "png" => "image/png", + "jpg" => "image/jpeg", + "jpeg" => "image/jpeg", + "gif" => "image/gif", + "svg" => "image/svg+xml", + _ => "text/plain", + }; + + if let Some(asset) = UiAssets::get(path) { + if accepted_encodings.contains(&"gzip") { + let mut encoder = GzEncoder::new(Vec::new(), flate2::Compression::default()); + encoder.write_all(asset.data.as_ref()).unwrap(); + let data = encoder.finish().unwrap(); + return ( + StatusCode::OK, + [ + (header::CONTENT_TYPE, content_type), + (header::CONTENT_ENCODING, "gzip"), + ], + data, + ) + .into_response(); + } else { + ( + StatusCode::OK, + [(header::CONTENT_TYPE, content_type)], + asset.data.to_vec(), + ) + .into_response() + } + } else { + ( + StatusCode::NOT_FOUND, + [(header::CONTENT_TYPE, "text/plain")], + format!("File not found: {}", path), + ) + .into_response() + } +} + +pub async fn ui_path_handler(Path(path): Path, req: Request) -> Response { + let mut path = if path.ends_with("/") { + path + "index.html" + } else { + path + }; + path = path.trim_start_matches('/').to_owned(); + + let accepted_encodings = req + .headers() + .get(header::ACCEPT_ENCODING) + .and_then(|v| v.to_str().ok()) + .unwrap_or("") + .split(", "); + + serve_file(&path, accepted_encodings.collect()).await +} + +pub async fn ui_index_handler(req: Request) -> Response { + let accepted_encodings = req + .headers() + .get(header::ACCEPT_ENCODING) + .and_then(|v| v.to_str().ok()) + .unwrap_or("") + .split(", "); + + serve_file("index.html", accepted_encodings.collect()).await +} + +pub async fn redirect_to_ui() -> Response { + ( + StatusCode::TEMPORARY_REDIRECT, + [(header::LOCATION, "/ui/")], + "", + ) + .into_response() +} + +pub fn ui_router(_dev: bool) -> Router { + Router::new() + .route("/*path", get(ui_path_handler)) + .fallback(get(ui_index_handler)) +} diff --git a/templates/grades.html b/templates/grades.html new file mode 100644 index 0000000..050f5fa --- /dev/null +++ b/templates/grades.html @@ -0,0 +1,28 @@ +

お姫様の成績がアップデートされました!

+

最近の採点

+ + + + + + + + + + + + + + {% for grade in grades %} + + + + + + + + + {% endfor %} + +
NameCourseAssignmentDueGradeGraded AtPosted At
{{ grade.submission_username }}{{ grade.course_code }}{{ grade.name }}{{ grade.due }} + {{ grade.grade }}{{ grade.graded_at }} {{ grade.posted_at }}
\ No newline at end of file diff --git a/ui/.eslintrc.cjs b/ui/.eslintrc.cjs new file mode 100644 index 0000000..4020bcb --- /dev/null +++ b/ui/.eslintrc.cjs @@ -0,0 +1,14 @@ +module.exports = { + env: { browser: true, es2020: true }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react-hooks/recommended', + ], + parser: '@typescript-eslint/parser', + parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, + plugins: ['react-refresh'], + rules: { + 'react-refresh/only-export-components': 'warn', + }, +} diff --git a/ui/.gitignore b/ui/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/ui/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/ui/.server_pid b/ui/.server_pid new file mode 100644 index 0000000..d9ceec0 --- /dev/null +++ b/ui/.server_pid @@ -0,0 +1 @@ +346497 diff --git a/ui/index.html b/ui/index.html new file mode 100644 index 0000000..b3c9e8b --- /dev/null +++ b/ui/index.html @@ -0,0 +1,16 @@ + + + + + + + + 夜明け + + + +
+ + + + \ No newline at end of file diff --git a/ui/package.json b/ui/package.json new file mode 100644 index 0000000..45a6d01 --- /dev/null +++ b/ui/package.json @@ -0,0 +1,35 @@ +{ + "name": "ui", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "@emotion/react": "^11.11.1", + "@emotion/styled": "^11.11.0", + "@mui/icons-material": "^5.11.16", + "@mui/material": "^5.13.4", + "@mui/system": "^5.13.2", + "@mui/x-data-grid": "^6.7.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.12.1" + }, + "devDependencies": { + "@types/react": "^18.0.37", + "@types/react-dom": "^18.0.11", + "@typescript-eslint/eslint-plugin": "^5.59.0", + "@typescript-eslint/parser": "^5.59.0", + "@vitejs/plugin-react": "^4.0.0", + "eslint": "^8.38.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.3.4", + "typescript": "^5.0.2", + "vite": "^4.3.9" + } +} diff --git a/ui/public/favicon.ico b/ui/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..e938ee85a88111202c948b92d83221ef17157a4c GIT binary patch literal 1406 zcmdUuc~DbV6vjW=+S;8iQ-*2FPMUEvDWyPI0||>8#zuq|3$+$3o7hee5uuQvfI)>y zfM5c{-UM69s!0V(mBj#tGH!?^g_(*aloly`xE4`sfVTiJ39?{Ctoo0`t1kyy&NKQyWa_ZMe5GNuvBMI*99k4V`!Sdc5Ean$r zu|V8kgvH_#EQxi)25@@&!M&`5^YtNchljwLG{TzpThV@N!e(v`wuB97`{`+726nTv zu%DU5dv~W{M{D+k9j#j}+=mTeOITAKE2?2l*pRj*Y)IP@8|eOEWhr(k$`PolMxeR@ z0Zkh0EU!ecrUlXBOvK5RNIY>A2TzwFx>|)eRT=izRwBCg9AY&MNK;iH>_Qv-doF_C zuY;e#fQ>hAAZYj&gujmxW7uIB#SYS&jT7KgO&>a=FU{LW#}KRQM%0J_67Gjc=Xycv zZI_IHDVkOz36gvUXNKL_$Ho=^mOK(a{yiQpp?ZWW+(8xIA7AQXiOAPW;h z8Xf~>LD)cY#>XMMa|iho6Oi4VLiKMWXdJiRVKXt8b7;M(y%K27Y)oO9Ats7UmbhzHzhg&_}80yqv zM-Tt?L;XLcN}(uMRDM@MBSqQILNkUr(?4Si%bY=BRwZO}EMFHz9?sduVY66_s!tf+ z-$aT;d*aeJxpF8Ez+qHn?-%WUDf47w+VjsPr21RcWQo2=sy}yL z>-*{pPk(vfs9G8k_CrT&%h?le9C^m%)A-u$9+KvU!s6o=Zx=O*pIRdma=7`w6nMUA z=B3g1)V-W5WHa4Q{UqP$clwfEukUIXGT6*_+;~2RQu5k*^}Ss}28+dEGnj#|#eDYd s#lFWmi-N;?*EBZy%mpEP^*Jcm=FF|H?EZdN@W)RgWas7||7+L&21g~_ZvX%Q literal 0 HcmV?d00001 diff --git a/ui/src/App.css b/ui/src/App.css new file mode 100644 index 0000000..b9d355d --- /dev/null +++ b/ui/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/ui/src/App.tsx b/ui/src/App.tsx new file mode 100644 index 0000000..d2b4cc1 --- /dev/null +++ b/ui/src/App.tsx @@ -0,0 +1,83 @@ + +import './App.css' +import { useState } from 'react'; +import { RouterProvider, createHashRouter } from 'react-router-dom'; +import PageBase from './PageBase'; +import { ThemeProvider } from '@emotion/react'; +import theme from './theme'; +import { LoginContext } from './context/LoginContext'; +import { EmptyAuthInfo, getLoginState } from './api/auth'; +import HomePage from './pages/HomePage'; +import LoginDialog from './pages/LoginDialog'; +import GradesPage from './pages/GradesPage'; +import FramePage from './pages/FramePage'; +import { EnsureRole } from './components/EnsureRole'; +import MedsPage from './pages/MedsPage'; + + +const persistent_pages = [ + { + path: "/gotify_ui", + element: + } +]; + +function App() { + + const [auth, setAuth] = useState(EmptyAuthInfo); + const [mounted, setMounted] = useState(false); + + const refreshAuth = () => { + getLoginState().then((auth) => { + setAuth(auth); + } + ).catch((error) => { + console.error(error); + setAuth(EmptyAuthInfo); + }); + } + + if (!mounted) { + setMounted(true); + refreshAuth(); + } + + const router = createHashRouter([ + { + path: "/", + element: + }, + { + path: "/grades", + element: + }, + { + path: "/meds", + element: + }, + { + path: "/gotify_ui", + element: + }, + { + path: "/login", + element: navigate("/")} + /> + } + ]); + + return ( + + + + + + ) +} + +export default App diff --git a/ui/src/PageBase.tsx b/ui/src/PageBase.tsx new file mode 100644 index 0000000..68ac88b --- /dev/null +++ b/ui/src/PageBase.tsx @@ -0,0 +1,175 @@ +import { ReactNode, useContext, useState } from 'react' +import './App.css' +import { AppBar, Button, Divider, IconButton, Toolbar, Drawer, List, ListItem, ListItemButton, ListItemText } from '@mui/material' +import Typography from '@mui/material/Typography' +import { Box, Container } from '@mui/system' +import MenuIcon from '@mui/icons-material/Menu' +import HomeIcon from '@mui/icons-material/Home' +import GradeIcon from '@mui/icons-material/Grade' +import CampaignIcon from '@mui/icons-material/Campaign' +import MedicationIcon from '@mui/icons-material/Medication' +import { useMatches, useNavigate } from 'react-router-dom' +import { LoginContext } from './context/LoginContext' +import { ServerInfo, getServerInfo } from './api/server_info' + +interface PersistentPage { + path: string; + element: ReactNode; +}; + +const drawerWidth = 240; + +function routerMatch(matches: { pathname: string }[], path: string) { + for (let i = 0; i < matches.length; i++) { + if (matches[i].pathname === path) { + return true; + } + } + return false; +} + + +function PageBase({ children, persistentPages }: { children?: ReactNode, persistentPages: PersistentPage[] }) { + const navigate = useNavigate(); + const matches = useMatches(); + + const { auth } = useContext(LoginContext); + const [openMenu, setOpenMenu] = useState(false); + + const handleMenuToggle = () => { + setOpenMenu(!openMenu); + }; + + const [server_info, setServerInfo] = useState(null); + if (!server_info) { + getServerInfo().then((serverInfo) => { + setServerInfo(serverInfo); + }).catch((error) => { + console.error(error); + }); + } + + const drawer = ( +
+ + + + {[ + { key: "home", name: "Home", icon: , path: "/" }, + { key: "grades", name: "Grades", icon: , path: "/grades" }, + { key: "meds", name: "Meds", icon: , path: "/meds" }, + { key: "gotify", name: "Gotify", icon: , path: "/gotify_ui" }, + ].map((item) => ( + { + navigate(item.path); + }} + sx={{ color: routerMatch(matches, item.path) ? "#f00" : "#000" }} + > + + {item.icon} + + + + )) + } + +
+ ) + + return ( + + + + + + + + + { navigate("/") }} + sx={{ color: 'inherit', textDecoration: 'none', display: { md: 'flex' }, }} > + 夜明け + + + + {server_info?.version ? `${server_info.version} (${server_info.profile})` : "Unknown"} + + + + { + !auth.valid ? + + : + + } + + + + + + {drawer} + + + {drawer} + + + + { + (children ? + + {children} + : null) + } + + { + persistentPages.map((page) => { + return ( + + {page.element} + + ) + }) + } + + ) +} + +export default PageBase diff --git a/ui/src/api/auth.ts b/ui/src/api/auth.ts new file mode 100644 index 0000000..58e42b0 --- /dev/null +++ b/ui/src/api/auth.ts @@ -0,0 +1,42 @@ +import { makeJSONRequest } from "./request"; + +export type Role = "Admin" | "User"; + +export interface AuthInfo { + valid: boolean, + user: string, + display_name: string, + roles: Role[], +} + +export const EmptyAuthInfo: AuthInfo = { + valid: false, + user: "", + display_name: "", + roles: [], +} + +export async function getLoginState(): Promise { + const response = await makeJSONRequest("/api/auth/self", "GET"); + return response.data; +} + +interface PostLoginParams { + username: string, + password: string, +} + +export async function postLogin(params: PostLoginParams): Promise { + const response = await makeJSONRequest("/api/auth/login", "POST", { + username: params.username, + password: params.password, + }); + if (response.status != "Ok") { + throw new Error(response.message); + } + return response.data; +} + +export async function postLogout(): Promise { + await makeJSONRequest("/api/auth/logout", "POST"); +} \ No newline at end of file diff --git a/ui/src/api/canvas_lms.ts b/ui/src/api/canvas_lms.ts new file mode 100644 index 0000000..873da3a --- /dev/null +++ b/ui/src/api/canvas_lms.ts @@ -0,0 +1,41 @@ +import { makeJSONRequest } from "./request"; + + +export interface Grading { + name: string, + course_name: string, + course_code: string, + + assignment_id: string, + assignment_legacy_id: string, + assignment_url: string, + submission_id: string, + submission_legacy_id: string, + course_id: string, + course_legacy_id: string, + + due_at: string | null, + state: string, + score: number | null, + entered_score: number | null, + possible_points: number, + grade: string, + grade_hidden: boolean, + entered_grade: string, + + graded_at: string | null, + posted_at: string | null, +} + +export interface GetGradingsResponse { + last_updated: string, + response: Grading[], +} + +export async function getGradings(force: boolean): Promise { + let ret = await makeJSONRequest("/api/canvas_lms/grades" + (force ? "?force_refresh=true" : ""), "GET"); + if (ret.status != "Ok") { + throw new Error(ret.message); + } + return ret.data; +} \ No newline at end of file diff --git a/ui/src/api/med_directive.ts b/ui/src/api/med_directive.ts new file mode 100644 index 0000000..1cbba4b --- /dev/null +++ b/ui/src/api/med_directive.ts @@ -0,0 +1,55 @@ +import { makeJSONRequest } from "./request"; + +export interface Medication { + uuid: string, + name: string, + dosage: number, + dosage_unit: string, + period_hours: number, + flags: string, + options: string, +} + +export async function parseShorthand(shorthand: string): Promise { + const url = "/api/med/parse_shorthand"; + const method = "POST"; + const body = { + shorthand: shorthand, + }; + return (await makeJSONRequest(url, method, body)).data; +} + +export async function formatShorthand(med: Medication): Promise { + const url = "/api/med/format_shorthand"; + const method = "POST"; + const body = med; + return (await makeJSONRequest(url, method, body)).data; +} + +export async function getDirectives(): Promise { + const url = "/api/med/directive"; + const method = "GET"; + return (await makeJSONRequest(url, method)).data; +} + +export async function postDirective(med: Medication): Promise { + const url = "/api/med/directive"; + const method = "POST"; + const body = med; + return (await makeJSONRequest(url, method, body)).data; +} + +export async function patchDirective(med: Medication): Promise { + const url = "/api/med/directive"; + const method = "PATCH"; + const body = med; + + return (await makeJSONRequest(url, method, body)).data; +} + +export async function deleteDirective(uuid: string): Promise { + const url = "/api/med/directive/" + uuid; + const method = "DELETE"; + + await makeJSONRequest(url, method); +} \ No newline at end of file diff --git a/ui/src/api/med_log.ts b/ui/src/api/med_log.ts new file mode 100644 index 0000000..93a95ee --- /dev/null +++ b/ui/src/api/med_log.ts @@ -0,0 +1,61 @@ +import { makeJSONRequest } from "./request"; + +/* +pub struct MedicationLog { + pub uuid: String, + pub med_uuid: String, + pub dosage: i32, + pub time_actual: chrono::NaiveDateTime, + pub time_expected: chrono::NaiveDateTime, + pub dose_offset: f32, + pub created: chrono::NaiveDateTime, + pub updated: chrono::NaiveDateTime, +} + +*/ + +export interface MedicationLog { + uuid: string, + med_uuid: string, + dosage: number, + time_actual: string, + time_expected: string, + dose_offset: number, + created: string, + updated: string, +} + +export async function projectNextDose(med_uuid: string): Promise { + const url = `/api/med/directive/${med_uuid}/project_next_dose`; + const method = "GET"; + return (await makeJSONRequest(url, method)).data; +} + +export interface GetMedicationLogParams { + until?: Date + limit: number, +} + +export async function getMedicationLog(med_uuid: string, params: GetMedicationLogParams): Promise { + let url = `/api/med/directive/${med_uuid}/log?limit=${params.limit}`; + if (params.until) { + url += `&until=${params.until.toISOString()}`; + } + const method = "GET"; + return (await makeJSONRequest(url, method)).data; +} + +export async function postMedicationLog(med: MedicationLog): Promise { + const uri = `/api/med/directive/${med.med_uuid}/log`; + const method = "POST"; + const body = med; + return (await makeJSONRequest(uri, method, body)).data; +} + + +export async function deleteMedicationLog(med_uuid: string, uuid: string): Promise { + const url = `/api/med/directive/${med_uuid}/log/${uuid}`; + const method = "DELETE"; + + await makeJSONRequest(url, method); +} \ No newline at end of file diff --git a/ui/src/api/request.ts b/ui/src/api/request.ts new file mode 100644 index 0000000..3ebe6f4 --- /dev/null +++ b/ui/src/api/request.ts @@ -0,0 +1,23 @@ + + +type APIStatus = "Ok" | "Error"; + +type APIResponse = { + code: number; + status: APIStatus, + message: string, + data: T, + +} + +export async function makeJSONRequest(url: string, method: string, body?: any): Promise> { + const response = await fetch(url, { + method: method, + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(body) + }); + const data = await response.json(); + return data; +} \ No newline at end of file diff --git a/ui/src/api/server_info.ts b/ui/src/api/server_info.ts new file mode 100644 index 0000000..79acb84 --- /dev/null +++ b/ui/src/api/server_info.ts @@ -0,0 +1,11 @@ +import { makeJSONRequest } from "./request"; + +export interface ServerInfo { + version: string, + profile: string, +}; + +export async function getServerInfo(): Promise { + const response = await makeJSONRequest("/api/server_info", "GET"); + return response.data; +} \ No newline at end of file diff --git a/ui/src/api/time.ts b/ui/src/api/time.ts new file mode 100644 index 0000000..3490df4 --- /dev/null +++ b/ui/src/api/time.ts @@ -0,0 +1,12 @@ +export function format_rust_naive_date(date: Date): string { + const rfc3339 = date.toISOString(); + + // Rust's NaiveDateTime::parse_from_rfc3339() doesn't like the trailing 'Z' + // that JS's toISOString() adds, so we strip it off. + return rfc3339.slice(0, rfc3339.length - 1); + +} + +export function parse_rust_naive_date(date: string): Date { + return new Date(Date.parse(date + "Z")); +} \ No newline at end of file diff --git a/ui/src/assets/react.svg b/ui/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/ui/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/src/components/EnsureRole.tsx b/ui/src/components/EnsureRole.tsx new file mode 100644 index 0000000..03d0647 --- /dev/null +++ b/ui/src/components/EnsureRole.tsx @@ -0,0 +1,18 @@ +import { useContext } from "react"; +import { LoginContext } from "../context/LoginContext"; +import { Role } from "../api/auth"; +import LoginDialog from "../pages/LoginDialog"; + +export function EnsureRole({ children, role }: { children: React.ReactNode, role: Role }) { + const { auth } = useContext(LoginContext); + + if (auth.roles.includes(role)) { + return <>{children}; + } else { + return <> { + if (!success) { + window.history.back(); + } + }} />; + } +} \ No newline at end of file diff --git a/ui/src/components/TimeBody.tsx b/ui/src/components/TimeBody.tsx new file mode 100644 index 0000000..c07baca --- /dev/null +++ b/ui/src/components/TimeBody.tsx @@ -0,0 +1,65 @@ +import { Tooltip, Typography } from "@mui/material"; +import { useEffect, useState } from "react"; + +interface TimeBodyProps { + time: Date | null; +} + + +function formatRelativeTime(time: Date): [string, number] { + const now = new Date(); + const diff = now.getTime() - time.getTime(); + if (diff >= 0) { + if (diff < 1000) { + return ["just now", 1000]; + } + if (diff < 60 * 1000) { + return [`${Math.floor(diff / 1000)} seconds ago`, 1000]; + } else if (diff < 60 * 60 * 1000) { + return [`${Math.floor(diff / 1000 / 60)} minutes ago`, 60 * 1000]; + } else if (diff < 24 * 60 * 60 * 1000) { + return [`${Math.floor(diff / 1000 / 60 / 60)} hours ago`, 60 * 60 * 1000]; + } else { + return [`${Math.floor(diff / 1000 / 60 / 60 / 24)} days ago`, 24 * 60 * 60 * 1000]; + } + } else { + if (diff > -1000) { + return ["just now", 1000]; + } + if (diff > -60 * 1000) { + return [`${Math.floor(-diff / 1000)} seconds from now`, 1000]; + } else if (diff > -60 * 60 * 1000) { + return [`${Math.floor(-diff / 1000 / 60)} minutes from now`, 60 * 1000]; + } else if (diff > -24 * 60 * 60 * 1000) { + return [`${Math.floor(-diff / 1000 / 60 / 60)} hours from now`, 60 * 60 * 1000]; + } else { + return [`${Math.floor(-diff / 1000 / 60 / 60 / 24)} days from now`, 24 * 60 * 60 * 1000]; + } + } +} + +export default function TimeBody(props: TimeBodyProps) { + const { time } = props; + if (time === null) { + return <>N/A; + } + + const [relativeTime, setRelativeTime] = useState(formatRelativeTime(time)[0]); + + useEffect(() => { + let timer: number | null = null; + const update = () => { + const [relativeTime, interval] = formatRelativeTime(time); + setRelativeTime(relativeTime); + timer = setTimeout(update, interval); + } + update(); + return () => clearTimeout(timer!); + }, [time]); + + + return + {relativeTime} + + +} \ No newline at end of file diff --git a/ui/src/context/LoginContext.ts b/ui/src/context/LoginContext.ts new file mode 100644 index 0000000..c20e2b3 --- /dev/null +++ b/ui/src/context/LoginContext.ts @@ -0,0 +1,8 @@ +import { createContext } from "react"; +import { AuthInfo, EmptyAuthInfo } from "../api/auth"; + +export const LoginContext = createContext({ + auth: EmptyAuthInfo, + setAuth: (_auth: AuthInfo) => { }, + refreshAuth: () => { }, +}); \ No newline at end of file diff --git a/ui/src/index.css b/ui/src/index.css new file mode 100644 index 0000000..3e3b6a1 --- /dev/null +++ b/ui/src/index.css @@ -0,0 +1,13 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} \ No newline at end of file diff --git a/ui/src/main.tsx b/ui/src/main.tsx new file mode 100644 index 0000000..91c03f3 --- /dev/null +++ b/ui/src/main.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App.tsx' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + + + , +) diff --git a/ui/src/pages/FramePage.tsx b/ui/src/pages/FramePage.tsx new file mode 100644 index 0000000..775358a --- /dev/null +++ b/ui/src/pages/FramePage.tsx @@ -0,0 +1,21 @@ +import { useEffect } from "react"; + + +type FramePageProps = { + url: string; + sx?: React.CSSProperties; +} + +export default function FramePage(props: FramePageProps) { + useEffect(() => { + console.log("FramePage mounted, url: " + props.url); + return () => { + console.log("FramePage unmounted, url: " + props.url); + } + }, [props.url]); + return ( + + ) +} \ No newline at end of file diff --git a/ui/src/pages/GradesPage.tsx b/ui/src/pages/GradesPage.tsx new file mode 100644 index 0000000..f0b98aa --- /dev/null +++ b/ui/src/pages/GradesPage.tsx @@ -0,0 +1,195 @@ +import { useEffect, useState } from "react"; +import { Grading, getGradings, GetGradingsResponse } from "../api/canvas_lms"; +import { Typography, Box, Button, Link } from "@mui/material"; +import { DataGrid, GridCellParams, GridColDef } from "@mui/x-data-grid"; +import TimeBody from "../components/TimeBody"; + + +const renderTimeCell = (params: GridCellParams) => { + const value = params.value as Date | null; + return ; +} + + +const grading_columns: GridColDef[] = [ + { + field: "course_code", + headerName: "Course", + width: 100, + }, + { + field: "name", + headerName: "Name", + width: 200, + renderCell: (params) => { + const row = params.row as Grading; + return {row.name}; + } + }, + { + field: "grade", + headerName: "Grade", + minWidth: 250, + valueGetter: (params) => { + const row = params.row as Grading; + if (row.grade_hidden) { + return "Hidden"; + } else if (row.score === null) { + return "Not Graded"; + } + const percentage = row.score! / row.possible_points; + return `${row.score!.toFixed(2)} (${row.grade}) / ${row.possible_points} (${(percentage * 100).toFixed(2)}%)`; + }, + flex: 1, + cellClassName: (params) => { + const row = params.row as Grading; + if (row.grade_hidden) { + return "grade_hidden"; + } else if (row.score === null) { + return "grade_not_graded"; + } + const percentage = row.score! / row.possible_points; + if (percentage < 0.6) { + return "grade_bad"; + } else if (percentage < 0.8) { + return "grade_ok"; + } else if (percentage < 1.0) { + return "grade_good"; + } else { + return "grade_perfect"; + } + } + }, + { + field: "graded_at", + headerName: "Graded At", + minWidth: 100, + valueGetter: (params) => { + const row = params.row as Grading; + return row.graded_at ? new Date(row.graded_at) : null; + }, + flex: 1, + renderCell: renderTimeCell + }, + { + field: "posted_at", + headerName: "Posted At", + minWidth: 100, + valueGetter: (params) => { + const row = params.row as Grading; + return row.posted_at ? new Date(row.posted_at) : null; + }, + flex: 1, + renderCell: renderTimeCell + }, + { + field: "updated_at", + headerName: "Updated At", + minWidth: 100, + valueGetter: (params) => { + const row = params.row as Grading; + let ret = null; + if (row.posted_at !== null) + ret = new Date(row.posted_at); + if (row.graded_at !== null && (ret == null || new Date(row.graded_at) > ret)) + ret = new Date(row.graded_at); + return ret; + }, + flex: 1, + renderCell: renderTimeCell + } +]; + +function GradePage() { + const [mounted, setMounted] = useState(false); + const [gradings, setGradings] = useState(null); + const [updating, setUpdating] = useState(false); + + + const updateGradings = (force: boolean) => { + setUpdating(true); + getGradings(force).then((gradings) => { + setGradings(gradings); + }).catch((error) => { + console.error(error); + }).finally(() => { + setUpdating(false); + }); + } + + if (!mounted) { + setMounted(true); + updateGradings(false); + } + + useEffect(() => { + const interval = setInterval(() => updateGradings(false), 5 * 60 * 1000); + return () => clearInterval(interval); + }, []); + + + return ( + <> + + Grades + { + gradings ? + + + Last updated: + + + row.submission_id} + autoHeight={true} + sortModel={[{ + field: "updated_at", + sort: "desc", + }]} + initialState={{ + pagination: { + paginationModel: { + pageSize: 25, + } + }, + }} + sx={{ + "& .grade_hidden": { + color: "grey", + }, + "& .grade_not_graded": { + color: "grey", + }, + "& .grade_bad": { + color: "red", + }, + "& .grade_ok": { + color: "orange", + }, + "& .grade_good": { + color: "blue", + }, + "& .grade_perfect": { + color: "green", + }, + }} + /> + + : "Loading..." + } + + + ) +} + +export default GradePage \ No newline at end of file diff --git a/ui/src/pages/HomePage.tsx b/ui/src/pages/HomePage.tsx new file mode 100644 index 0000000..b7da1cd --- /dev/null +++ b/ui/src/pages/HomePage.tsx @@ -0,0 +1,117 @@ +import { styled } from '@mui/material/styles'; +import { useState, useEffect, useContext } from 'react'; +import Grid from '@mui/material/Grid'; +import Card from '@mui/material/Card'; +import { CardContent, CardHeader, Typography, Button, Box } from '@mui/material'; +import { useTheme } from '@mui/material/styles'; + +import { LoginContext } from '../context/LoginContext'; +import { postLogout } from '../api/auth'; +import { useNavigate } from 'react-router-dom'; + +const CardItem = styled(Card)(({ theme }) => ({ + ...theme.typography.body2, + padding: theme.spacing(1), + textAlign: 'center', + backgroundColor: "#fff4f5", +})) + +function dateInZone(zone?: string) { + if (zone) + return new Date(new Date().toLocaleString("en-US", { timeZone: zone })); + return new Date() +} + +function ClockCard() { + + const theme = useTheme(); + const [time, setTime] = useState(new Date()); + + useEffect(() => { + const timer = setInterval(() => { + setTime(new Date()); + }, 1000); + return () => clearInterval(timer); + }, []); + + return ( + + + + {time.toLocaleTimeString()} + +
+ { + ["America/Los_Angeles", "America/New_York", "America/Chicago", + "Asia/Tokyo", "Asia/Shanghai", "UTC"].map((zone) => { + return ( + + + {zone} + + + {dateInZone(zone).toLocaleTimeString()} + + + ) + }) + } +
+
); +} + +function HomePage() { + + const theme = useTheme(); + const { auth, refreshAuth } = useContext(LoginContext); + const navigate = useNavigate(); + + return ( + + + + + + + + 夜明け前が一番暗い。 + + + The darkest hour is just before the dawn. + +
+ + This is yoake.yumechi.jp, Yumechi's PIM.
+ Built with axum and React. +
+
+ { + auth.valid ? + <> + + Welcome, {auth.display_name} + + + + : + <> + + You are not logged in + + + + } + +
+
+
+ + + +
+ ); +} + +export default HomePage; \ No newline at end of file diff --git a/ui/src/pages/LoginDialog.tsx b/ui/src/pages/LoginDialog.tsx new file mode 100644 index 0000000..a655df7 --- /dev/null +++ b/ui/src/pages/LoginDialog.tsx @@ -0,0 +1,89 @@ +import { + Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, + TextField, Button, Alert +} from "@mui/material"; +import { Box } from "@mui/system"; +import { postLogin } from "../api/auth"; +import { LoginContext } from "../context/LoginContext"; +import { useContext, useRef, useState } from "react"; +import { Navigate, NavigateFunction, useNavigate } from "react-router-dom"; + + +type Props = { + open: boolean; + onClose?: (success: boolean, navigate: NavigateFunction) => void; +} +export default function LoginDialog({ open, onClose }: Props) { + + const { auth, refreshAuth } = useContext(LoginContext); + const usernameNode = useRef(null); + const passwordNode = useRef(null); + const [loginError, setLoginError] = useState(null); + const navigate = useNavigate(); + + return auth.valid ? : + + ( + onClose?.(false, navigate)}> + { + e.preventDefault(); + if ( + !usernameNode.current || + !passwordNode.current || + !usernameNode.current + ) { + setLoginError("Internal error: missing form elements."); + return; + } + const username = usernameNode.current?.value; + const password = passwordNode.current?.value; + postLogin({ username: username!, password: password! }).then((auth) => { + console.log("Got new auth state: ", auth); + refreshAuth(); + onClose?.(true, navigate); + }).catch((error) => { + console.error(error); + setLoginError(error.message); + refreshAuth(); + }); + }}> + Login + + { + loginError ? + {loginError} + : Present Credentials. + } + + Userpass login: + + + + + + + { + onClose ? + + : + + } + + + + + ) +} \ No newline at end of file diff --git a/ui/src/pages/MedsPage.tsx b/ui/src/pages/MedsPage.tsx new file mode 100644 index 0000000..232d9bc --- /dev/null +++ b/ui/src/pages/MedsPage.tsx @@ -0,0 +1,273 @@ +import { useState } from "react"; +import { Accordion, AccordionDetails, AccordionSummary, Typography, TextField, Box, Divider, Button } from "@mui/material" +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import { parseShorthand, Medication, postDirective, patchDirective, getDirectives, deleteDirective, formatShorthand } from "../api/med_directive"; +import { MedicationLog, deleteMedicationLog, getMedicationLog, postMedicationLog, projectNextDose } from "../api/med_log"; +import { DataGrid, GridColDef } from "@mui/x-data-grid"; +import TimeBody from "../components/TimeBody"; +import { format_rust_naive_date, parse_rust_naive_date } from "../api/time"; + + +function DirectionEditor({ onUpdate }: { onUpdate?: (medication: Medication) => void }) { + const [direction, setDirection] = useState(null); + + return ( + + } + > + Edit Direction + + + { + parseShorthand(event.target.value) + .then((medication) => { + medication.uuid = direction?.uuid || ""; + setDirection(medication); + }) + }} + /> + { + setDirection({ ...direction!, uuid: event.target.value }); + }} /> + { + setDirection({ ...direction!, name: event.target.value }); + }} /> + { + setDirection({ ...direction!, dosage: parseInt(event.target.value) }); + }} /> + { + setDirection({ ...direction!, dosage_unit: event.target.value }); + }} /> +
+ { + setDirection({ ...direction!, period_hours: parseInt(event.target.value) }); + }} /> + { + setDirection({ ...direction!, flags: event.target.value }); + }} /> + { + setDirection({ ...direction!, options: event.target.value }); + }} /> + + + +
+
+ ) +} + + +interface MedPanelForm { + dosage: number; + time_actual: Date | null; +} + +function MedPanel({ medication }: { medication: Medication }) { + const [shorthand, setShorthand] = useState(""); + const [mounted, setMounted] = useState(false); + const [log, setLog] = useState([]); + const [nextDose, setNextDose] = useState(null); + const [form, setForm] = useState({ dosage: medication.dosage, time_actual: null }); + + const updateShorthand = () => { + formatShorthand(medication).then((shorthand) => { + setShorthand(shorthand); + }); + } + + + const updateLog = () => { + getMedicationLog(medication.uuid, { limit: 100 }).then((log) => { + setLog(log); + }); + } + + const updateNextDose = () => { + projectNextDose(medication.uuid).then((nextDose) => { + setNextDose(nextDose); + }); + } + + + if (!mounted) { + updateShorthand(); + updateLog(); + updateNextDose(); + setMounted(true); + } + + const med_log_columns: GridColDef[] = [ + { + field: "Action", + headerName: "Action", minWidth: 100, + renderCell: (params) => { + const log = params.row as MedicationLog; + return ( + + ) + } + }, + { field: 'dosage', headerName: 'Dosage', minWidth: 100 }, + { + field: 'time_actual', headerName: 'Time Actual', minWidth: 200, + renderCell: (params) => + + + }, + { + field: 'time_expected', headerName: 'Time Expected', minWidth: 200, + renderCell: (params) => + + }, + { + field: 'dose_offset', headerName: 'Dose Offset', minWidth: 100, + renderCell: (params) => { + const log = params.row as MedicationLog; + return ( + <> + { + log.dose_offset.toFixed(2) + } + + ) + } + }, + ]; + + + return ( + + } + > + + {shorthand} + { + nextDose ? Next Dose: + { + nextDose.dose_offset >= 0 ? now : + } + : null + } + + + + + UUID: {medication.uuid} + + + { + setForm({ ...form, dosage: parseInt(event.target.value) }); + }} /> + { + setForm({ ...form, time_actual: new Date(event.target.value) }); + }} /> + + + row.uuid} + autoHeight + initialState={{ + pagination: { + paginationModel: { + pageSize: 25, + } + }, + }} + /> + + + ) +} + +function MedsPage() { + const [medications, setMedications] = useState(null); + const [mounted, setMounted] = useState(false); + + const refreshMedications = () => { + getDirectives().then((response) => { + setMedications(response); + }); + } + + if (!mounted) { + refreshMedications(); + setMounted(true); + } + + return ( + + { + medications?.map((medication) => { + return + }) + } + { + refreshMedications(); + }} /> + + ) +} + +export default MedsPage \ No newline at end of file diff --git a/ui/src/theme.ts b/ui/src/theme.ts new file mode 100644 index 0000000..06cbcc1 --- /dev/null +++ b/ui/src/theme.ts @@ -0,0 +1,11 @@ +import { createTheme } from '@mui/material/styles'; + +const theme = createTheme({ + palette: { + primary: { + main: "#F8C3CD", + }, + } +}) + +export default theme; \ No newline at end of file diff --git a/ui/src/vite-env.d.ts b/ui/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/ui/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/ui/tsconfig.json b/ui/tsconfig.json new file mode 100644 index 0000000..a7fc6fb --- /dev/null +++ b/ui/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/ui/tsconfig.node.json b/ui/tsconfig.node.json new file mode 100644 index 0000000..42872c5 --- /dev/null +++ b/ui/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/ui/vite.config.ts b/ui/vite.config.ts new file mode 100644 index 0000000..d4fcda0 --- /dev/null +++ b/ui/vite.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vitejs.dev/config/ +export default defineConfig({ + base: "/ui/", + plugins: [react()], + server: { + proxy: { + "/api": { + target: "http://localhost:3000", + } + } + } +}) diff --git a/ui/yarn.lock b/ui/yarn.lock new file mode 100644 index 0000000..8680f06 --- /dev/null +++ b/ui/yarn.lock @@ -0,0 +1,1983 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@ampproject/remapping@^2.2.0": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" + integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.5.tgz#234d98e1551960604f1246e6475891a570ad5658" + integrity sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ== + dependencies: + "@babel/highlight" "^7.22.5" + +"@babel/compat-data@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.22.5.tgz#b1f6c86a02d85d2dd3368a2b67c09add8cd0c255" + integrity sha512-4Jc/YuIaYqKnDDz892kPIledykKg12Aw1PYX5i/TY28anJtacvM1Rrr8wbieB9GfEJwlzqT0hUEao0CxEebiDA== + +"@babel/core@^7.21.4": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.22.5.tgz#d67d9747ecf26ee7ecd3ebae1ee22225fe902a89" + integrity sha512-SBuTAjg91A3eKOvD+bPEz3LlhHZRNu1nFOVts9lzDJTXshHTjII0BAtDS3Y2DAkdZdDKWVZGVwkDfc4Clxn1dg== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.22.5" + "@babel/generator" "^7.22.5" + "@babel/helper-compilation-targets" "^7.22.5" + "@babel/helper-module-transforms" "^7.22.5" + "@babel/helpers" "^7.22.5" + "@babel/parser" "^7.22.5" + "@babel/template" "^7.22.5" + "@babel/traverse" "^7.22.5" + "@babel/types" "^7.22.5" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.2" + semver "^6.3.0" + +"@babel/generator@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.22.5.tgz#1e7bf768688acfb05cf30b2369ef855e82d984f7" + integrity sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA== + dependencies: + "@babel/types" "^7.22.5" + "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" + jsesc "^2.5.1" + +"@babel/helper-compilation-targets@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.5.tgz#fc7319fc54c5e2fa14b2909cf3c5fd3046813e02" + integrity sha512-Ji+ywpHeuqxB8WDxraCiqR0xfhYjiDE/e6k7FuIaANnoOFxAHskHChz4vA1mJC9Lbm01s1PVAGhQY4FUKSkGZw== + dependencies: + "@babel/compat-data" "^7.22.5" + "@babel/helper-validator-option" "^7.22.5" + browserslist "^4.21.3" + lru-cache "^5.1.1" + semver "^6.3.0" + +"@babel/helper-environment-visitor@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz#f06dd41b7c1f44e1f8da6c4055b41ab3a09a7e98" + integrity sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q== + +"@babel/helper-function-name@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz#ede300828905bb15e582c037162f99d5183af1be" + integrity sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ== + dependencies: + "@babel/template" "^7.22.5" + "@babel/types" "^7.22.5" + +"@babel/helper-hoist-variables@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" + integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz#1a8f4c9f4027d23f520bd76b364d44434a72660c" + integrity sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-module-transforms@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.22.5.tgz#0f65daa0716961b6e96b164034e737f60a80d2ef" + integrity sha512-+hGKDt/Ze8GFExiVHno/2dvG5IdstpzCq0y4Qc9OJ25D4q3pKfiIP/4Vp3/JvhDkLKsDK2api3q3fpIgiIF5bw== + dependencies: + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-module-imports" "^7.22.5" + "@babel/helper-simple-access" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.5" + "@babel/template" "^7.22.5" + "@babel/traverse" "^7.22.5" + "@babel/types" "^7.22.5" + +"@babel/helper-plugin-utils@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz#dd7ee3735e8a313b9f7b05a773d892e88e6d7295" + integrity sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg== + +"@babel/helper-simple-access@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz#4938357dc7d782b80ed6dbb03a0fba3d22b1d5de" + integrity sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-split-export-declaration@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.5.tgz#88cf11050edb95ed08d596f7a044462189127a08" + integrity sha512-thqK5QFghPKWLhAV321lxF95yCg2K3Ob5yw+M3VHWfdia0IkPXUtoLH8x/6Fh486QUvzhb8YOWHChTVen2/PoQ== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-string-parser@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" + integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== + +"@babel/helper-validator-identifier@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz#9544ef6a33999343c8740fa51350f30eeaaaf193" + integrity sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ== + +"@babel/helper-validator-option@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz#de52000a15a177413c8234fa3a8af4ee8102d0ac" + integrity sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw== + +"@babel/helpers@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.22.5.tgz#74bb4373eb390d1ceed74a15ef97767e63120820" + integrity sha512-pSXRmfE1vzcUIDFQcSGA5Mr+GxBV9oiRKDuDxXvWQQBCh8HoIjs/2DlDB7H8smac1IVrB9/xdXj2N3Wol9Cr+Q== + dependencies: + "@babel/template" "^7.22.5" + "@babel/traverse" "^7.22.5" + "@babel/types" "^7.22.5" + +"@babel/highlight@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.5.tgz#aa6c05c5407a67ebce408162b7ede789b4d22031" + integrity sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw== + dependencies: + "@babel/helper-validator-identifier" "^7.22.5" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@babel/parser@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.5.tgz#721fd042f3ce1896238cf1b341c77eb7dee7dbea" + integrity sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q== + +"@babel/plugin-transform-react-jsx-self@^7.21.0": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.22.5.tgz#ca2fdc11bc20d4d46de01137318b13d04e481d8e" + integrity sha512-nTh2ogNUtxbiSbxaT4Ds6aXnXEipHweN9YRgOX/oNXdf0cCrGn/+2LozFa3lnPV5D90MkjhgckCPBrsoSc1a7g== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-react-jsx-source@^7.19.6": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.22.5.tgz#49af1615bfdf6ed9d3e9e43e425e0b2b65d15b6c" + integrity sha512-yIiRO6yobeEIaI0RTbIr8iAK9FcBHLtZq0S89ZPjDLQXBA4xvghaKqI0etp/tF3htTM0sazJKKLz9oEiGRtu7w== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.21.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.5.tgz#8564dd588182ce0047d55d7a75e93921107b57ec" + integrity sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA== + dependencies: + regenerator-runtime "^0.13.11" + +"@babel/template@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.5.tgz#0c8c4d944509875849bd0344ff0050756eefc6ec" + integrity sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw== + dependencies: + "@babel/code-frame" "^7.22.5" + "@babel/parser" "^7.22.5" + "@babel/types" "^7.22.5" + +"@babel/traverse@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.22.5.tgz#44bd276690db6f4940fdb84e1cb4abd2f729ccd1" + integrity sha512-7DuIjPgERaNo6r+PZwItpjCZEa5vyw4eJGufeLxrPdBXBoLcCJCIasvK6pK/9DVNrLZTLFhUGqaC6X/PA007TQ== + dependencies: + "@babel/code-frame" "^7.22.5" + "@babel/generator" "^7.22.5" + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-function-name" "^7.22.5" + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.5" + "@babel/parser" "^7.22.5" + "@babel/types" "^7.22.5" + debug "^4.1.0" + globals "^11.1.0" + +"@babel/types@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.22.5.tgz#cd93eeaab025880a3a47ec881f4b096a5b786fbe" + integrity sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA== + dependencies: + "@babel/helper-string-parser" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.5" + to-fast-properties "^2.0.0" + +"@emotion/babel-plugin@^11.11.0": + version "11.11.0" + resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz#c2d872b6a7767a9d176d007f5b31f7d504bb5d6c" + integrity sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ== + dependencies: + "@babel/helper-module-imports" "^7.16.7" + "@babel/runtime" "^7.18.3" + "@emotion/hash" "^0.9.1" + "@emotion/memoize" "^0.8.1" + "@emotion/serialize" "^1.1.2" + babel-plugin-macros "^3.1.0" + convert-source-map "^1.5.0" + escape-string-regexp "^4.0.0" + find-root "^1.1.0" + source-map "^0.5.7" + stylis "4.2.0" + +"@emotion/cache@^11.11.0": + version "11.11.0" + resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.11.0.tgz#809b33ee6b1cb1a625fef7a45bc568ccd9b8f3ff" + integrity sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ== + dependencies: + "@emotion/memoize" "^0.8.1" + "@emotion/sheet" "^1.2.2" + "@emotion/utils" "^1.2.1" + "@emotion/weak-memoize" "^0.3.1" + stylis "4.2.0" + +"@emotion/hash@^0.9.1": + version "0.9.1" + resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.9.1.tgz#4ffb0055f7ef676ebc3a5a91fb621393294e2f43" + integrity sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ== + +"@emotion/is-prop-valid@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz#23116cf1ed18bfeac910ec6436561ecb1a3885cc" + integrity sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw== + dependencies: + "@emotion/memoize" "^0.8.1" + +"@emotion/memoize@^0.8.1": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.1.tgz#c1ddb040429c6d21d38cc945fe75c818cfb68e17" + integrity sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA== + +"@emotion/react@^11.11.1": + version "11.11.1" + resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.11.1.tgz#b2c36afac95b184f73b08da8c214fdf861fa4157" + integrity sha512-5mlW1DquU5HaxjLkfkGN1GA/fvVGdyHURRiX/0FHl2cfIfRxSOfmxEH5YS43edp0OldZrZ+dkBKbngxcNCdZvA== + dependencies: + "@babel/runtime" "^7.18.3" + "@emotion/babel-plugin" "^11.11.0" + "@emotion/cache" "^11.11.0" + "@emotion/serialize" "^1.1.2" + "@emotion/use-insertion-effect-with-fallbacks" "^1.0.1" + "@emotion/utils" "^1.2.1" + "@emotion/weak-memoize" "^0.3.1" + hoist-non-react-statics "^3.3.1" + +"@emotion/serialize@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.1.2.tgz#017a6e4c9b8a803bd576ff3d52a0ea6fa5a62b51" + integrity sha512-zR6a/fkFP4EAcCMQtLOhIgpprZOwNmCldtpaISpvz348+DP4Mz8ZoKaGGCQpbzepNIUWbq4w6hNZkwDyKoS+HA== + dependencies: + "@emotion/hash" "^0.9.1" + "@emotion/memoize" "^0.8.1" + "@emotion/unitless" "^0.8.1" + "@emotion/utils" "^1.2.1" + csstype "^3.0.2" + +"@emotion/sheet@^1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.2.2.tgz#d58e788ee27267a14342303e1abb3d508b6d0fec" + integrity sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA== + +"@emotion/styled@^11.11.0": + version "11.11.0" + resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-11.11.0.tgz#26b75e1b5a1b7a629d7c0a8b708fbf5a9cdce346" + integrity sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng== + dependencies: + "@babel/runtime" "^7.18.3" + "@emotion/babel-plugin" "^11.11.0" + "@emotion/is-prop-valid" "^1.2.1" + "@emotion/serialize" "^1.1.2" + "@emotion/use-insertion-effect-with-fallbacks" "^1.0.1" + "@emotion/utils" "^1.2.1" + +"@emotion/unitless@^0.8.1": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.8.1.tgz#182b5a4704ef8ad91bde93f7a860a88fd92c79a3" + integrity sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ== + +"@emotion/use-insertion-effect-with-fallbacks@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz#08de79f54eb3406f9daaf77c76e35313da963963" + integrity sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw== + +"@emotion/utils@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.2.1.tgz#bbab58465738d31ae4cb3dbb6fc00a5991f755e4" + integrity sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg== + +"@emotion/weak-memoize@^0.3.1": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz#d0fce5d07b0620caa282b5131c297bb60f9d87e6" + integrity sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww== + +"@esbuild/android-arm64@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz#bafb75234a5d3d1b690e7c2956a599345e84a2fd" + integrity sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA== + +"@esbuild/android-arm@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.17.19.tgz#5898f7832c2298bc7d0ab53701c57beb74d78b4d" + integrity sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A== + +"@esbuild/android-x64@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.17.19.tgz#658368ef92067866d95fb268719f98f363d13ae1" + integrity sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww== + +"@esbuild/darwin-arm64@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz#584c34c5991b95d4d48d333300b1a4e2ff7be276" + integrity sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg== + +"@esbuild/darwin-x64@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz#7751d236dfe6ce136cce343dce69f52d76b7f6cb" + integrity sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw== + +"@esbuild/freebsd-arm64@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz#cacd171665dd1d500f45c167d50c6b7e539d5fd2" + integrity sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ== + +"@esbuild/freebsd-x64@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz#0769456eee2a08b8d925d7c00b79e861cb3162e4" + integrity sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ== + +"@esbuild/linux-arm64@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz#38e162ecb723862c6be1c27d6389f48960b68edb" + integrity sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg== + +"@esbuild/linux-arm@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz#1a2cd399c50040184a805174a6d89097d9d1559a" + integrity sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA== + +"@esbuild/linux-ia32@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz#e28c25266b036ce1cabca3c30155222841dc035a" + integrity sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ== + +"@esbuild/linux-loong64@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz#0f887b8bb3f90658d1a0117283e55dbd4c9dcf72" + integrity sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ== + +"@esbuild/linux-mips64el@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz#f5d2a0b8047ea9a5d9f592a178ea054053a70289" + integrity sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A== + +"@esbuild/linux-ppc64@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz#876590e3acbd9fa7f57a2c7d86f83717dbbac8c7" + integrity sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg== + +"@esbuild/linux-riscv64@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz#7f49373df463cd9f41dc34f9b2262d771688bf09" + integrity sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA== + +"@esbuild/linux-s390x@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz#e2afd1afcaf63afe2c7d9ceacd28ec57c77f8829" + integrity sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q== + +"@esbuild/linux-x64@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz#8a0e9738b1635f0c53389e515ae83826dec22aa4" + integrity sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw== + +"@esbuild/netbsd-x64@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz#c29fb2453c6b7ddef9a35e2c18b37bda1ae5c462" + integrity sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q== + +"@esbuild/openbsd-x64@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz#95e75a391403cb10297280d524d66ce04c920691" + integrity sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g== + +"@esbuild/sunos-x64@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz#722eaf057b83c2575937d3ffe5aeb16540da7273" + integrity sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg== + +"@esbuild/win32-arm64@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz#9aa9dc074399288bdcdd283443e9aeb6b9552b6f" + integrity sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag== + +"@esbuild/win32-ia32@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz#95ad43c62ad62485e210f6299c7b2571e48d2b03" + integrity sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw== + +"@esbuild/win32-x64@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz#8cfaf2ff603e9aabb910e9c0558c26cf32744061" + integrity sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA== + +"@eslint-community/eslint-utils@^4.2.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" + integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== + dependencies: + eslint-visitor-keys "^3.3.0" + +"@eslint-community/regexpp@^4.4.0": + version "4.5.1" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.5.1.tgz#cdd35dce4fa1a89a4fd42b1599eb35b3af408884" + integrity sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ== + +"@eslint/eslintrc@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.0.3.tgz#4910db5505f4d503f27774bf356e3704818a0331" + integrity sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^9.5.2" + globals "^13.19.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" + +"@eslint/js@8.42.0": + version "8.42.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.42.0.tgz#484a1d638de2911e6f5a30c12f49c7e4a3270fb6" + integrity sha512-6SWlXpWU5AvId8Ac7zjzmIOqMOba/JWY8XZ4A7q7Gn1Vlfg/SFFIlrtHXt9nPn4op9ZPAkl91Jao+QQv3r/ukw== + +"@humanwhocodes/config-array@^0.11.10": + version "0.11.10" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.10.tgz#5a3ffe32cc9306365fb3fd572596cd602d5e12d2" + integrity sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ== + dependencies: + "@humanwhocodes/object-schema" "^1.2.1" + debug "^4.1.1" + minimatch "^3.0.5" + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/object-schema@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" + integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== + +"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" + integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/resolve-uri@3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" + integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== + +"@jridgewell/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + +"@jridgewell/sourcemap-codec@1.4.14": + version "1.4.14" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + +"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": + version "0.3.18" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz#25783b2086daf6ff1dcb53c9249ae480e4dd4cd6" + integrity sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA== + dependencies: + "@jridgewell/resolve-uri" "3.1.0" + "@jridgewell/sourcemap-codec" "1.4.14" + +"@mui/base@5.0.0-beta.4": + version "5.0.0-beta.4" + resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-beta.4.tgz#e3f4f4a056b88ab357194a245e223177ce35e0b0" + integrity sha512-ejhtqYJpjDgHGEljjMBQWZ22yEK0OzIXNa7toJmmXsP4TT3W7xVy8bTJ0TniPDf+JNjrsgfgiFTDGdlEhV1E+g== + dependencies: + "@babel/runtime" "^7.21.0" + "@emotion/is-prop-valid" "^1.2.1" + "@mui/types" "^7.2.4" + "@mui/utils" "^5.13.1" + "@popperjs/core" "^2.11.8" + clsx "^1.2.1" + prop-types "^15.8.1" + react-is "^18.2.0" + +"@mui/core-downloads-tracker@^5.13.4": + version "5.13.4" + resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.13.4.tgz#7e4b491d8081b6d45ae51556d82cb16b31315a19" + integrity sha512-yFrMWcrlI0TqRN5jpb6Ma9iI7sGTHpytdzzL33oskFHNQ8UgrtPas33Y1K7sWAMwCrr1qbWDrOHLAQG4tAzuSw== + +"@mui/icons-material@^5.11.16": + version "5.11.16" + resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-5.11.16.tgz#417fa773c56672e39d6ccfed9ac55591985f0d38" + integrity sha512-oKkx9z9Kwg40NtcIajF9uOXhxiyTZrrm9nmIJ4UjkU2IdHpd4QVLbCc/5hZN/y0C6qzi2Zlxyr9TGddQx2vx2A== + dependencies: + "@babel/runtime" "^7.21.0" + +"@mui/material@^5.13.4": + version "5.13.4" + resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.13.4.tgz#1fed8249c980ed37f9767f9dba8aa3a589495ff3" + integrity sha512-Yq+4f1KLPa/Szd3xqra2hbOAf2Usl8GbubncArM6LIp40mBLtXIdPE29MNtHsbtuzz4g+eidrETgoi3wdbEYfQ== + dependencies: + "@babel/runtime" "^7.21.0" + "@mui/base" "5.0.0-beta.4" + "@mui/core-downloads-tracker" "^5.13.4" + "@mui/system" "^5.13.2" + "@mui/types" "^7.2.4" + "@mui/utils" "^5.13.1" + "@types/react-transition-group" "^4.4.6" + clsx "^1.2.1" + csstype "^3.1.2" + prop-types "^15.8.1" + react-is "^18.2.0" + react-transition-group "^4.4.5" + +"@mui/private-theming@^5.13.1": + version "5.13.1" + resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.13.1.tgz#c3e9a0b44f9c5a51b92cfcfb660536060cb61ed7" + integrity sha512-HW4npLUD9BAkVppOUZHeO1FOKUJWAwbpy0VQoGe3McUYTlck1HezGHQCfBQ5S/Nszi7EViqiimECVl9xi+/WjQ== + dependencies: + "@babel/runtime" "^7.21.0" + "@mui/utils" "^5.13.1" + prop-types "^15.8.1" + +"@mui/styled-engine@^5.13.2": + version "5.13.2" + resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.13.2.tgz#c87bd61c0ab8086d34828b6defe97c02bcd642ef" + integrity sha512-VCYCU6xVtXOrIN8lcbuPmoG+u7FYuOERG++fpY74hPpEWkyFQG97F+/XfTQVYzlR2m7nPjnwVUgATcTCMEaMvw== + dependencies: + "@babel/runtime" "^7.21.0" + "@emotion/cache" "^11.11.0" + csstype "^3.1.2" + prop-types "^15.8.1" + +"@mui/system@^5.13.2": + version "5.13.2" + resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.13.2.tgz#c9aa821049fc75d8ade1c0646dc4d2b67605b8fc" + integrity sha512-TPyWmRJPt0JPVxacZISI4o070xEJ7ftxpVtu6LWuYVOUOINlhoGOclam4iV8PDT3EMQEHuUrwU49po34UdWLlw== + dependencies: + "@babel/runtime" "^7.21.0" + "@mui/private-theming" "^5.13.1" + "@mui/styled-engine" "^5.13.2" + "@mui/types" "^7.2.4" + "@mui/utils" "^5.13.1" + clsx "^1.2.1" + csstype "^3.1.2" + prop-types "^15.8.1" + +"@mui/types@^7.2.4": + version "7.2.4" + resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.4.tgz#b6fade19323b754c5c6de679a38f068fd50b9328" + integrity sha512-LBcwa8rN84bKF+f5sDyku42w1NTxaPgPyYKODsh01U1fVstTClbUoSA96oyRBnSNyEiAVjKm6Gwx9vjR+xyqHA== + +"@mui/utils@^5.13.1": + version "5.13.1" + resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.13.1.tgz#86199e46014215f95da046a5ec803f4a39c96eee" + integrity sha512-6lXdWwmlUbEU2jUI8blw38Kt+3ly7xkmV9ljzY4Q20WhsJMWiNry9CX8M+TaP/HbtuyR8XKsdMgQW7h7MM3n3A== + dependencies: + "@babel/runtime" "^7.21.0" + "@types/prop-types" "^15.7.5" + "@types/react-is" "^18.2.0" + prop-types "^15.8.1" + react-is "^18.2.0" + +"@mui/x-data-grid@^6.7.0": + version "6.7.0" + resolved "https://registry.yarnpkg.com/@mui/x-data-grid/-/x-data-grid-6.7.0.tgz#ebc89a6a8a008712af01ac9ebbd713238607e002" + integrity sha512-gkMjvIT2oMKs7VOxLXxu0NkSKFBhlh/AepHB89xKcf+XVMkKBEwq8VT2GtrccAO+7Lr7ciZhcX2EmiDHsXdhbA== + dependencies: + "@babel/runtime" "^7.21.0" + "@mui/utils" "^5.13.1" + clsx "^1.2.1" + prop-types "^15.8.1" + reselect "^4.1.8" + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@popperjs/core@^2.11.8": + version "2.11.8" + resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" + integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== + +"@remix-run/router@1.6.3": + version "1.6.3" + resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.6.3.tgz#8205baf6e17ef93be35bf62c37d2d594e9be0dad" + integrity sha512-EXJysQ7J3veRECd0kZFQwYYd5sJMcq2O/m60zu1W2l3oVQ9xtub8jTOtYRE0+M2iomyG/W3Ps7+vp2kna0C27Q== + +"@types/json-schema@^7.0.9": + version "7.0.12" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb" + integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA== + +"@types/parse-json@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" + integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== + +"@types/prop-types@*", "@types/prop-types@^15.7.5": + version "15.7.5" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" + integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w== + +"@types/react-dom@^18.0.11": + version "18.2.4" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.4.tgz#13f25bfbf4e404d26f62ac6e406591451acba9e0" + integrity sha512-G2mHoTMTL4yoydITgOGwWdWMVd8sNgyEP85xVmMKAPUBwQWm9wBPQUmvbeF4V3WBY1P7mmL4BkjQ0SqUpf1snw== + dependencies: + "@types/react" "*" + +"@types/react-is@^18.2.0": + version "18.2.0" + resolved "https://registry.yarnpkg.com/@types/react-is/-/react-is-18.2.0.tgz#2f5137853a46017b3d56447940fb3eb92bbf24a5" + integrity sha512-1vz2yObaQkLL7YFe/pme2cpvDsCwI1WXIfL+5eLz0MI9gFG24Re16RzUsI8t9XZn9ZWvgLNDrJBmrqXJO7GNQQ== + dependencies: + "@types/react" "*" + +"@types/react-transition-group@^4.4.6": + version "4.4.6" + resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.6.tgz#18187bcda5281f8e10dfc48f0943e2fdf4f75e2e" + integrity sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew== + dependencies: + "@types/react" "*" + +"@types/react@*", "@types/react@^18.0.37": + version "18.2.9" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.9.tgz#9207f8571afdc59a9c9c30df50e8ad2591ecefaf" + integrity sha512-pL3JAesUkF7PEQGxh5XOwdXGV907te6m1/Qe1ERJLgomojS6Ne790QiA7GUl434JEkFA2aAaB6qJ5z4e1zJn/w== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + +"@types/scheduler@*": + version "0.16.3" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.3.tgz#cef09e3ec9af1d63d2a6cc5b383a737e24e6dcf5" + integrity sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ== + +"@types/semver@^7.3.12": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.0.tgz#591c1ce3a702c45ee15f47a42ade72c2fd78978a" + integrity sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw== + +"@typescript-eslint/eslint-plugin@^5.59.0": + version "5.59.9" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.9.tgz#2604cfaf2b306e120044f901e20c8ed926debf15" + integrity sha512-4uQIBq1ffXd2YvF7MAvehWKW3zVv/w+mSfRAu+8cKbfj3nwzyqJLNcZJpQ/WZ1HLbJDiowwmQ6NO+63nCA+fqA== + dependencies: + "@eslint-community/regexpp" "^4.4.0" + "@typescript-eslint/scope-manager" "5.59.9" + "@typescript-eslint/type-utils" "5.59.9" + "@typescript-eslint/utils" "5.59.9" + debug "^4.3.4" + grapheme-splitter "^1.0.4" + ignore "^5.2.0" + natural-compare-lite "^1.4.0" + semver "^7.3.7" + tsutils "^3.21.0" + +"@typescript-eslint/parser@^5.59.0": + version "5.59.9" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.9.tgz#a85c47ccdd7e285697463da15200f9a8561dd5fa" + integrity sha512-FsPkRvBtcLQ/eVK1ivDiNYBjn3TGJdXy2fhXX+rc7czWl4ARwnpArwbihSOHI2Peg9WbtGHrbThfBUkZZGTtvQ== + dependencies: + "@typescript-eslint/scope-manager" "5.59.9" + "@typescript-eslint/types" "5.59.9" + "@typescript-eslint/typescript-estree" "5.59.9" + debug "^4.3.4" + +"@typescript-eslint/scope-manager@5.59.9": + version "5.59.9" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.59.9.tgz#eadce1f2733389cdb58c49770192c0f95470d2f4" + integrity sha512-8RA+E+w78z1+2dzvK/tGZ2cpGigBZ58VMEHDZtpE1v+LLjzrYGc8mMaTONSxKyEkz3IuXFM0IqYiGHlCsmlZxQ== + dependencies: + "@typescript-eslint/types" "5.59.9" + "@typescript-eslint/visitor-keys" "5.59.9" + +"@typescript-eslint/type-utils@5.59.9": + version "5.59.9" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.59.9.tgz#53bfaae2e901e6ac637ab0536d1754dfef4dafc2" + integrity sha512-ksEsT0/mEHg9e3qZu98AlSrONAQtrSTljL3ow9CGej8eRo7pe+yaC/mvTjptp23Xo/xIf2mLZKC6KPv4Sji26Q== + dependencies: + "@typescript-eslint/typescript-estree" "5.59.9" + "@typescript-eslint/utils" "5.59.9" + debug "^4.3.4" + tsutils "^3.21.0" + +"@typescript-eslint/types@5.59.9": + version "5.59.9" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.9.tgz#3b4e7ae63718ce1b966e0ae620adc4099a6dcc52" + integrity sha512-uW8H5NRgTVneSVTfiCVffBb8AbwWSKg7qcA4Ot3JI3MPCJGsB4Db4BhvAODIIYE5mNj7Q+VJkK7JxmRhk2Lyjw== + +"@typescript-eslint/typescript-estree@5.59.9": + version "5.59.9" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.9.tgz#6bfea844e468427b5e72034d33c9fffc9557392b" + integrity sha512-pmM0/VQ7kUhd1QyIxgS+aRvMgw+ZljB3eDb+jYyp6d2bC0mQWLzUDF+DLwCTkQ3tlNyVsvZRXjFyV0LkU/aXjA== + dependencies: + "@typescript-eslint/types" "5.59.9" + "@typescript-eslint/visitor-keys" "5.59.9" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.3.7" + tsutils "^3.21.0" + +"@typescript-eslint/utils@5.59.9": + version "5.59.9" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.9.tgz#adee890107b5ffe02cd46fdaa6c2125fb3c6c7c4" + integrity sha512-1PuMYsju/38I5Ggblaeb98TOoUvjhRvLpLa1DoTOFaLWqaXl/1iQ1eGurTXgBY58NUdtfTXKP5xBq7q9NDaLKg== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@types/json-schema" "^7.0.9" + "@types/semver" "^7.3.12" + "@typescript-eslint/scope-manager" "5.59.9" + "@typescript-eslint/types" "5.59.9" + "@typescript-eslint/typescript-estree" "5.59.9" + eslint-scope "^5.1.1" + semver "^7.3.7" + +"@typescript-eslint/visitor-keys@5.59.9": + version "5.59.9" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.9.tgz#9f86ef8e95aca30fb5a705bb7430f95fc58b146d" + integrity sha512-bT7s0td97KMaLwpEBckbzj/YohnvXtqbe2XgqNvTl6RJVakY5mvENOTPvw5u66nljfZxthESpDozs86U+oLY8Q== + dependencies: + "@typescript-eslint/types" "5.59.9" + eslint-visitor-keys "^3.3.0" + +"@vitejs/plugin-react@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-4.0.0.tgz#46d1c37c507447d10467be1c111595174555ef28" + integrity sha512-HX0XzMjL3hhOYm+0s95pb0Z7F8O81G7joUHgfDd/9J/ZZf5k4xX6QAMFkKsHFxaHlf6X7GD7+XuaZ66ULiJuhQ== + dependencies: + "@babel/core" "^7.21.4" + "@babel/plugin-transform-react-jsx-self" "^7.21.0" + "@babel/plugin-transform-react-jsx-source" "^7.19.6" + react-refresh "^0.14.0" + +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn@^8.8.0: + version "8.8.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" + integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== + +ajv@^6.10.0, ajv@^6.12.4: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +babel-plugin-macros@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz#9ef6dc74deb934b4db344dc973ee851d148c50c1" + integrity sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg== + dependencies: + "@babel/runtime" "^7.12.5" + cosmiconfig "^7.0.0" + resolve "^1.19.0" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +browserslist@^4.21.3: + version "4.21.7" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.7.tgz#e2b420947e5fb0a58e8f4668ae6e23488127e551" + integrity sha512-BauCXrQ7I2ftSqd2mvKHGo85XR0u7Ru3C/Hxsy/0TkfCtjrmAbPdzLGasmoiBxplpDXlPvdjX9u7srIMfgasNA== + dependencies: + caniuse-lite "^1.0.30001489" + electron-to-chromium "^1.4.411" + node-releases "^2.0.12" + update-browserslist-db "^1.0.11" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +caniuse-lite@^1.0.30001489: + version "1.0.30001497" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001497.tgz#0e5387b98e7dbf9c4f743fb16e92cbf0ca780714" + integrity sha512-I4/duVK4wL6rAK/aKZl3HXB4g+lIZvaT4VLAn2rCgJ38jVLb0lv2Xug6QuqmxXFVRJMF74SPPWPJ/1Sdm3vCzw== + +chalk@^2.0.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +clsx@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" + integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +convert-source-map@^1.5.0, convert-source-map@^1.7.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" + integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== + +cosmiconfig@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6" + integrity sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.2.1" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.10.0" + +cross-spawn@^7.0.2: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +csstype@^3.0.2, csstype@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b" + integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ== + +debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +dom-helpers@^5.0.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" + integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== + dependencies: + "@babel/runtime" "^7.8.7" + csstype "^3.0.2" + +electron-to-chromium@^1.4.411: + version "1.4.427" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.427.tgz#67e8069f7a864fc092fe2e09f196e68af5cb88a1" + integrity sha512-HK3r9l+Jm8dYAm1ctXEWIC+hV60zfcjS9UA5BDlYvnI5S7PU/yytjpvSrTNrSSRRkuu3tDyZhdkwIczh+0DWaw== + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +esbuild@^0.17.5: + version "0.17.19" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.17.19.tgz#087a727e98299f0462a3d0bcdd9cd7ff100bd955" + integrity sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw== + optionalDependencies: + "@esbuild/android-arm" "0.17.19" + "@esbuild/android-arm64" "0.17.19" + "@esbuild/android-x64" "0.17.19" + "@esbuild/darwin-arm64" "0.17.19" + "@esbuild/darwin-x64" "0.17.19" + "@esbuild/freebsd-arm64" "0.17.19" + "@esbuild/freebsd-x64" "0.17.19" + "@esbuild/linux-arm" "0.17.19" + "@esbuild/linux-arm64" "0.17.19" + "@esbuild/linux-ia32" "0.17.19" + "@esbuild/linux-loong64" "0.17.19" + "@esbuild/linux-mips64el" "0.17.19" + "@esbuild/linux-ppc64" "0.17.19" + "@esbuild/linux-riscv64" "0.17.19" + "@esbuild/linux-s390x" "0.17.19" + "@esbuild/linux-x64" "0.17.19" + "@esbuild/netbsd-x64" "0.17.19" + "@esbuild/openbsd-x64" "0.17.19" + "@esbuild/sunos-x64" "0.17.19" + "@esbuild/win32-arm64" "0.17.19" + "@esbuild/win32-ia32" "0.17.19" + "@esbuild/win32-x64" "0.17.19" + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-plugin-react-hooks@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz#4c3e697ad95b77e93f8646aaa1630c1ba607edd3" + integrity sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g== + +eslint-plugin-react-refresh@^0.3.4: + version "0.3.5" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.3.5.tgz#0121e3f05f940250d3544bfaeff52e1c6adf4117" + integrity sha512-61qNIsc7fo9Pp/mju0J83kzvLm0Bsayu7OQSLEoJxLDCBjIIyb87bkzufoOvdDxLkSlMfkF7UxomC4+eztUBSA== + +eslint-scope@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +eslint-scope@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.0.tgz#f21ebdafda02352f103634b96dd47d9f81ca117b" + integrity sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz#c22c48f48942d08ca824cc526211ae400478a994" + integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA== + +eslint@^8.38.0: + version "8.42.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.42.0.tgz#7bebdc3a55f9ed7167251fe7259f75219cade291" + integrity sha512-ulg9Ms6E1WPf67PHaEY4/6E2tEn5/f7FXGzr3t9cBMugOmf1INYvuUwwh1aXQN4MfJ6a5K2iNwP3w4AColvI9A== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.4.0" + "@eslint/eslintrc" "^2.0.3" + "@eslint/js" "8.42.0" + "@humanwhocodes/config-array" "^0.11.10" + "@humanwhocodes/module-importer" "^1.0.1" + "@nodelib/fs.walk" "^1.2.8" + ajv "^6.10.0" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + doctrine "^3.0.0" + escape-string-regexp "^4.0.0" + eslint-scope "^7.2.0" + eslint-visitor-keys "^3.4.1" + espree "^9.5.2" + esquery "^1.4.2" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + find-up "^5.0.0" + glob-parent "^6.0.2" + globals "^13.19.0" + graphemer "^1.4.0" + ignore "^5.2.0" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + is-path-inside "^3.0.3" + js-yaml "^4.1.0" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.1" + strip-ansi "^6.0.1" + strip-json-comments "^3.1.0" + text-table "^0.2.0" + +espree@^9.5.2: + version "9.5.2" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.5.2.tgz#e994e7dc33a082a7a82dceaf12883a829353215b" + integrity sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw== + dependencies: + acorn "^8.8.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.4.1" + +esquery@^1.4.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" + integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.1.0, estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^3.2.9: + version "3.2.12" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80" + integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + +fastq@^1.6.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a" + integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw== + dependencies: + reusify "^1.0.4" + +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + dependencies: + flat-cache "^3.0.4" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +find-root@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" + integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat-cache@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" + integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== + dependencies: + flatted "^3.1.0" + rimraf "^3.0.2" + +flatted@^3.1.0: + version "3.2.7" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" + integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +glob-parent@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob@^7.1.3: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +globals@^13.19.0: + version "13.20.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.20.0.tgz#ea276a1e508ffd4f1612888f9d1bad1e2717bf82" + integrity sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ== + dependencies: + type-fest "^0.20.2" + +globby@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^3.0.0" + +grapheme-splitter@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" + integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== + +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +hoist-non-react-statics@^3.3.1: + version "3.3.2" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + +ignore@^5.2.0: + version "5.2.4" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" + integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== + +import-fresh@^3.0.0, import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + +is-core-module@^2.11.0: + version "2.12.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.12.1.tgz#0c0b6885b6f80011c71541ce15c8d66cf5a4f9fd" + integrity sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg== + dependencies: + has "^1.0.3" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-path-inside@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== + +json5@^2.2.2: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +loose-envify@^1.1.0, loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +merge2@^1.3.0, merge2@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +micromatch@^4.0.4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + dependencies: + braces "^3.0.2" + picomatch "^2.3.1" + +minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +nanoid@^3.3.6: + version "3.3.6" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" + integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== + +natural-compare-lite@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" + integrity sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + +node-releases@^2.0.12: + version "2.0.12" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.12.tgz#35627cc224a23bfb06fb3380f2b3afaaa7eb1039" + integrity sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ== + +object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +optionator@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" + integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.3" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-json@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +postcss@^8.4.23: + version "8.4.24" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.24.tgz#f714dba9b2284be3cc07dbd2fc57ee4dc972d2df" + integrity sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg== + dependencies: + nanoid "^3.3.6" + picocolors "^1.0.0" + source-map-js "^1.0.2" + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +prop-types@^15.6.2, prop-types@^15.8.1: + version "15.8.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + +punycode@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" + integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +react-dom@^18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" + integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== + dependencies: + loose-envify "^1.1.0" + scheduler "^0.23.0" + +react-is@^16.13.1, react-is@^16.7.0: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +react-is@^18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" + integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== + +react-refresh@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.0.tgz#4e02825378a5f227079554d4284889354e5f553e" + integrity sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ== + +react-router-dom@^6.12.1: + version "6.12.1" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.12.1.tgz#601fe7eb493071a33dc7573a20b920324a834606" + integrity sha512-POIZN9UDKWwEDga054LvYr2KnK8V+0HR4Ny4Bwv8V7/FZCPxJgsCjYxXGxqxzHs7VBxMKZfgvtKhafuJkJSPGA== + dependencies: + "@remix-run/router" "1.6.3" + react-router "6.12.1" + +react-router@6.12.1: + version "6.12.1" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.12.1.tgz#9e4126aa1139ec6b5d347e19576d5e940cd46362" + integrity sha512-evd/GrKJOeOypD0JB9e1r7pQh2gWCsTbUfq059Wm1AFT/K2MNZuDo19lFtAgIhlBrp0MmpgpqtvZC7LPAs7vSw== + dependencies: + "@remix-run/router" "1.6.3" + +react-transition-group@^4.4.5: + version "4.4.5" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1" + integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g== + dependencies: + "@babel/runtime" "^7.5.5" + dom-helpers "^5.0.1" + loose-envify "^1.4.0" + prop-types "^15.6.2" + +react@^18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" + integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== + dependencies: + loose-envify "^1.1.0" + +regenerator-runtime@^0.13.11: + version "0.13.11" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" + integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== + +reselect@^4.1.8: + version "4.1.8" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.1.8.tgz#3f5dc671ea168dccdeb3e141236f69f02eaec524" + integrity sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ== + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve@^1.19.0: + version "1.22.2" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.2.tgz#0ed0943d4e301867955766c9f3e1ae6d01c6845f" + integrity sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g== + dependencies: + is-core-module "^2.11.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +rollup@^3.21.0: + version "3.24.0" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.24.0.tgz#865dee1fe0bb528747b59914dfab25e6f480e370" + integrity sha512-OgraHOIg2YpHQTjl0/ymWfFNBEyPucB7lmhXrQUh38qNOegxLapSPFs9sNr0qKR75awW41D93XafoR2QfhBdUQ== + optionalDependencies: + fsevents "~2.3.2" + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +scheduler@^0.23.0: + version "0.23.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" + integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw== + dependencies: + loose-envify "^1.1.0" + +semver@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +semver@^7.3.7: + version "7.5.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.1.tgz#c90c4d631cf74720e46b21c1d37ea07edfab91ec" + integrity sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw== + dependencies: + lru-cache "^6.0.0" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +source-map-js@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== + +source-map@^0.5.7: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== + +strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +stylis@4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.2.0.tgz#79daee0208964c8fe695a42fcffcac633a211a51" + integrity sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw== + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +tslib@^1.8.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tsutils@^3.21.0: + version "3.21.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" + integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== + dependencies: + tslib "^1.8.1" + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + +typescript@^5.0.2: + version "5.1.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.3.tgz#8d84219244a6b40b6fb2b33cc1c062f715b9e826" + integrity sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw== + +update-browserslist-db@^1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940" + integrity sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +vite@^4.3.9: + version "4.3.9" + resolved "https://registry.yarnpkg.com/vite/-/vite-4.3.9.tgz#db896200c0b1aa13b37cdc35c9e99ee2fdd5f96d" + integrity sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg== + dependencies: + esbuild "^0.17.5" + postcss "^8.4.23" + rollup "^3.21.0" + optionalDependencies: + fsevents "~2.3.2" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +word-wrap@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" + integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yaml@^1.10.0: + version "1.10.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==