From 608ce1d9910cd68ce825838ea313e02c598f908e Mon Sep 17 00:00:00 2001 From: Mica White Date: Mon, 8 Dec 2025 20:08:21 -0500 Subject: Stuff --- .gitignore | 4 +- Cargo.lock | 5648 ++++++++++++++++++++--------------------- Cargo.toml | 76 +- rustfmt.toml | 6 +- sqlx-data.json | 2136 ++++++++-------- src/api/clients.rs | 966 +++---- src/api/liveops.rs | 22 +- src/api/mod.rs | 26 +- src/api/oauth.rs | 1852 +++++++------- src/api/ops.rs | 140 +- src/api/users.rs | 544 ++-- src/main.rs | 216 +- src/models/client.rs | 330 +-- src/models/mod.rs | 4 +- src/models/user.rs | 98 +- src/resources/languages.rs | 134 +- src/resources/mod.rs | 8 +- src/resources/scripts.rs | 76 +- src/resources/style.rs | 108 +- src/resources/templates.rs | 202 +- src/scopes/admin.rs | 56 +- src/scopes/mod.rs | 256 +- src/services/authorization.rs | 164 +- src/services/config.rs | 148 +- src/services/crypto.rs | 194 +- src/services/db.rs | 30 +- src/services/db/client.rs | 784 +++--- src/services/db/jwt.rs | 398 +-- src/services/db/user.rs | 472 ++-- src/services/id.rs | 54 +- src/services/jwt.rs | 582 ++--- src/services/mod.rs | 14 +- src/services/secrets.rs | 48 +- static/config/local.toml | 10 +- static/languages/en.ini | 40 +- static/scripts/tsconfig.json | 22 +- static/templates/base.html | 32 +- static/templates/error.html | 14 +- static/templates/login.html | 26 +- 39 files changed, 7970 insertions(+), 7970 deletions(-) diff --git a/.gitignore b/.gitignore index fedaa2b..4893348 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -/target -.env +/target +.env diff --git a/Cargo.lock b/Cargo.lock index 39fd579..3625c4e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,2824 +1,2824 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "actix-codec" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57a7559404a7f3573127aab53c08ce37a6c6a315c374a31070f3c91cd1b4a7fe" -dependencies = [ - "bitflags", - "bytes", - "futures-core", - "futures-sink", - "log", - "memchr", - "pin-project-lite", - "tokio", - "tokio-util", -] - -[[package]] -name = "actix-http" -version = "3.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2079246596c18b4a33e274ae10c0e50613f4d32a4198e09c7b93771013fed74" -dependencies = [ - "actix-codec", - "actix-rt", - "actix-service", - "actix-tls", - "actix-utils", - "ahash 0.8.3", - "base64 0.21.0", - "bitflags", - "brotli", - "bytes", - "bytestring", - "derive_more", - "encoding_rs", - "flate2", - "futures-core", - "h2", - "http", - "httparse", - "httpdate", - "itoa", - "language-tags", - "local-channel", - "mime", - "percent-encoding", - "pin-project-lite", - "rand", - "sha1", - "smallvec", - "tokio", - "tokio-util", - "tracing", - "zstd", -] - -[[package]] -name = "actix-macros" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "465a6172cf69b960917811022d8f29bc0b7fa1398bc4f78b3c466673db1213b6" -dependencies = [ - "quote", - "syn 1.0.109", -] - -[[package]] -name = "actix-router" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66ff4d247d2b160861fa2866457e85706833527840e4133f8f49aa423a38799" -dependencies = [ - "bytestring", - "http", - "regex", - "serde", - "tracing", -] - -[[package]] -name = "actix-rt" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15265b6b8e2347670eb363c47fc8c75208b4a4994b27192f345fcbe707804f3e" -dependencies = [ - "actix-macros", - "futures-core", - "tokio", -] - -[[package]] -name = "actix-server" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e8613a75dd50cc45f473cee3c34d59ed677c0f7b44480ce3b8247d7dc519327" -dependencies = [ - "actix-rt", - "actix-service", - "actix-utils", - "futures-core", - "futures-util", - "mio", - "num_cpus", - "socket2", - "tokio", - "tracing", -] - -[[package]] -name = "actix-service" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" -dependencies = [ - "futures-core", - "paste", - "pin-project-lite", -] - -[[package]] -name = "actix-tls" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fde0cf292f7cdc7f070803cb9a0d45c018441321a78b1042ffbbb81ec333297" -dependencies = [ - "actix-codec", - "actix-rt", - "actix-service", - "actix-utils", - "futures-core", - "log", - "pin-project-lite", - "tokio-rustls", - "tokio-util", - "webpki-roots", -] - -[[package]] -name = "actix-utils" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" -dependencies = [ - "local-waker", - "pin-project-lite", -] - -[[package]] -name = "actix-web" -version = "4.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3cb42f9566ab176e1ef0b8b3a896529062b4efc6be0123046095914c4c1c96" -dependencies = [ - "actix-codec", - "actix-http", - "actix-macros", - "actix-router", - "actix-rt", - "actix-server", - "actix-service", - "actix-tls", - "actix-utils", - "actix-web-codegen", - "ahash 0.7.6", - "bytes", - "bytestring", - "cfg-if", - "cookie", - "derive_more", - "encoding_rs", - "futures-core", - "futures-util", - "http", - "itoa", - "language-tags", - "log", - "mime", - "once_cell", - "pin-project-lite", - "regex", - "serde", - "serde_json", - "serde_urlencoded", - "smallvec", - "socket2", - "time 0.3.20", - "url", -] - -[[package]] -name = "actix-web-codegen" -version = "4.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2262160a7ae29e3415554a3f1fc04c764b1540c116aa524683208078b7a75bc9" -dependencies = [ - "actix-router", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - -[[package]] -name = "ahash" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" -dependencies = [ - "getrandom", - "once_cell", - "version_check", -] - -[[package]] -name = "ahash" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" -dependencies = [ - "cfg-if", - "getrandom", - "once_cell", - "version_check", -] - -[[package]] -name = "aho-corasick" -version = "0.7.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" -dependencies = [ - "memchr", -] - -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" -dependencies = [ - "alloc-no-stdlib", -] - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "ansi_term" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" -dependencies = [ - "winapi", -] - -[[package]] -name = "arrayref" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" - -[[package]] -name = "arrayvec" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" - -[[package]] -name = "atoi" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c57d12312ff59c811c0643f4d80830505833c9ffaebd193d819392b265be8e" -dependencies = [ - "num-traits", -] - -[[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 = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - -[[package]] -name = "base64" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" - -[[package]] -name = "base64ct" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "blake2b_simd" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c2f0dc9a68c6317d884f97cc36cf5a3d20ba14ce404227df55e1af708ab04bc" -dependencies = [ - "arrayref", - "arrayvec", - "constant_time_eq 0.2.5", -] - -[[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 = "bpaf" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bac242287491ba761f8943d48c2b3eca2b30485187a7a13fa6b2168c058f342" -dependencies = [ - "bpaf_derive", -] - -[[package]] -name = "bpaf_derive" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af3c1dc174c8c49192fe1553cb25f75ba410a4b26b2bf5ca620307579e9ca078" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.2", -] - -[[package]] -name = "brotli" -version = "3.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "2.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b6561fd3f895a11e8f72af2cb7d22e08366bebc2b6b57f7744c4bda27034744" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - -[[package]] -name = "bstr" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d4260bcc2e8fc9df1eac4919a720effeb63a3f0952f5bf4944adfa18897f09" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "bumpalo" -version = "3.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" - -[[package]] -name = "byteorder" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" - -[[package]] -name = "bytes" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" - -[[package]] -name = "bytestring" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "238e4886760d98c4f899360c834fa93e62cf7f721ac3c2da375cbdf4b8679aae" -dependencies = [ - "bytes", -] - -[[package]] -name = "cc" -version = "1.0.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" -dependencies = [ - "jobserver", -] - -[[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.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-integer", - "num-traits", - "serde", - "time 0.1.45", - "wasm-bindgen", - "winapi", -] - -[[package]] -name = "chrono-tz" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58549f1842da3080ce63002102d5bc954c7bc843d4f47818e642abdc36253552" -dependencies = [ - "chrono", - "chrono-tz-build", - "phf", -] - -[[package]] -name = "chrono-tz-build" -version = "0.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db058d493fb2f65f41861bfed7e3fe6335264a9f0f92710cab5bdf01fef09069" -dependencies = [ - "parse-zoneinfo", - "phf", - "phf_codegen", -] - -[[package]] -name = "clap" -version = "2.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" -dependencies = [ - "ansi_term", - "atty", - "bitflags", - "strsim", - "textwrap", - "unicode-width", - "vec_map", -] - -[[package]] -name = "codemap" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e769b5c8c8283982a987c6e948e540254f1058d5a74b8794914d4ef5fc2a24" - -[[package]] -name = "const-oid" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3" - -[[package]] -name = "constant_time_eq" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" - -[[package]] -name = "constant_time_eq" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" - -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - -[[package]] -name = "cookie" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" -dependencies = [ - "percent-encoding", - "time 0.3.20", - "version_check", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" - -[[package]] -name = "cpufeatures" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" -dependencies = [ - "libc", -] - -[[package]] -name = "crc" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe" -dependencies = [ - "crc-catalog", -] - -[[package]] -name = "crc-catalog" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484" - -[[package]] -name = "crc32fast" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-queue" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" -dependencies = [ - "cfg-if", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crypto-bigint" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c6a1d5fa1de37e071642dfa44ec552ca5b299adb128fab16138e24b548fd21" -dependencies = [ - "generic-array", - "subtle", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "der" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6919815d73839e7ad218de758883aae3a257ba6759ce7a9992501efbb53d705c" -dependencies = [ - "const-oid", - "crypto-bigint", - "pem-rfc7468", -] - -[[package]] -name = "derive_more" -version = "0.99.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn 1.0.109", -] - -[[package]] -name = "deunicode" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "850878694b7933ca4c9569d30a34b55031b9b139ee1fc7b94a527c4ef960d690" - -[[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 = "displaydoc" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.2", -] - -[[package]] -name = "dlv-list" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" - -[[package]] -name = "dotenv" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" - -[[package]] -name = "dotenvy" -version = "0.15.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03d8c417d7a8cb362e0c37e5d815f5eb7c37f79ff93707329d5a194e42e54ca0" - -[[package]] -name = "either" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" -dependencies = [ - "serde", -] - -[[package]] -name = "encoding_rs" -version = "0.8.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "equivalent" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1" - -[[package]] -name = "event-listener" -version = "2.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" - -[[package]] -name = "exun" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "466e7214c6417026835a8cc2bc81b1cfee500ddd37cd5df8b73a59d92a4c90a5" - -[[package]] -name = "flate2" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" -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 = "form_urlencoded" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "futures-channel" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "164713a5a0dcc3e7b4b1ed7d3b433cabc18025386f9339346e8daf15963cf7ac" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86d7a0c1aa76363dac491de0ee99faf6941128376f1cf96f07db7603b7de69dd" - -[[package]] -name = "futures-intrusive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a604f7a68fbf8103337523b1fadc8ade7361ee3f112f7c680ad179651616aed5" -dependencies = [ - "futures-core", - "lock_api", - "parking_lot 0.11.2", -] - -[[package]] -name = "futures-sink" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec93083a4aecafb2a80a885c9de1f0ccae9dbd32c2bb54b0c3a65690e0b8d2f2" - -[[package]] -name = "futures-task" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd65540d33b37b16542a0438c12e6aeead10d4ac5d05bd3f805b8f35ab592879" - -[[package]] -name = "futures-util" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ef6b17e481503ec85211fed8f39d1970f128935ca1f814cd32ac4a6842e84ab" -dependencies = [ - "futures-core", - "futures-sink", - "futures-task", - "pin-project-lite", - "pin-utils", -] - -[[package]] -name = "generic-array" -version = "0.14.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.11.0+wasi-snapshot-preview1", -] - -[[package]] -name = "globset" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc" -dependencies = [ - "aho-corasick", - "bstr", - "fnv", - "log", - "regex", -] - -[[package]] -name = "globwalk" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" -dependencies = [ - "bitflags", - "ignore", - "walkdir", -] - -[[package]] -name = "grass" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85cc4b64880a2264a41f9eab431780e72a68a6c88b9bddef361ba638812d572e" -dependencies = [ - "clap", - "grass_compiler", -] - -[[package]] -name = "grass_compiler" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e4feeef87d958eebd4d55431040768b93a5b088202198e0b203adc3c1d468c6" -dependencies = [ - "codemap", - "indexmap 1.9.2", - "lasso", - "once_cell", - "phf", - "rand", -] - -[[package]] -name = "h2" -version = "0.3.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be7b54589b581f624f566bf5d8eb2bab1db736c51528720b6bd36b96b55924d" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http", - "indexmap 1.9.2", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" -dependencies = [ - "ahash 0.7.6", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -dependencies = [ - "ahash 0.7.6", -] - -[[package]] -name = "hashbrown" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" - -[[package]] -name = "hashlink" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa" -dependencies = [ - "hashbrown 0.12.3", -] - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "hermit-abi" -version = "0.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 = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] -name = "http" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "httparse" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" - -[[package]] -name = "httpdate" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" - -[[package]] -name = "humansize" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" -dependencies = [ - "libm", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c" -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 = "ignore" -version = "0.4.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbe7873dab538a9a44ad79ede1faf5f30d49f9a5c883ddbab48bce81b64b7492" -dependencies = [ - "globset", - "lazy_static", - "log", - "memchr", - "regex", - "same-file", - "thread_local", - "walkdir", - "winapi-util", -] - -[[package]] -name = "indexmap" -version = "1.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", -] - -[[package]] -name = "indexmap" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" -dependencies = [ - "equivalent", - "hashbrown 0.14.0", -] - -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" - -[[package]] -name = "jobserver" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" -dependencies = [ - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "jwt" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6204285f77fe7d9784db3fdc449ecce1a0114927a51d5a41c4c7a292011c015f" -dependencies = [ - "base64 0.13.1", - "crypto-common", - "digest", - "hmac", - "serde", - "serde_json", - "sha2", -] - -[[package]] -name = "language-tags" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" - -[[package]] -name = "lasso" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aeb7b21a526375c5ca55f1a6dfd4e1fad9fa4edd750f530252a718a44b2608f0" -dependencies = [ - "hashbrown 0.11.2", -] - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" -dependencies = [ - "spin", -] - -[[package]] -name = "libc" -version = "0.2.140" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" - -[[package]] -name = "libm" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb" - -[[package]] -name = "local-channel" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f303ec0e94c6c54447f84f3b0ef7af769858a9c4ef56ef2a986d3dcd4c3fc9c" -dependencies = [ - "futures-core", - "futures-sink", - "futures-util", - "local-waker", -] - -[[package]] -name = "local-waker" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e34f76eb3611940e0e7d53a9aaa4e6a3151f69541a282fd0dad5571420c53ff1" - -[[package]] -name = "lock_api" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "memchr" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" - -[[package]] -name = "mime" -version = "0.3.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" - -[[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.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" -dependencies = [ - "adler", -] - -[[package]] -name = "mio" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" -dependencies = [ - "libc", - "log", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys", -] - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "num-bigint" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-bigint-dig" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2399c9463abc5f909349d8aa9ba080e0b88b3ce2885389b60b993f39b1a56905" -dependencies = [ - "byteorder", - "lazy_static", - "libm", - "num-integer", - "num-iter", - "num-traits", - "rand", - "smallvec", - "zeroize", -] - -[[package]] -name = "num-integer" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" -dependencies = [ - "autocfg", - "num-traits", -] - -[[package]] -name = "num-iter" -version = "0.1.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" -dependencies = [ - "autocfg", - "libm", -] - -[[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 = "once_cell" -version = "1.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" - -[[package]] -name = "ordered-multimap" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" -dependencies = [ - "dlv-list", - "hashbrown 0.12.3", -] - -[[package]] -name = "parking_lot" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" -dependencies = [ - "instant", - "lock_api", - "parking_lot_core 0.8.6", -] - -[[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 0.9.7", -] - -[[package]] -name = "parking_lot_core" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" -dependencies = [ - "cfg-if", - "instant", - "libc", - "redox_syscall", - "smallvec", - "winapi", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-sys", -] - -[[package]] -name = "parse-zoneinfo" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41" -dependencies = [ - "regex", -] - -[[package]] -name = "paste" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" - -[[package]] -name = "path-clean" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17359afc20d7ab31fdb42bb844c8b3bb1dabd7dcf7e68428492da7f16966fcef" - -[[package]] -name = "pem-rfc7468" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01de5d978f34aa4b2296576379fcc416034702fd94117c56ffd8a1a767cefb30" -dependencies = [ - "base64ct", -] - -[[package]] -name = "percent-encoding" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" - -[[package]] -name = "pest" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e68e84bfb01f0507134eac1e9b410a12ba379d064eab48c50ba4ce329a527b70" -dependencies = [ - "thiserror", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b79d4c71c865a25a4322296122e3924d30bc8ee0834c8bfc8b95f7f054afbfb" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c435bf1076437b851ebc8edc3a18442796b30f1728ffea6262d59bbe28b077e" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn 2.0.2", -] - -[[package]] -name = "pest_meta" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "745a452f8eb71e39ffd8ee32b3c5f51d03845f99786fa9b68db6ff509c505411" -dependencies = [ - "once_cell", - "pest", - "sha2", -] - -[[package]] -name = "phf" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" -dependencies = [ - "phf_macros", - "phf_shared", - "proc-macro-hack", -] - -[[package]] -name = "phf_codegen" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" -dependencies = [ - "phf_generator", - "phf_shared", -] - -[[package]] -name = "phf_generator" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" -dependencies = [ - "phf_shared", - "rand", -] - -[[package]] -name = "phf_macros" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro-hack", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "phf_shared" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" -dependencies = [ - "siphasher", - "uncased", -] - -[[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 = "pkcs1" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a78f66c04ccc83dd4486fd46c33896f4e17b24a7a3a6400dedc48ed0ddd72320" -dependencies = [ - "der", - "pkcs8", - "zeroize", -] - -[[package]] -name = "pkcs8" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cabda3fb821068a9a4fab19a683eac3af12edf0f34b94a8be53c4972b8149d0" -dependencies = [ - "der", - "spki", - "zeroize", -] - -[[package]] -name = "pkg-config" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" - -[[package]] -name = "ppv-lite86" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" - -[[package]] -name = "proc-macro-hack" -version = "0.5.20+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" - -[[package]] -name = "proc-macro2" -version = "1.0.52" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d0e1ae9e836cc3beddd63db0df682593d7e2d3d891ae8c9083d2113e1744224" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "raise" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5127de0ecc0dd007559117b06737ec010d7316513685c2d3adf2b8b8252ce589" - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] - -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags", -] - -[[package]] -name = "regex" -version = "1.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.6.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" - -[[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 = "rsa" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf22754c49613d2b3b119f0e5d46e34a2c628a937e3024b8762de4e7d8c710b" -dependencies = [ - "byteorder", - "digest", - "num-bigint-dig", - "num-integer", - "num-iter", - "num-traits", - "pkcs1", - "pkcs8", - "rand_core", - "smallvec", - "subtle", - "zeroize", -] - -[[package]] -name = "rust-argon2" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b50162d19404029c1ceca6f6980fe40d45c8b369f6f44446fa14bb39573b5bb9" -dependencies = [ - "base64 0.13.1", - "blake2b_simd", - "constant_time_eq 0.1.5", - "crossbeam-utils", -] - -[[package]] -name = "rust-ini" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" -dependencies = [ - "cfg-if", - "ordered-multimap", -] - -[[package]] -name = "rust-pw-server" -version = "0.1.0" -dependencies = [ - "actix-rt", - "actix-web", - "base64 0.21.0", - "bpaf", - "chrono", - "dotenv", - "exun", - "grass", - "hex", - "hmac", - "jwt", - "log", - "parking_lot 0.12.1", - "path-clean", - "raise", - "rand", - "rust-argon2", - "rust-ini", - "serde", - "serde_json", - "serde_urlencoded", - "serde_variant", - "sha2", - "sqlx", - "tera", - "thiserror", - "toml", - "unic-langid", - "url", - "uuid", -] - -[[package]] -name = "rustc_version" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" -dependencies = [ - "semver", -] - -[[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-pemfile" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" -dependencies = [ - "base64 0.21.0", -] - -[[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 = "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 = "semver" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" - -[[package]] -name = "serde" -version = "1.0.156" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "314b5b092c0ade17c00142951e50ced110ec27cea304b1037c6969246c2469a4" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.156" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7e29c4601e36bcec74a223228dce795f4cd3616341a4af93520ca1a837c087d" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "serde_json" -version = "1.0.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c533a59c9d8a93a09c6ab31f0fd5e5f4dd1b8fc9434804029839884765d04ea" -dependencies = [ - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_spanned" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" -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_variant" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47a8ec0b2fd0506290348d9699c0e3eb2e3e8c0498b5a9a6158b3bd4d6970076" -dependencies = [ - "serde", -] - -[[package]] -name = "sha1" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha2" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "signal-hook-registry" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" -dependencies = [ - "libc", -] - -[[package]] -name = "siphasher" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" - -[[package]] -name = "slab" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" -dependencies = [ - "autocfg", -] - -[[package]] -name = "slug" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3bc762e6a4b6c6fcaade73e77f9ebc6991b676f88bb2358bddb56560f073373" -dependencies = [ - "deunicode", -] - -[[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 = "spki" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d01ac02a6ccf3e07db148d2be087da624fea0221a16152ed01f0496a6b0a27" -dependencies = [ - "base64ct", - "der", -] - -[[package]] -name = "sqlformat" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c12bc9199d1db8234678b7051747c07f517cdcf019262d1847b94ec8b1aee3e" -dependencies = [ - "itertools", - "nom", - "unicode_categories", -] - -[[package]] -name = "sqlx" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9249290c05928352f71c077cc44a464d880c63f26f7534728cca008e135c0428" -dependencies = [ - "sqlx-core", - "sqlx-macros", -] - -[[package]] -name = "sqlx-core" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcbc16ddba161afc99e14d1713a453747a2b07fc097d2009f4c300ec99286105" -dependencies = [ - "ahash 0.7.6", - "atoi", - "bitflags", - "byteorder", - "bytes", - "chrono", - "crc", - "crossbeam-queue", - "digest", - "dotenvy", - "either", - "event-listener", - "futures-channel", - "futures-core", - "futures-intrusive", - "futures-util", - "generic-array", - "hashlink", - "hex", - "indexmap 1.9.2", - "itoa", - "libc", - "log", - "memchr", - "num-bigint", - "once_cell", - "paste", - "percent-encoding", - "rand", - "rsa", - "rustls", - "rustls-pemfile", - "serde", - "sha1", - "sha2", - "smallvec", - "sqlformat", - "sqlx-rt", - "stringprep", - "thiserror", - "tokio-stream", - "url", - "uuid", - "webpki-roots", -] - -[[package]] -name = "sqlx-macros" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b850fa514dc11f2ee85be9d055c512aa866746adfacd1cb42d867d68e6a5b0d9" -dependencies = [ - "dotenvy", - "either", - "heck", - "hex", - "once_cell", - "proc-macro2", - "quote", - "serde", - "serde_json", - "sha2", - "sqlx-core", - "sqlx-rt", - "syn 1.0.109", - "url", -] - -[[package]] -name = "sqlx-rt" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24c5b2d25fa654cc5f841750b8e1cdedbe21189bf9a9382ee90bfa9dd3562396" -dependencies = [ - "once_cell", - "tokio", - "tokio-rustls", -] - -[[package]] -name = "stringprep" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ee348cb74b87454fff4b551cbf727025810a004f88aeacae7f85b87f4e9a1c1" -dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "strsim" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" - -[[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.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59d3276aee1fa0c33612917969b5172b5be2db051232a6e4826f1a1a9191b045" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "tera" -version = "1.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a665751302f22a03c56721e23094e4dc22b04a80f381e6737a07bf7a7c70c0" -dependencies = [ - "chrono", - "chrono-tz", - "globwalk", - "humansize", - "lazy_static", - "percent-encoding", - "pest", - "pest_derive", - "rand", - "regex", - "serde", - "serde_json", - "slug", - "thread_local", - "unic-segment", -] - -[[package]] -name = "textwrap" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" -dependencies = [ - "unicode-width", -] - -[[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.2", -] - -[[package]] -name = "thread_local" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" -dependencies = [ - "once_cell", -] - -[[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.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" -dependencies = [ - "itoa", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" - -[[package]] -name = "time-macros" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36" -dependencies = [ - "time-core", -] - -[[package]] -name = "tinystr" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ac3f5b6856e931e15e07b478e98c8045239829a65f9156d4fa7e7788197a5ef" -dependencies = [ - "displaydoc", -] - -[[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.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03201d01c3c27a29c8a5cee5b55a93ddae1ccf6f08f65365c2c918f8c1b76f64" -dependencies = [ - "autocfg", - "bytes", - "libc", - "memchr", - "mio", - "num_cpus", - "parking_lot 0.12.1", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "windows-sys", -] - -[[package]] -name = "tokio-rustls" -version = "0.23.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" -dependencies = [ - "rustls", - "tokio", - "webpki", -] - -[[package]] -name = "tokio-stream" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fb52b74f05dbf495a8fba459fdc331812b96aa086d9eb78101fa0d4569c3313" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tokio-util" -version = "0.7.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", - "tracing", -] - -[[package]] -name = "toml" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebafdf5ad1220cb59e7d17cf4d2c72015297b75b19a10472f99b89225089240" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.19.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266f016b7f039eec8a1a80dfe6156b633d208b9fccca5e4db1d6775b0c4e34a7" -dependencies = [ - "indexmap 2.0.0", - "serde", - "serde_spanned", - "toml_datetime", - "winnow", -] - -[[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.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" -dependencies = [ - "once_cell", -] - -[[package]] -name = "typenum" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" - -[[package]] -name = "ucd-trie" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" - -[[package]] -name = "uncased" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b9bc53168a4be7402ab86c3aad243a84dd7381d09be0eddc81280c1da95ca68" -dependencies = [ - "version_check", -] - -[[package]] -name = "unic-char-property" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" -dependencies = [ - "unic-char-range", -] - -[[package]] -name = "unic-char-range" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" - -[[package]] -name = "unic-common" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" - -[[package]] -name = "unic-langid" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "398f9ad7239db44fd0f80fe068d12ff22d78354080332a5077dc6f52f14dcf2f" -dependencies = [ - "unic-langid-impl", -] - -[[package]] -name = "unic-langid-impl" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e35bfd2f2b8796545b55d7d3fd3e89a0613f68a0d1c8bc28cb7ff96b411a35ff" -dependencies = [ - "serde", - "tinystr", -] - -[[package]] -name = "unic-segment" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23" -dependencies = [ - "unic-ucd-segment", -] - -[[package]] -name = "unic-ucd-segment" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700" -dependencies = [ - "unic-char-property", - "unic-char-range", - "unic-ucd-version", -] - -[[package]] -name = "unic-ucd-version" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" -dependencies = [ - "unic-common", -] - -[[package]] -name = "unicode-bidi" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524b68aca1d05e03fdf03fcdce2c6c94b6daf6d16861ddaa7e4f2b6638a9052c" - -[[package]] -name = "unicode-ident" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" - -[[package]] -name = "unicode-normalization" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "unicode-segmentation" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" - -[[package]] -name = "unicode-width" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" - -[[package]] -name = "unicode_categories" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" - -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" - -[[package]] -name = "url" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "uuid" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79" -dependencies = [ - "getrandom", - "rand", - "serde", -] - -[[package]] -name = "vec_map" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" - -[[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 = "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.84" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" -dependencies = [ - "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.84" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn 1.0.109", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.84" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.84" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.84" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" - -[[package]] -name = "web-sys" -version = "0.3.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webpki" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "webpki-roots" -version = "0.22.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" -dependencies = [ - "webpki", -] - -[[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 0.48.0", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -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-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.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca0ace3845f0d96209f0375e6d367e3eb87eb65d27d445bdc9f1843a26f39448" -dependencies = [ - "memchr", -] - -[[package]] -name = "zeroize" -version = "1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" - -[[package]] -name = "zstd" -version = "0.12.3+zstd.1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76eea132fb024e0e13fd9c2f5d5d595d8a967aa72382ac2f9d39fcc95afd0806" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "6.0.4+zstd.1.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7afb4b54b8910cf5447638cb54bf4e8a65cbedd783af98b98c62ffe91f185543" -dependencies = [ - "libc", - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.7+zstd.1.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94509c3ba2fe55294d752b79842c530ccfab760192521df74a081a78d2b3c7f5" -dependencies = [ - "cc", - "libc", - "pkg-config", -] +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "actix-codec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a7559404a7f3573127aab53c08ce37a6c6a315c374a31070f3c91cd1b4a7fe" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-sink", + "log", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "actix-http" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2079246596c18b4a33e274ae10c0e50613f4d32a4198e09c7b93771013fed74" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-tls", + "actix-utils", + "ahash 0.8.3", + "base64 0.21.0", + "bitflags", + "brotli", + "bytes", + "bytestring", + "derive_more", + "encoding_rs", + "flate2", + "futures-core", + "h2", + "http", + "httparse", + "httpdate", + "itoa", + "language-tags", + "local-channel", + "mime", + "percent-encoding", + "pin-project-lite", + "rand", + "sha1", + "smallvec", + "tokio", + "tokio-util", + "tracing", + "zstd", +] + +[[package]] +name = "actix-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465a6172cf69b960917811022d8f29bc0b7fa1398bc4f78b3c466673db1213b6" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "actix-router" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66ff4d247d2b160861fa2866457e85706833527840e4133f8f49aa423a38799" +dependencies = [ + "bytestring", + "http", + "regex", + "serde", + "tracing", +] + +[[package]] +name = "actix-rt" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15265b6b8e2347670eb363c47fc8c75208b4a4994b27192f345fcbe707804f3e" +dependencies = [ + "actix-macros", + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e8613a75dd50cc45f473cee3c34d59ed677c0f7b44480ce3b8247d7dc519327" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio", + "num_cpus", + "socket2", + "tokio", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" +dependencies = [ + "futures-core", + "paste", + "pin-project-lite", +] + +[[package]] +name = "actix-tls" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fde0cf292f7cdc7f070803cb9a0d45c018441321a78b1042ffbbb81ec333297" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "log", + "pin-project-lite", + "tokio-rustls", + "tokio-util", + "webpki-roots", +] + +[[package]] +name = "actix-utils" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd3cb42f9566ab176e1ef0b8b3a896529062b4efc6be0123046095914c4c1c96" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-tls", + "actix-utils", + "actix-web-codegen", + "ahash 0.7.6", + "bytes", + "bytestring", + "cfg-if", + "cookie", + "derive_more", + "encoding_rs", + "futures-core", + "futures-util", + "http", + "itoa", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2", + "time 0.3.20", + "url", +] + +[[package]] +name = "actix-web-codegen" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2262160a7ae29e3415554a3f1fc04c764b1540c116aa524683208078b7a75bc9" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + +[[package]] +name = "atoi" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c57d12312ff59c811c0643f4d80830505833c9ffaebd193d819392b265be8e" +dependencies = [ + "num-traits", +] + +[[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 = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "blake2b_simd" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c2f0dc9a68c6317d884f97cc36cf5a3d20ba14ce404227df55e1af708ab04bc" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq 0.2.5", +] + +[[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 = "bpaf" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bac242287491ba761f8943d48c2b3eca2b30485187a7a13fa6b2168c058f342" +dependencies = [ + "bpaf_derive", +] + +[[package]] +name = "bpaf_derive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af3c1dc174c8c49192fe1553cb25f75ba410a4b26b2bf5ca620307579e9ca078" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.2", +] + +[[package]] +name = "brotli" +version = "3.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b6561fd3f895a11e8f72af2cb7d22e08366bebc2b6b57f7744c4bda27034744" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bstr" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d4260bcc2e8fc9df1eac4919a720effeb63a3f0952f5bf4944adfa18897f09" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" + +[[package]] +name = "bytestring" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "238e4886760d98c4f899360c834fa93e62cf7f721ac3c2da375cbdf4b8679aae" +dependencies = [ + "bytes", +] + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +dependencies = [ + "jobserver", +] + +[[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.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-integer", + "num-traits", + "serde", + "time 0.1.45", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "chrono-tz" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58549f1842da3080ce63002102d5bc954c7bc843d4f47818e642abdc36253552" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db058d493fb2f65f41861bfed7e3fe6335264a9f0f92710cab5bdf01fef09069" +dependencies = [ + "parse-zoneinfo", + "phf", + "phf_codegen", +] + +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "codemap" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e769b5c8c8283982a987c6e948e540254f1058d5a74b8794914d4ef5fc2a24" + +[[package]] +name = "const-oid" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3" + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "constant_time_eq" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "percent-encoding", + "time 0.3.20", + "version_check", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "cpufeatures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484" + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-bigint" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c6a1d5fa1de37e071642dfa44ec552ca5b299adb128fab16138e24b548fd21" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "der" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6919815d73839e7ad218de758883aae3a257ba6759ce7a9992501efbb53d705c" +dependencies = [ + "const-oid", + "crypto-bigint", + "pem-rfc7468", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 1.0.109", +] + +[[package]] +name = "deunicode" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "850878694b7933ca4c9569d30a34b55031b9b139ee1fc7b94a527c4ef960d690" + +[[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 = "displaydoc" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.2", +] + +[[package]] +name = "dlv-list" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" + +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + +[[package]] +name = "dotenvy" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d8c417d7a8cb362e0c37e5d815f5eb7c37f79ff93707329d5a194e42e54ca0" + +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" +dependencies = [ + "serde", +] + +[[package]] +name = "encoding_rs" +version = "0.8.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1" + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "exun" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "466e7214c6417026835a8cc2bc81b1cfee500ddd37cd5df8b73a59d92a4c90a5" + +[[package]] +name = "flate2" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" +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 = "form_urlencoded" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "164713a5a0dcc3e7b4b1ed7d3b433cabc18025386f9339346e8daf15963cf7ac" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86d7a0c1aa76363dac491de0ee99faf6941128376f1cf96f07db7603b7de69dd" + +[[package]] +name = "futures-intrusive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a604f7a68fbf8103337523b1fadc8ade7361ee3f112f7c680ad179651616aed5" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot 0.11.2", +] + +[[package]] +name = "futures-sink" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec93083a4aecafb2a80a885c9de1f0ccae9dbd32c2bb54b0c3a65690e0b8d2f2" + +[[package]] +name = "futures-task" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd65540d33b37b16542a0438c12e6aeead10d4ac5d05bd3f805b8f35ab592879" + +[[package]] +name = "futures-util" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ef6b17e481503ec85211fed8f39d1970f128935ca1f814cd32ac4a6842e84ab" +dependencies = [ + "futures-core", + "futures-sink", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "generic-array" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "globset" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc" +dependencies = [ + "aho-corasick", + "bstr", + "fnv", + "log", + "regex", +] + +[[package]] +name = "globwalk" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" +dependencies = [ + "bitflags", + "ignore", + "walkdir", +] + +[[package]] +name = "grass" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cc4b64880a2264a41f9eab431780e72a68a6c88b9bddef361ba638812d572e" +dependencies = [ + "clap", + "grass_compiler", +] + +[[package]] +name = "grass_compiler" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e4feeef87d958eebd4d55431040768b93a5b088202198e0b203adc3c1d468c6" +dependencies = [ + "codemap", + "indexmap 1.9.2", + "lasso", + "once_cell", + "phf", + "rand", +] + +[[package]] +name = "h2" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be7b54589b581f624f566bf5d8eb2bab1db736c51528720b6bd36b96b55924d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 1.9.2", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash 0.7.6", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.6", +] + +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" + +[[package]] +name = "hashlink" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa" +dependencies = [ + "hashbrown 0.12.3", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.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 = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "http" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c" +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 = "ignore" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbe7873dab538a9a44ad79ede1faf5f30d49f9a5c883ddbab48bce81b64b7492" +dependencies = [ + "globset", + "lazy_static", + "log", + "memchr", + "regex", + "same-file", + "thread_local", + "walkdir", + "winapi-util", +] + +[[package]] +name = "indexmap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown 0.14.0", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" + +[[package]] +name = "jobserver" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "jwt" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6204285f77fe7d9784db3fdc449ecce1a0114927a51d5a41c4c7a292011c015f" +dependencies = [ + "base64 0.13.1", + "crypto-common", + "digest", + "hmac", + "serde", + "serde_json", + "sha2", +] + +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + +[[package]] +name = "lasso" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeb7b21a526375c5ca55f1a6dfd4e1fad9fa4edd750f530252a718a44b2608f0" +dependencies = [ + "hashbrown 0.11.2", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin", +] + +[[package]] +name = "libc" +version = "0.2.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" + +[[package]] +name = "libm" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb" + +[[package]] +name = "local-channel" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f303ec0e94c6c54447f84f3b0ef7af769858a9c4ef56ef2a986d3dcd4c3fc9c" +dependencies = [ + "futures-core", + "futures-sink", + "futures-util", + "local-waker", +] + +[[package]] +name = "local-waker" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34f76eb3611940e0e7d53a9aaa4e6a3151f69541a282fd0dad5571420c53ff1" + +[[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[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.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2399c9463abc5f909349d8aa9ba080e0b88b3ce2885389b60b993f39b1a56905" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", + "libm", +] + +[[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 = "once_cell" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" + +[[package]] +name = "ordered-multimap" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" +dependencies = [ + "dlv-list", + "hashbrown 0.12.3", +] + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + +[[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 0.9.7", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + +[[package]] +name = "parse-zoneinfo" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41" +dependencies = [ + "regex", +] + +[[package]] +name = "paste" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" + +[[package]] +name = "path-clean" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17359afc20d7ab31fdb42bb844c8b3bb1dabd7dcf7e68428492da7f16966fcef" + +[[package]] +name = "pem-rfc7468" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01de5d978f34aa4b2296576379fcc416034702fd94117c56ffd8a1a767cefb30" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + +[[package]] +name = "pest" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e68e84bfb01f0507134eac1e9b410a12ba379d064eab48c50ba4ce329a527b70" +dependencies = [ + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b79d4c71c865a25a4322296122e3924d30bc8ee0834c8bfc8b95f7f054afbfb" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c435bf1076437b851ebc8edc3a18442796b30f1728ffea6262d59bbe28b077e" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.2", +] + +[[package]] +name = "pest_meta" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "745a452f8eb71e39ffd8ee32b3c5f51d03845f99786fa9b68db6ff509c505411" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_macros", + "phf_shared", + "proc-macro-hack", +] + +[[package]] +name = "phf_codegen" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", + "uncased", +] + +[[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 = "pkcs1" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a78f66c04ccc83dd4486fd46c33896f4e17b24a7a3a6400dedc48ed0ddd72320" +dependencies = [ + "der", + "pkcs8", + "zeroize", +] + +[[package]] +name = "pkcs8" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cabda3fb821068a9a4fab19a683eac3af12edf0f34b94a8be53c4972b8149d0" +dependencies = [ + "der", + "spki", + "zeroize", +] + +[[package]] +name = "pkg-config" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro2" +version = "1.0.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d0e1ae9e836cc3beddd63db0df682593d7e2d3d891ae8c9083d2113e1744224" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "raise" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5127de0ecc0dd007559117b06737ec010d7316513685c2d3adf2b8b8252ce589" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" + +[[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 = "rsa" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf22754c49613d2b3b119f0e5d46e34a2c628a937e3024b8762de4e7d8c710b" +dependencies = [ + "byteorder", + "digest", + "num-bigint-dig", + "num-integer", + "num-iter", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "smallvec", + "subtle", + "zeroize", +] + +[[package]] +name = "rust-argon2" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50162d19404029c1ceca6f6980fe40d45c8b369f6f44446fa14bb39573b5bb9" +dependencies = [ + "base64 0.13.1", + "blake2b_simd", + "constant_time_eq 0.1.5", + "crossbeam-utils", +] + +[[package]] +name = "rust-ini" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + +[[package]] +name = "rust-pw-server" +version = "0.1.0" +dependencies = [ + "actix-rt", + "actix-web", + "base64 0.21.0", + "bpaf", + "chrono", + "dotenv", + "exun", + "grass", + "hex", + "hmac", + "jwt", + "log", + "parking_lot 0.12.1", + "path-clean", + "raise", + "rand", + "rust-argon2", + "rust-ini", + "serde", + "serde_json", + "serde_urlencoded", + "serde_variant", + "sha2", + "sqlx", + "tera", + "thiserror", + "toml", + "unic-langid", + "url", + "uuid", +] + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[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-pemfile" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" +dependencies = [ + "base64 0.21.0", +] + +[[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 = "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 = "semver" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" + +[[package]] +name = "serde" +version = "1.0.156" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "314b5b092c0ade17c00142951e50ced110ec27cea304b1037c6969246c2469a4" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.156" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7e29c4601e36bcec74a223228dce795f4cd3616341a4af93520ca1a837c087d" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "serde_json" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c533a59c9d8a93a09c6ab31f0fd5e5f4dd1b8fc9434804029839884765d04ea" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" +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_variant" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47a8ec0b2fd0506290348d9699c0e3eb2e3e8c0498b5a9a6158b3bd4d6970076" +dependencies = [ + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "siphasher" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" + +[[package]] +name = "slab" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +dependencies = [ + "autocfg", +] + +[[package]] +name = "slug" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3bc762e6a4b6c6fcaade73e77f9ebc6991b676f88bb2358bddb56560f073373" +dependencies = [ + "deunicode", +] + +[[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 = "spki" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d01ac02a6ccf3e07db148d2be087da624fea0221a16152ed01f0496a6b0a27" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlformat" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c12bc9199d1db8234678b7051747c07f517cdcf019262d1847b94ec8b1aee3e" +dependencies = [ + "itertools", + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9249290c05928352f71c077cc44a464d880c63f26f7534728cca008e135c0428" +dependencies = [ + "sqlx-core", + "sqlx-macros", +] + +[[package]] +name = "sqlx-core" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbc16ddba161afc99e14d1713a453747a2b07fc097d2009f4c300ec99286105" +dependencies = [ + "ahash 0.7.6", + "atoi", + "bitflags", + "byteorder", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "digest", + "dotenvy", + "either", + "event-listener", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-util", + "generic-array", + "hashlink", + "hex", + "indexmap 1.9.2", + "itoa", + "libc", + "log", + "memchr", + "num-bigint", + "once_cell", + "paste", + "percent-encoding", + "rand", + "rsa", + "rustls", + "rustls-pemfile", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlformat", + "sqlx-rt", + "stringprep", + "thiserror", + "tokio-stream", + "url", + "uuid", + "webpki-roots", +] + +[[package]] +name = "sqlx-macros" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b850fa514dc11f2ee85be9d055c512aa866746adfacd1cb42d867d68e6a5b0d9" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-rt", + "syn 1.0.109", + "url", +] + +[[package]] +name = "sqlx-rt" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24c5b2d25fa654cc5f841750b8e1cdedbe21189bf9a9382ee90bfa9dd3562396" +dependencies = [ + "once_cell", + "tokio", + "tokio-rustls", +] + +[[package]] +name = "stringprep" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee348cb74b87454fff4b551cbf727025810a004f88aeacae7f85b87f4e9a1c1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59d3276aee1fa0c33612917969b5172b5be2db051232a6e4826f1a1a9191b045" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tera" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a665751302f22a03c56721e23094e4dc22b04a80f381e6737a07bf7a7c70c0" +dependencies = [ + "chrono", + "chrono-tz", + "globwalk", + "humansize", + "lazy_static", + "percent-encoding", + "pest", + "pest_derive", + "rand", + "regex", + "serde", + "serde_json", + "slug", + "thread_local", + "unic-segment", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[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.2", +] + +[[package]] +name = "thread_local" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +dependencies = [ + "once_cell", +] + +[[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.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" +dependencies = [ + "itoa", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" + +[[package]] +name = "time-macros" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36" +dependencies = [ + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ac3f5b6856e931e15e07b478e98c8045239829a65f9156d4fa7e7788197a5ef" +dependencies = [ + "displaydoc", +] + +[[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.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03201d01c3c27a29c8a5cee5b55a93ddae1ccf6f08f65365c2c918f8c1b76f64" +dependencies = [ + "autocfg", + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "parking_lot 0.12.1", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "windows-sys", +] + +[[package]] +name = "tokio-rustls" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +dependencies = [ + "rustls", + "tokio", + "webpki", +] + +[[package]] +name = "tokio-stream" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fb52b74f05dbf495a8fba459fdc331812b96aa086d9eb78101fa0d4569c3313" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "toml" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebafdf5ad1220cb59e7d17cf4d2c72015297b75b19a10472f99b89225089240" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266f016b7f039eec8a1a80dfe6156b633d208b9fccca5e4db1d6775b0c4e34a7" +dependencies = [ + "indexmap 2.0.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[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.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "ucd-trie" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" + +[[package]] +name = "uncased" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b9bc53168a4be7402ab86c3aad243a84dd7381d09be0eddc81280c1da95ca68" +dependencies = [ + "version_check", +] + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-langid" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "398f9ad7239db44fd0f80fe068d12ff22d78354080332a5077dc6f52f14dcf2f" +dependencies = [ + "unic-langid-impl", +] + +[[package]] +name = "unic-langid-impl" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35bfd2f2b8796545b55d7d3fd3e89a0613f68a0d1c8bc28cb7ff96b411a35ff" +dependencies = [ + "serde", + "tinystr", +] + +[[package]] +name = "unic-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23" +dependencies = [ + "unic-ucd-segment", +] + +[[package]] +name = "unic-ucd-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524b68aca1d05e03fdf03fcdce2c6c94b6daf6d16861ddaa7e4f2b6638a9052c" + +[[package]] +name = "unicode-ident" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "url" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "uuid" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79" +dependencies = [ + "getrandom", + "rand", + "serde", +] + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[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 = "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.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 1.0.109", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" + +[[package]] +name = "web-sys" +version = "0.3.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" +dependencies = [ + "webpki", +] + +[[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 0.48.0", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +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-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.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca0ace3845f0d96209f0375e6d367e3eb87eb65d27d445bdc9f1843a26f39448" +dependencies = [ + "memchr", +] + +[[package]] +name = "zeroize" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" + +[[package]] +name = "zstd" +version = "0.12.3+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76eea132fb024e0e13fd9c2f5d5d595d8a967aa72382ac2f9d39fcc95afd0806" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "6.0.4+zstd.1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7afb4b54b8910cf5447638cb54bf4e8a65cbedd783af98b98c62ffe91f185543" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.7+zstd.1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94509c3ba2fe55294d752b79842c530ccfab760192521df74a081a78d2b3c7f5" +dependencies = [ + "cc", + "libc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index dd52982..c7651ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,38 +1,38 @@ -[package] -name = "rust-pw-server" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -actix-web = { version = "4", features = ["rustls"] } -actix-rt = "2" -tera = "1" -serde = "1" -thiserror = "1" -rust-argon2 = "1" -path-clean = "1" -uuid = { version = "1", features = [ "v4", "fast-rng", "serde" ] } -url = { version = "2", features = ["serde"] } -raise = "2" -serde_json = "1" -exun = "0.1" -base64 = "0.21" -rust-ini = "0.18" -jwt = "0.16" -dotenv = "0.15" -hmac = "0.12" -parking_lot = "0.12" -grass = "0.12" -sha2 = "0.10" -unic-langid = { version = "0.9", features = ["serde"] } -rand = "0.8" -bpaf = { version = "0.8", features = ["derive"] } -serde_urlencoded = "0.7" -toml = { version = "0.7", features = ["parse"] } -sqlx = { version = "0.6", features = [ "runtime-actix-rustls", "mysql", "uuid", "chrono", "offline" ] } -log = "0.4" -chrono = { version = "0.4", features = ["serde"] } -hex = "0.4" -serde_variant = "0.1" +[package] +name = "rust-pw-server" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +actix-web = { version = "4", features = ["rustls"] } +actix-rt = "2" +tera = "1" +serde = "1" +thiserror = "1" +rust-argon2 = "1" +path-clean = "1" +uuid = { version = "1", features = [ "v4", "fast-rng", "serde" ] } +url = { version = "2", features = ["serde"] } +raise = "2" +serde_json = "1" +exun = "0.1" +base64 = "0.21" +rust-ini = "0.18" +jwt = "0.16" +dotenv = "0.15" +hmac = "0.12" +parking_lot = "0.12" +grass = "0.12" +sha2 = "0.10" +unic-langid = { version = "0.9", features = ["serde"] } +rand = "0.8" +bpaf = { version = "0.8", features = ["derive"] } +serde_urlencoded = "0.7" +toml = { version = "0.7", features = ["parse"] } +sqlx = { version = "0.6", features = [ "runtime-actix-rustls", "mysql", "uuid", "chrono", "offline" ] } +log = "0.4" +chrono = { version = "0.4", features = ["serde"] } +hex = "0.4" +serde_variant = "0.1" diff --git a/rustfmt.toml b/rustfmt.toml index 751a0aa..48d6c3f 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,3 +1,3 @@ -edition = "2021" -hard_tabs = true -newline_style = "Unix" +edition = "2021" +hard_tabs = true +newline_style = "Unix" diff --git a/sqlx-data.json b/sqlx-data.json index cce3a53..7f4f975 100644 --- a/sqlx-data.json +++ b/sqlx-data.json @@ -1,1069 +1,1069 @@ -{ - "db": "MySQL", - "07221a593704fa3cb5d17f15f3fc18dff0359631db8393b5a1cebfdef748b495": { - "describe": { - "columns": [ - { - "name": "id: Uuid", - "ordinal": 0, - "type_info": { - "char_set": 63, - "flags": { - "bits": 4231 - }, - "max_size": 16, - "type": "String" - } - }, - { - "name": "username", - "ordinal": 1, - "type_info": { - "char_set": 224, - "flags": { - "bits": 4101 - }, - "max_size": 1020, - "type": "VarString" - } - }, - { - "name": "password_hash", - "ordinal": 2, - "type_info": { - "char_set": 63, - "flags": { - "bits": 4241 - }, - "max_size": 255, - "type": "Blob" - } - }, - { - "name": "password_salt", - "ordinal": 3, - "type_info": { - "char_set": 63, - "flags": { - "bits": 4241 - }, - "max_size": 255, - "type": "Blob" - } - }, - { - "name": "password_version", - "ordinal": 4, - "type_info": { - "char_set": 63, - "flags": { - "bits": 33 - }, - "max_size": 10, - "type": "Long" - } - } - ], - "nullable": [ - false, - false, - false, - false, - false - ], - "parameters": { - "Right": 1 - } - }, - "query": "SELECT id as `id: Uuid`, username, password_hash, password_salt, password_version\n\t\t FROM users\n\t\t WHERE LOCATE(?, username) != 0" - }, - "0d28efa4c9c7bdc32bc51152dab7cf4b2ecdd2955c930e59abfeed6e4b25e726": { - "describe": { - "columns": [ - { - "name": "e: bool", - "ordinal": 0, - "type_info": { - "char_set": 63, - "flags": { - "bits": 129 - }, - "max_size": 1, - "type": "LongLong" - } - } - ], - "nullable": [ - false - ], - "parameters": { - "Right": 1 - } - }, - "query": "SELECT EXISTS(SELECT id FROM clients WHERE id = ?) as `e: bool`" - }, - "0fb414b2015617ebdbe1303d71439302920d31275c97995d3d50513b07382ac1": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Right": 2 - } - }, - "query": "UPDATE clients SET type = ? WHERE id = ?" - }, - "19270d592676012569585d7796cb407d2c331dfbc7ac4481e5e38bcee5b6fcde": { - "describe": { - "columns": [ - { - "name": "type: ClientType", - "ordinal": 0, - "type_info": { - "char_set": 224, - "flags": { - "bits": 4097 - }, - "max_size": 180, - "type": "VarString" - } - } - ], - "nullable": [ - false - ], - "parameters": { - "Right": 1 - } - }, - "query": "SELECT type as `type: ClientType` FROM clients WHERE id = ?" - }, - "1ef0455513dcdc1b7e468d826139613502a8209aca0db3372cd4acc46c226ba5": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Right": 1 - } - }, - "query": "DELETE FROM client_redirect_uris WHERE client_id = ?" - }, - "22617c9e76806df78eb4a2636780837ff0993f142029a0e9d323981dd316a9d8": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Right": 2 - } - }, - "query": "INSERT INTO auth_codes (jti, exp)\n\t VALUES ( ?, ?)" - }, - "2558b6cad04d6c8af7efabc0e95e669e1de0ce9e04f7de2be321db4cbfae9eb5": { - "describe": { - "columns": [ - { - "name": "e: bool", - "ordinal": 0, - "type_info": { - "char_set": 63, - "flags": { - "bits": 129 - }, - "max_size": 1, - "type": "LongLong" - } - } - ], - "nullable": [ - false - ], - "parameters": { - "Right": 2 - } - }, - "query": "SELECT EXISTS(\n\t\t\t SELECT redirect_uri\n\t\t\t FROM client_redirect_uris\n\t\t\t WHERE client_id = ? AND redirect_uri = ?\n\t\t ) as `e: bool`" - }, - "32e1e172efd2dfe26c97ec9bf82b5d773a7373ebf949bbe73677c863cc67b45d": { - "describe": { - "columns": [ - { - "name": "e: bool", - "ordinal": 0, - "type_info": { - "char_set": 63, - "flags": { - "bits": 129 - }, - "max_size": 1, - "type": "LongLong" - } - } - ], - "nullable": [ - false - ], - "parameters": { - "Right": 1 - } - }, - "query": "SELECT EXISTS(SELECT alias FROM clients WHERE alias = ?) as `e: bool`" - }, - "37589f6cbc849bbbcf243c67392c1a39f6d3d408f999a030fd21e1b42021f08e": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Right": 3 - } - }, - "query": "INSERT INTO access_tokens (jti, auth_code, exp)\n\t VALUES ( ?, ?, ?)" - }, - "3976faac4ffd4660e3d9523fcb7f69f52797d7e0b0bc6a0b9bb18a5198bc9721": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Right": 1 - } - }, - "query": "DELETE FROM access_tokens WHERE auth_code = ?" - }, - "3eef97b5a7d77ef845923d890f929321c9a8a125893fe5f6c847364797d20c9c": { - "describe": { - "columns": [ - { - "name": "redirect_uri", - "ordinal": 0, - "type_info": { - "char_set": 224, - "flags": { - "bits": 4099 - }, - "max_size": 1020, - "type": "VarString" - } - } - ], - "nullable": [ - false - ], - "parameters": { - "Right": 1 - } - }, - "query": "SELECT redirect_uri FROM client_redirect_uris WHERE client_id = ?" - }, - "4de0b09543ed56032215a9830d75a2b41878bdb795df1fc3786a530a5455ae9e": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Right": 1 - } - }, - "query": "DELETE FROM refresh_tokens WHERE exp < ?" - }, - "4faa455ac38672dd2f3f29287125d772aae6956d7a3c0e67d31597e09778e1ee": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Right": 1 - } - }, - "query": "DELETE FROM auth_codes WHERE exp < ?" - }, - "5ae6b0a1174e5735cb3ea5b073f4d1877f7552ac0a6df54c978fcad9e87d5f9b": { - "describe": { - "columns": [ - { - "name": "allowed_scopes", - "ordinal": 0, - "type_info": { - "char_set": 224, - "flags": { - "bits": 4113 - }, - "max_size": 67108860, - "type": "Blob" - } - } - ], - "nullable": [ - false - ], - "parameters": { - "Right": 1 - } - }, - "query": "SELECT allowed_scopes FROM clients WHERE id = ?" - }, - "5c1a88c154b6e69bb53aee7d0beafbfe7519592f51579d7880117fa52b7be315": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Right": 8 - } - }, - "query": "INSERT INTO clients (id, alias, type, secret_hash, secret_salt, secret_version, allowed_scopes, default_scopes)\n\t\t\t\t\t VALUES ( ?, ?, ?, ?, ?, ?, ?, ?)" - }, - "5f3a2ca5d0f61a806ca58195ebbb051758302ed0d376875c671a0aaddb448224": { - "describe": { - "columns": [ - { - "name": "default_scopes", - "ordinal": 0, - "type_info": { - "char_set": 224, - "flags": { - "bits": 16 - }, - "max_size": 67108860, - "type": "Blob" - } - } - ], - "nullable": [ - true - ], - "parameters": { - "Right": 1 - } - }, - "query": "SELECT default_scopes FROM clients WHERE id = ?" - }, - "64bd64c1c6b272fdd47d12e928be89f2eb69cc0a9f904402d038616b460c8553": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Right": 2 - } - }, - "query": "UPDATE refresh_tokens SET revoked_reason = ? WHERE jti = ?" - }, - "65e689c69b316a8c3423cc6b96f536ec967530f8f1761f1fee45af98397f2292": { - "describe": { - "columns": [ - { - "name": "e: bool", - "ordinal": 0, - "type_info": { - "char_set": 63, - "flags": { - "bits": 129 - }, - "max_size": 1, - "type": "LongLong" - } - } - ], - "nullable": [ - false - ], - "parameters": { - "Right": 1 - } - }, - "query": "SELECT EXISTS(SELECT jti FROM refresh_tokens WHERE jti = ?) as `e: bool`" - }, - "67705466821f2940497b914bd10e7fafae76f5deb5d5460d9126ccfdb8fab51d": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Right": 1 - } - }, - "query": "DELETE FROM access_tokens WHERE exp < ?" - }, - "72abd9cddf183bcb13ea75491c532ede5a1b165c56347f0c4822ff19a50758d4": { - "describe": { - "columns": [ - { - "name": "e: bool", - "ordinal": 0, - "type_info": { - "char_set": 63, - "flags": { - "bits": 129 - }, - "max_size": 1, - "type": "LongLong" - } - } - ], - "nullable": [ - false - ], - "parameters": { - "Right": 1 - } - }, - "query": "SELECT EXISTS(SELECT jti FROM access_tokens WHERE jti = ?) as `e: bool`" - }, - "76a5f21dacb2b48fb797bcc0e5054b519192ae0bb6dcf8c29fbf9c2913b4746b": { - "describe": { - "columns": [ - { - "name": "username", - "ordinal": 0, - "type_info": { - "char_set": 224, - "flags": { - "bits": 4101 - }, - "max_size": 1020, - "type": "VarString" - } - } - ], - "nullable": [ - false - ], - "parameters": { - "Right": 1 - } - }, - "query": "SELECT username FROM users where id = ?" - }, - "7a004114b63d4356573591c960bb640d1d1ab61c4dc89e9030d59869278a2f94": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Right": 1 - } - }, - "query": "DELETE FROM auth_codes WHERE jti = ?" - }, - "7b6de4c923629669f449f91fe17679c8654a6ce9c1238b07dcec2cdb7fcdf18d": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Right": 2 - } - }, - "query": "UPDATE clients SET allowed_scopes = ? WHERE id = ?" - }, - "866d1d42c698528f0195a0c2fc7c971ca1a140802dd205bd9918bdcc08fe377b": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Right": 2 - } - }, - "query": "UPDATE clients SET alias = ? WHERE id = ?" - }, - "8c2f7aa20382907ae8e101522c75d6ea3d371d78aca92b2b7c90c544cc0e4919": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Right": 2 - } - }, - "query": "UPDATE refresh_tokens SET revoked_reason = ? WHERE auth_code = ?" - }, - "8f4656ed3a928dd4b33cf037b9aa60092a17219b9a46366a5fdb0c28ea3e79a7": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Right": 1 - } - }, - "query": "UPDATE clients\n\t\t\t SET secret_hash = NULL, secret_salt = NULL, secret_version = NULL\n\t\t\t WHERE id = ?" - }, - "91688c5521ab1272e4937451a2bd9c467915f8e4d8cef6eac95013a5a94cc08a": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Right": 5 - } - }, - "query": "INSERT INTO users (id, username, password_hash, password_salt, password_version)\n\t\t\t\t\t VALUES ( ?, ?, ?, ?, ?)" - }, - "95484e1dd619ec5e486ce61b3827a08cbe629826d1fb89a6af9790eb54eb2185": { - "describe": { - "columns": [ - { - "name": "e: bool", - "ordinal": 0, - "type_info": { - "char_set": 63, - "flags": { - "bits": 129 - }, - "max_size": 1, - "type": "LongLong" - } - } - ], - "nullable": [ - false - ], - "parameters": { - "Right": 1 - } - }, - "query": "SELECT EXISTS(SELECT jti FROM auth_codes WHERE jti = ?) as `e: bool`" - }, - "9710cd5915616165c6d27031b21cc7b3cfbd5aae574eb07797dca57064880ef9": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Right": 2 - } - }, - "query": "UPDATE users SET username = ? WHERE id = ?" - }, - "981d6ca67138bfa4377025ff560f53fd77edcb9bed0d7f0cfb3468357ea5f1fe": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Right": 8 - } - }, - "query": "UPDATE clients SET\n\t\talias = ?,\n\t\ttype = ?,\n\t\tsecret_hash = ?,\n\t\tsecret_salt = ?,\n\t\tsecret_version = ?,\n\t\tallowed_scopes = ?,\n\t\tdefault_scopes = ?\n\t\tWHERE id = ?" - }, - "983348e316c3c8c11f9f5cf0479170d4d7246696010302a472267caeb5d2b62d": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Right": 2 - } - }, - "query": "UPDATE clients SET default_scopes = ? WHERE id = ?" - }, - "a5d7e7e4a36cb1bb0675ccde12dadd013ae2c847648b3274494e206b14cc1370": { - "describe": { - "columns": [ - { - "name": "id: Uuid", - "ordinal": 0, - "type_info": { - "char_set": 63, - "flags": { - "bits": 4231 - }, - "max_size": 16, - "type": "String" - } - }, - { - "name": "username", - "ordinal": 1, - "type_info": { - "char_set": 224, - "flags": { - "bits": 4101 - }, - "max_size": 1020, - "type": "VarString" - } - }, - { - "name": "password_hash", - "ordinal": 2, - "type_info": { - "char_set": 63, - "flags": { - "bits": 4241 - }, - "max_size": 255, - "type": "Blob" - } - }, - { - "name": "password_salt", - "ordinal": 3, - "type_info": { - "char_set": 63, - "flags": { - "bits": 4241 - }, - "max_size": 255, - "type": "Blob" - } - }, - { - "name": "password_version", - "ordinal": 4, - "type_info": { - "char_set": 63, - "flags": { - "bits": 33 - }, - "max_size": 10, - "type": "Long" - } - } - ], - "nullable": [ - false, - false, - false, - false, - false - ], - "parameters": { - "Right": 1 - } - }, - "query": "SELECT id as `id: Uuid`, username, password_hash, password_salt, password_version\n\t\t FROM users WHERE id = ?" - }, - "ac93da5d341986aef384f8f11c24861fc290aa9974c44400fb46ee09e383dcae": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Right": 2 - } - }, - "query": "INSERT INTO client_redirect_uris (client_id, redirect_uri)\n\t\t\t\t\t\t\t\t\t VALUES ( ?, ?)" - }, - "b1d60244a68b9c132e5b3125505606d156913acf062802e4e1783f9e859f4c49": { - "describe": { - "columns": [ - { - "name": "e: bool", - "ordinal": 0, - "type_info": { - "char_set": 63, - "flags": { - "bits": 129 - }, - "max_size": 1, - "type": "LongLong" - } - } - ], - "nullable": [ - false - ], - "parameters": { - "Right": 1 - } - }, - "query": "SELECT EXISTS(SELECT id FROM users WHERE username = ?) as \"e: bool\"" - }, - "b765470e11aa3a02586b0ea0a65f1bb93f104afde56fb2d77b2c72a8742fb9e0": { - "describe": { - "columns": [ - { - "name": "secret_hash", - "ordinal": 0, - "type_info": { - "char_set": 63, - "flags": { - "bits": 144 - }, - "max_size": 255, - "type": "Blob" - } - }, - { - "name": "secret_salt", - "ordinal": 1, - "type_info": { - "char_set": 63, - "flags": { - "bits": 144 - }, - "max_size": 255, - "type": "Blob" - } - }, - { - "name": "secret_version", - "ordinal": 2, - "type_info": { - "char_set": 63, - "flags": { - "bits": 32 - }, - "max_size": 10, - "type": "Long" - } - } - ], - "nullable": [ - true, - true, - true - ], - "parameters": { - "Right": 1 - } - }, - "query": "SELECT secret_hash, secret_salt, secret_version\n\t\tFROM clients WHERE id = ?" - }, - "c61516c0c3d51f322a8207581802c2c9723a65beeaeae558d997590dc9e88ef2": { - "describe": { - "columns": [ - { - "name": "e: bool", - "ordinal": 0, - "type_info": { - "char_set": 63, - "flags": { - "bits": 129 - }, - "max_size": 1, - "type": "LongLong" - } - } - ], - "nullable": [ - false - ], - "parameters": { - "Right": 1 - } - }, - "query": "SELECT EXISTS(SELECT id FROM users WHERE id = ?) as `e: bool`" - }, - "db75cdf97363794437c3a14c142639473ac2a07cdf00fa7186407c27678dee96": { - "describe": { - "columns": [ - { - "name": "e: bool", - "ordinal": 0, - "type_info": { - "char_set": 63, - "flags": { - "bits": 128 - }, - "max_size": 1, - "type": "LongLong" - } - } - ], - "nullable": [ - true - ], - "parameters": { - "Right": 1 - } - }, - "query": "SELECT EXISTS(\n\t\t\tSELECT revoked_reason FROM refresh_tokens WHERE jti = ? and revoked_reason IS NOT NULL\n\t\t) as `e: bool`" - }, - "dda087e364dd82216ea8e5d7266d63ab671382744eb350d446fe1025e2df12bb": { - "describe": { - "columns": [ - { - "name": "alias", - "ordinal": 0, - "type_info": { - "char_set": 224, - "flags": { - "bits": 4101 - }, - "max_size": 1020, - "type": "VarString" - } - } - ], - "nullable": [ - false - ], - "parameters": { - "Right": 1 - } - }, - "query": "SELECT alias FROM clients WHERE id = ?" - }, - "df0033aa7c0e5066fed30d944387293d26d1de93b1a24a202214d6ee06fc6a1c": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Right": 4 - } - }, - "query": "UPDATE users SET\n\t\tpassword_hash = ?,\n\t\tpassword_salt = ?,\n\t\tpassword_version = ?\n\t\tWHERE id = ?" - }, - "e757406f5b996a1204700cd4840ac2c5d1e09b82e13aa98d6dc017da81c059e0": { - "describe": { - "columns": [ - { - "name": "id: Uuid", - "ordinal": 0, - "type_info": { - "char_set": 63, - "flags": { - "bits": 4231 - }, - "max_size": 16, - "type": "String" - } - }, - { - "name": "alias", - "ordinal": 1, - "type_info": { - "char_set": 224, - "flags": { - "bits": 4101 - }, - "max_size": 1020, - "type": "VarString" - } - }, - { - "name": "client_type: ClientType", - "ordinal": 2, - "type_info": { - "char_set": 224, - "flags": { - "bits": 4097 - }, - "max_size": 180, - "type": "VarString" - } - }, - { - "name": "allowed_scopes", - "ordinal": 3, - "type_info": { - "char_set": 224, - "flags": { - "bits": 4113 - }, - "max_size": 67108860, - "type": "Blob" - } - }, - { - "name": "default_scopes", - "ordinal": 4, - "type_info": { - "char_set": 224, - "flags": { - "bits": 16 - }, - "max_size": 67108860, - "type": "Blob" - } - } - ], - "nullable": [ - false, - false, - false, - false, - true - ], - "parameters": { - "Right": 1 - } - }, - "query": "SELECT id as `id: Uuid`,\n\t\t alias,\n\t\t\t\t type as `client_type: ClientType`,\n\t\t\t\t allowed_scopes,\n\t\t\t\t default_scopes\n\t\t FROM clients WHERE id = ?" - }, - "f39c1d0c05c8cba9f31aa7365b36eff3c258eb6f554be456600f79b925a808d6": { - "describe": { - "columns": [ - { - "name": "id: Uuid", - "ordinal": 0, - "type_info": { - "char_set": 63, - "flags": { - "bits": 4231 - }, - "max_size": 16, - "type": "String" - } - } - ], - "nullable": [ - false - ], - "parameters": { - "Right": 1 - } - }, - "query": "SELECT id as `id: Uuid` FROM clients WHERE alias = ?" - }, - "f488b319d6f387db08fb49920ddb381b2b1496605914275cd1ccd81c9420b23c": { - "describe": { - "columns": [ - { - "name": "id: Uuid", - "ordinal": 0, - "type_info": { - "char_set": 63, - "flags": { - "bits": 4231 - }, - "max_size": 16, - "type": "String" - } - }, - { - "name": "username", - "ordinal": 1, - "type_info": { - "char_set": 224, - "flags": { - "bits": 4101 - }, - "max_size": 1020, - "type": "VarString" - } - }, - { - "name": "password_hash", - "ordinal": 2, - "type_info": { - "char_set": 63, - "flags": { - "bits": 4241 - }, - "max_size": 255, - "type": "Blob" - } - }, - { - "name": "password_salt", - "ordinal": 3, - "type_info": { - "char_set": 63, - "flags": { - "bits": 4241 - }, - "max_size": 255, - "type": "Blob" - } - }, - { - "name": "password_version", - "ordinal": 4, - "type_info": { - "char_set": 63, - "flags": { - "bits": 33 - }, - "max_size": 10, - "type": "Long" - } - } - ], - "nullable": [ - false, - false, - false, - false, - false - ], - "parameters": { - "Right": 3 - } - }, - "query": "SELECT id as `id: Uuid`, username, password_hash, password_salt, password_version\n\t\t FROM users\n\t\t WHERE LOCATE(?, username) != 0\n\t\t LIMIT ?\n\t\t OFFSET ?" - }, - "f4e088a309a5fa63652fd1aeb95805d64d255a12d5313dbf2f7f2f99c7918e62": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Right": 4 - } - }, - "query": "UPDATE clients SET secret_hash = ?, secret_salt = ?, secret_version = ? WHERE id = ?" - }, - "f9d2c85bdcc3b7d0d1fca4e2f0bb37df6dee23bc50af97d8e4112baacd6eb7c9": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Right": 5 - } - }, - "query": "UPDATE users SET\n\t\t username = ?,\n\t\t password_hash = ?,\n\t\t password_salt = ?,\n\t\t password_version = ?\n\t\t WHERE id = ?" - }, - "fc393b1464413bb7045d33a8ca5aa0100ab217434570e6be732f97db1d9b04aa": { - "describe": { - "columns": [ - { - "name": "id: Uuid", - "ordinal": 0, - "type_info": { - "char_set": 63, - "flags": { - "bits": 4231 - }, - "max_size": 16, - "type": "String" - } - }, - { - "name": "username", - "ordinal": 1, - "type_info": { - "char_set": 224, - "flags": { - "bits": 4101 - }, - "max_size": 1020, - "type": "VarString" - } - }, - { - "name": "password_hash", - "ordinal": 2, - "type_info": { - "char_set": 63, - "flags": { - "bits": 4241 - }, - "max_size": 255, - "type": "Blob" - } - }, - { - "name": "password_salt", - "ordinal": 3, - "type_info": { - "char_set": 63, - "flags": { - "bits": 4241 - }, - "max_size": 255, - "type": "Blob" - } - }, - { - "name": "password_version", - "ordinal": 4, - "type_info": { - "char_set": 63, - "flags": { - "bits": 33 - }, - "max_size": 10, - "type": "Long" - } - } - ], - "nullable": [ - false, - false, - false, - false, - false - ], - "parameters": { - "Right": 1 - } - }, - "query": "SELECT id as `id: Uuid`, username, password_hash, password_salt, password_version\n\t\t FROM users WHERE username = ?" - } +{ + "db": "MySQL", + "07221a593704fa3cb5d17f15f3fc18dff0359631db8393b5a1cebfdef748b495": { + "describe": { + "columns": [ + { + "name": "id: Uuid", + "ordinal": 0, + "type_info": { + "char_set": 63, + "flags": { + "bits": 4231 + }, + "max_size": 16, + "type": "String" + } + }, + { + "name": "username", + "ordinal": 1, + "type_info": { + "char_set": 224, + "flags": { + "bits": 4101 + }, + "max_size": 1020, + "type": "VarString" + } + }, + { + "name": "password_hash", + "ordinal": 2, + "type_info": { + "char_set": 63, + "flags": { + "bits": 4241 + }, + "max_size": 255, + "type": "Blob" + } + }, + { + "name": "password_salt", + "ordinal": 3, + "type_info": { + "char_set": 63, + "flags": { + "bits": 4241 + }, + "max_size": 255, + "type": "Blob" + } + }, + { + "name": "password_version", + "ordinal": 4, + "type_info": { + "char_set": 63, + "flags": { + "bits": 33 + }, + "max_size": 10, + "type": "Long" + } + } + ], + "nullable": [ + false, + false, + false, + false, + false + ], + "parameters": { + "Right": 1 + } + }, + "query": "SELECT id as `id: Uuid`, username, password_hash, password_salt, password_version\n\t\t FROM users\n\t\t WHERE LOCATE(?, username) != 0" + }, + "0d28efa4c9c7bdc32bc51152dab7cf4b2ecdd2955c930e59abfeed6e4b25e726": { + "describe": { + "columns": [ + { + "name": "e: bool", + "ordinal": 0, + "type_info": { + "char_set": 63, + "flags": { + "bits": 129 + }, + "max_size": 1, + "type": "LongLong" + } + } + ], + "nullable": [ + false + ], + "parameters": { + "Right": 1 + } + }, + "query": "SELECT EXISTS(SELECT id FROM clients WHERE id = ?) as `e: bool`" + }, + "0fb414b2015617ebdbe1303d71439302920d31275c97995d3d50513b07382ac1": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 2 + } + }, + "query": "UPDATE clients SET type = ? WHERE id = ?" + }, + "19270d592676012569585d7796cb407d2c331dfbc7ac4481e5e38bcee5b6fcde": { + "describe": { + "columns": [ + { + "name": "type: ClientType", + "ordinal": 0, + "type_info": { + "char_set": 224, + "flags": { + "bits": 4097 + }, + "max_size": 180, + "type": "VarString" + } + } + ], + "nullable": [ + false + ], + "parameters": { + "Right": 1 + } + }, + "query": "SELECT type as `type: ClientType` FROM clients WHERE id = ?" + }, + "1ef0455513dcdc1b7e468d826139613502a8209aca0db3372cd4acc46c226ba5": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 1 + } + }, + "query": "DELETE FROM client_redirect_uris WHERE client_id = ?" + }, + "22617c9e76806df78eb4a2636780837ff0993f142029a0e9d323981dd316a9d8": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 2 + } + }, + "query": "INSERT INTO auth_codes (jti, exp)\n\t VALUES ( ?, ?)" + }, + "2558b6cad04d6c8af7efabc0e95e669e1de0ce9e04f7de2be321db4cbfae9eb5": { + "describe": { + "columns": [ + { + "name": "e: bool", + "ordinal": 0, + "type_info": { + "char_set": 63, + "flags": { + "bits": 129 + }, + "max_size": 1, + "type": "LongLong" + } + } + ], + "nullable": [ + false + ], + "parameters": { + "Right": 2 + } + }, + "query": "SELECT EXISTS(\n\t\t\t SELECT redirect_uri\n\t\t\t FROM client_redirect_uris\n\t\t\t WHERE client_id = ? AND redirect_uri = ?\n\t\t ) as `e: bool`" + }, + "32e1e172efd2dfe26c97ec9bf82b5d773a7373ebf949bbe73677c863cc67b45d": { + "describe": { + "columns": [ + { + "name": "e: bool", + "ordinal": 0, + "type_info": { + "char_set": 63, + "flags": { + "bits": 129 + }, + "max_size": 1, + "type": "LongLong" + } + } + ], + "nullable": [ + false + ], + "parameters": { + "Right": 1 + } + }, + "query": "SELECT EXISTS(SELECT alias FROM clients WHERE alias = ?) as `e: bool`" + }, + "37589f6cbc849bbbcf243c67392c1a39f6d3d408f999a030fd21e1b42021f08e": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 3 + } + }, + "query": "INSERT INTO access_tokens (jti, auth_code, exp)\n\t VALUES ( ?, ?, ?)" + }, + "3976faac4ffd4660e3d9523fcb7f69f52797d7e0b0bc6a0b9bb18a5198bc9721": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 1 + } + }, + "query": "DELETE FROM access_tokens WHERE auth_code = ?" + }, + "3eef97b5a7d77ef845923d890f929321c9a8a125893fe5f6c847364797d20c9c": { + "describe": { + "columns": [ + { + "name": "redirect_uri", + "ordinal": 0, + "type_info": { + "char_set": 224, + "flags": { + "bits": 4099 + }, + "max_size": 1020, + "type": "VarString" + } + } + ], + "nullable": [ + false + ], + "parameters": { + "Right": 1 + } + }, + "query": "SELECT redirect_uri FROM client_redirect_uris WHERE client_id = ?" + }, + "4de0b09543ed56032215a9830d75a2b41878bdb795df1fc3786a530a5455ae9e": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 1 + } + }, + "query": "DELETE FROM refresh_tokens WHERE exp < ?" + }, + "4faa455ac38672dd2f3f29287125d772aae6956d7a3c0e67d31597e09778e1ee": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 1 + } + }, + "query": "DELETE FROM auth_codes WHERE exp < ?" + }, + "5ae6b0a1174e5735cb3ea5b073f4d1877f7552ac0a6df54c978fcad9e87d5f9b": { + "describe": { + "columns": [ + { + "name": "allowed_scopes", + "ordinal": 0, + "type_info": { + "char_set": 224, + "flags": { + "bits": 4113 + }, + "max_size": 67108860, + "type": "Blob" + } + } + ], + "nullable": [ + false + ], + "parameters": { + "Right": 1 + } + }, + "query": "SELECT allowed_scopes FROM clients WHERE id = ?" + }, + "5c1a88c154b6e69bb53aee7d0beafbfe7519592f51579d7880117fa52b7be315": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 8 + } + }, + "query": "INSERT INTO clients (id, alias, type, secret_hash, secret_salt, secret_version, allowed_scopes, default_scopes)\n\t\t\t\t\t VALUES ( ?, ?, ?, ?, ?, ?, ?, ?)" + }, + "5f3a2ca5d0f61a806ca58195ebbb051758302ed0d376875c671a0aaddb448224": { + "describe": { + "columns": [ + { + "name": "default_scopes", + "ordinal": 0, + "type_info": { + "char_set": 224, + "flags": { + "bits": 16 + }, + "max_size": 67108860, + "type": "Blob" + } + } + ], + "nullable": [ + true + ], + "parameters": { + "Right": 1 + } + }, + "query": "SELECT default_scopes FROM clients WHERE id = ?" + }, + "64bd64c1c6b272fdd47d12e928be89f2eb69cc0a9f904402d038616b460c8553": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 2 + } + }, + "query": "UPDATE refresh_tokens SET revoked_reason = ? WHERE jti = ?" + }, + "65e689c69b316a8c3423cc6b96f536ec967530f8f1761f1fee45af98397f2292": { + "describe": { + "columns": [ + { + "name": "e: bool", + "ordinal": 0, + "type_info": { + "char_set": 63, + "flags": { + "bits": 129 + }, + "max_size": 1, + "type": "LongLong" + } + } + ], + "nullable": [ + false + ], + "parameters": { + "Right": 1 + } + }, + "query": "SELECT EXISTS(SELECT jti FROM refresh_tokens WHERE jti = ?) as `e: bool`" + }, + "67705466821f2940497b914bd10e7fafae76f5deb5d5460d9126ccfdb8fab51d": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 1 + } + }, + "query": "DELETE FROM access_tokens WHERE exp < ?" + }, + "72abd9cddf183bcb13ea75491c532ede5a1b165c56347f0c4822ff19a50758d4": { + "describe": { + "columns": [ + { + "name": "e: bool", + "ordinal": 0, + "type_info": { + "char_set": 63, + "flags": { + "bits": 129 + }, + "max_size": 1, + "type": "LongLong" + } + } + ], + "nullable": [ + false + ], + "parameters": { + "Right": 1 + } + }, + "query": "SELECT EXISTS(SELECT jti FROM access_tokens WHERE jti = ?) as `e: bool`" + }, + "76a5f21dacb2b48fb797bcc0e5054b519192ae0bb6dcf8c29fbf9c2913b4746b": { + "describe": { + "columns": [ + { + "name": "username", + "ordinal": 0, + "type_info": { + "char_set": 224, + "flags": { + "bits": 4101 + }, + "max_size": 1020, + "type": "VarString" + } + } + ], + "nullable": [ + false + ], + "parameters": { + "Right": 1 + } + }, + "query": "SELECT username FROM users where id = ?" + }, + "7a004114b63d4356573591c960bb640d1d1ab61c4dc89e9030d59869278a2f94": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 1 + } + }, + "query": "DELETE FROM auth_codes WHERE jti = ?" + }, + "7b6de4c923629669f449f91fe17679c8654a6ce9c1238b07dcec2cdb7fcdf18d": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 2 + } + }, + "query": "UPDATE clients SET allowed_scopes = ? WHERE id = ?" + }, + "866d1d42c698528f0195a0c2fc7c971ca1a140802dd205bd9918bdcc08fe377b": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 2 + } + }, + "query": "UPDATE clients SET alias = ? WHERE id = ?" + }, + "8c2f7aa20382907ae8e101522c75d6ea3d371d78aca92b2b7c90c544cc0e4919": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 2 + } + }, + "query": "UPDATE refresh_tokens SET revoked_reason = ? WHERE auth_code = ?" + }, + "8f4656ed3a928dd4b33cf037b9aa60092a17219b9a46366a5fdb0c28ea3e79a7": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 1 + } + }, + "query": "UPDATE clients\n\t\t\t SET secret_hash = NULL, secret_salt = NULL, secret_version = NULL\n\t\t\t WHERE id = ?" + }, + "91688c5521ab1272e4937451a2bd9c467915f8e4d8cef6eac95013a5a94cc08a": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 5 + } + }, + "query": "INSERT INTO users (id, username, password_hash, password_salt, password_version)\n\t\t\t\t\t VALUES ( ?, ?, ?, ?, ?)" + }, + "95484e1dd619ec5e486ce61b3827a08cbe629826d1fb89a6af9790eb54eb2185": { + "describe": { + "columns": [ + { + "name": "e: bool", + "ordinal": 0, + "type_info": { + "char_set": 63, + "flags": { + "bits": 129 + }, + "max_size": 1, + "type": "LongLong" + } + } + ], + "nullable": [ + false + ], + "parameters": { + "Right": 1 + } + }, + "query": "SELECT EXISTS(SELECT jti FROM auth_codes WHERE jti = ?) as `e: bool`" + }, + "9710cd5915616165c6d27031b21cc7b3cfbd5aae574eb07797dca57064880ef9": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 2 + } + }, + "query": "UPDATE users SET username = ? WHERE id = ?" + }, + "981d6ca67138bfa4377025ff560f53fd77edcb9bed0d7f0cfb3468357ea5f1fe": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 8 + } + }, + "query": "UPDATE clients SET\n\t\talias = ?,\n\t\ttype = ?,\n\t\tsecret_hash = ?,\n\t\tsecret_salt = ?,\n\t\tsecret_version = ?,\n\t\tallowed_scopes = ?,\n\t\tdefault_scopes = ?\n\t\tWHERE id = ?" + }, + "983348e316c3c8c11f9f5cf0479170d4d7246696010302a472267caeb5d2b62d": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 2 + } + }, + "query": "UPDATE clients SET default_scopes = ? WHERE id = ?" + }, + "a5d7e7e4a36cb1bb0675ccde12dadd013ae2c847648b3274494e206b14cc1370": { + "describe": { + "columns": [ + { + "name": "id: Uuid", + "ordinal": 0, + "type_info": { + "char_set": 63, + "flags": { + "bits": 4231 + }, + "max_size": 16, + "type": "String" + } + }, + { + "name": "username", + "ordinal": 1, + "type_info": { + "char_set": 224, + "flags": { + "bits": 4101 + }, + "max_size": 1020, + "type": "VarString" + } + }, + { + "name": "password_hash", + "ordinal": 2, + "type_info": { + "char_set": 63, + "flags": { + "bits": 4241 + }, + "max_size": 255, + "type": "Blob" + } + }, + { + "name": "password_salt", + "ordinal": 3, + "type_info": { + "char_set": 63, + "flags": { + "bits": 4241 + }, + "max_size": 255, + "type": "Blob" + } + }, + { + "name": "password_version", + "ordinal": 4, + "type_info": { + "char_set": 63, + "flags": { + "bits": 33 + }, + "max_size": 10, + "type": "Long" + } + } + ], + "nullable": [ + false, + false, + false, + false, + false + ], + "parameters": { + "Right": 1 + } + }, + "query": "SELECT id as `id: Uuid`, username, password_hash, password_salt, password_version\n\t\t FROM users WHERE id = ?" + }, + "ac93da5d341986aef384f8f11c24861fc290aa9974c44400fb46ee09e383dcae": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 2 + } + }, + "query": "INSERT INTO client_redirect_uris (client_id, redirect_uri)\n\t\t\t\t\t\t\t\t\t VALUES ( ?, ?)" + }, + "b1d60244a68b9c132e5b3125505606d156913acf062802e4e1783f9e859f4c49": { + "describe": { + "columns": [ + { + "name": "e: bool", + "ordinal": 0, + "type_info": { + "char_set": 63, + "flags": { + "bits": 129 + }, + "max_size": 1, + "type": "LongLong" + } + } + ], + "nullable": [ + false + ], + "parameters": { + "Right": 1 + } + }, + "query": "SELECT EXISTS(SELECT id FROM users WHERE username = ?) as \"e: bool\"" + }, + "b765470e11aa3a02586b0ea0a65f1bb93f104afde56fb2d77b2c72a8742fb9e0": { + "describe": { + "columns": [ + { + "name": "secret_hash", + "ordinal": 0, + "type_info": { + "char_set": 63, + "flags": { + "bits": 144 + }, + "max_size": 255, + "type": "Blob" + } + }, + { + "name": "secret_salt", + "ordinal": 1, + "type_info": { + "char_set": 63, + "flags": { + "bits": 144 + }, + "max_size": 255, + "type": "Blob" + } + }, + { + "name": "secret_version", + "ordinal": 2, + "type_info": { + "char_set": 63, + "flags": { + "bits": 32 + }, + "max_size": 10, + "type": "Long" + } + } + ], + "nullable": [ + true, + true, + true + ], + "parameters": { + "Right": 1 + } + }, + "query": "SELECT secret_hash, secret_salt, secret_version\n\t\tFROM clients WHERE id = ?" + }, + "c61516c0c3d51f322a8207581802c2c9723a65beeaeae558d997590dc9e88ef2": { + "describe": { + "columns": [ + { + "name": "e: bool", + "ordinal": 0, + "type_info": { + "char_set": 63, + "flags": { + "bits": 129 + }, + "max_size": 1, + "type": "LongLong" + } + } + ], + "nullable": [ + false + ], + "parameters": { + "Right": 1 + } + }, + "query": "SELECT EXISTS(SELECT id FROM users WHERE id = ?) as `e: bool`" + }, + "db75cdf97363794437c3a14c142639473ac2a07cdf00fa7186407c27678dee96": { + "describe": { + "columns": [ + { + "name": "e: bool", + "ordinal": 0, + "type_info": { + "char_set": 63, + "flags": { + "bits": 128 + }, + "max_size": 1, + "type": "LongLong" + } + } + ], + "nullable": [ + true + ], + "parameters": { + "Right": 1 + } + }, + "query": "SELECT EXISTS(\n\t\t\tSELECT revoked_reason FROM refresh_tokens WHERE jti = ? and revoked_reason IS NOT NULL\n\t\t) as `e: bool`" + }, + "dda087e364dd82216ea8e5d7266d63ab671382744eb350d446fe1025e2df12bb": { + "describe": { + "columns": [ + { + "name": "alias", + "ordinal": 0, + "type_info": { + "char_set": 224, + "flags": { + "bits": 4101 + }, + "max_size": 1020, + "type": "VarString" + } + } + ], + "nullable": [ + false + ], + "parameters": { + "Right": 1 + } + }, + "query": "SELECT alias FROM clients WHERE id = ?" + }, + "df0033aa7c0e5066fed30d944387293d26d1de93b1a24a202214d6ee06fc6a1c": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 4 + } + }, + "query": "UPDATE users SET\n\t\tpassword_hash = ?,\n\t\tpassword_salt = ?,\n\t\tpassword_version = ?\n\t\tWHERE id = ?" + }, + "e757406f5b996a1204700cd4840ac2c5d1e09b82e13aa98d6dc017da81c059e0": { + "describe": { + "columns": [ + { + "name": "id: Uuid", + "ordinal": 0, + "type_info": { + "char_set": 63, + "flags": { + "bits": 4231 + }, + "max_size": 16, + "type": "String" + } + }, + { + "name": "alias", + "ordinal": 1, + "type_info": { + "char_set": 224, + "flags": { + "bits": 4101 + }, + "max_size": 1020, + "type": "VarString" + } + }, + { + "name": "client_type: ClientType", + "ordinal": 2, + "type_info": { + "char_set": 224, + "flags": { + "bits": 4097 + }, + "max_size": 180, + "type": "VarString" + } + }, + { + "name": "allowed_scopes", + "ordinal": 3, + "type_info": { + "char_set": 224, + "flags": { + "bits": 4113 + }, + "max_size": 67108860, + "type": "Blob" + } + }, + { + "name": "default_scopes", + "ordinal": 4, + "type_info": { + "char_set": 224, + "flags": { + "bits": 16 + }, + "max_size": 67108860, + "type": "Blob" + } + } + ], + "nullable": [ + false, + false, + false, + false, + true + ], + "parameters": { + "Right": 1 + } + }, + "query": "SELECT id as `id: Uuid`,\n\t\t alias,\n\t\t\t\t type as `client_type: ClientType`,\n\t\t\t\t allowed_scopes,\n\t\t\t\t default_scopes\n\t\t FROM clients WHERE id = ?" + }, + "f39c1d0c05c8cba9f31aa7365b36eff3c258eb6f554be456600f79b925a808d6": { + "describe": { + "columns": [ + { + "name": "id: Uuid", + "ordinal": 0, + "type_info": { + "char_set": 63, + "flags": { + "bits": 4231 + }, + "max_size": 16, + "type": "String" + } + } + ], + "nullable": [ + false + ], + "parameters": { + "Right": 1 + } + }, + "query": "SELECT id as `id: Uuid` FROM clients WHERE alias = ?" + }, + "f488b319d6f387db08fb49920ddb381b2b1496605914275cd1ccd81c9420b23c": { + "describe": { + "columns": [ + { + "name": "id: Uuid", + "ordinal": 0, + "type_info": { + "char_set": 63, + "flags": { + "bits": 4231 + }, + "max_size": 16, + "type": "String" + } + }, + { + "name": "username", + "ordinal": 1, + "type_info": { + "char_set": 224, + "flags": { + "bits": 4101 + }, + "max_size": 1020, + "type": "VarString" + } + }, + { + "name": "password_hash", + "ordinal": 2, + "type_info": { + "char_set": 63, + "flags": { + "bits": 4241 + }, + "max_size": 255, + "type": "Blob" + } + }, + { + "name": "password_salt", + "ordinal": 3, + "type_info": { + "char_set": 63, + "flags": { + "bits": 4241 + }, + "max_size": 255, + "type": "Blob" + } + }, + { + "name": "password_version", + "ordinal": 4, + "type_info": { + "char_set": 63, + "flags": { + "bits": 33 + }, + "max_size": 10, + "type": "Long" + } + } + ], + "nullable": [ + false, + false, + false, + false, + false + ], + "parameters": { + "Right": 3 + } + }, + "query": "SELECT id as `id: Uuid`, username, password_hash, password_salt, password_version\n\t\t FROM users\n\t\t WHERE LOCATE(?, username) != 0\n\t\t LIMIT ?\n\t\t OFFSET ?" + }, + "f4e088a309a5fa63652fd1aeb95805d64d255a12d5313dbf2f7f2f99c7918e62": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 4 + } + }, + "query": "UPDATE clients SET secret_hash = ?, secret_salt = ?, secret_version = ? WHERE id = ?" + }, + "f9d2c85bdcc3b7d0d1fca4e2f0bb37df6dee23bc50af97d8e4112baacd6eb7c9": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 5 + } + }, + "query": "UPDATE users SET\n\t\t username = ?,\n\t\t password_hash = ?,\n\t\t password_salt = ?,\n\t\t password_version = ?\n\t\t WHERE id = ?" + }, + "fc393b1464413bb7045d33a8ca5aa0100ab217434570e6be732f97db1d9b04aa": { + "describe": { + "columns": [ + { + "name": "id: Uuid", + "ordinal": 0, + "type_info": { + "char_set": 63, + "flags": { + "bits": 4231 + }, + "max_size": 16, + "type": "String" + } + }, + { + "name": "username", + "ordinal": 1, + "type_info": { + "char_set": 224, + "flags": { + "bits": 4101 + }, + "max_size": 1020, + "type": "VarString" + } + }, + { + "name": "password_hash", + "ordinal": 2, + "type_info": { + "char_set": 63, + "flags": { + "bits": 4241 + }, + "max_size": 255, + "type": "Blob" + } + }, + { + "name": "password_salt", + "ordinal": 3, + "type_info": { + "char_set": 63, + "flags": { + "bits": 4241 + }, + "max_size": 255, + "type": "Blob" + } + }, + { + "name": "password_version", + "ordinal": 4, + "type_info": { + "char_set": 63, + "flags": { + "bits": 33 + }, + "max_size": 10, + "type": "Long" + } + } + ], + "nullable": [ + false, + false, + false, + false, + false + ], + "parameters": { + "Right": 1 + } + }, + "query": "SELECT id as `id: Uuid`, username, password_hash, password_salt, password_version\n\t\t FROM users WHERE username = ?" + } } \ No newline at end of file diff --git a/src/api/clients.rs b/src/api/clients.rs index 3f906bb..ded8b81 100644 --- a/src/api/clients.rs +++ b/src/api/clients.rs @@ -1,483 +1,483 @@ -use actix_web::http::{header, StatusCode}; -use actix_web::{get, post, put, web, HttpResponse, ResponseError, Scope}; -use raise::yeet; -use serde::{Deserialize, Serialize}; -use sqlx::MySqlPool; -use thiserror::Error; -use url::Url; -use uuid::Uuid; - -use crate::models::client::{Client, ClientType, CreateClientError}; -use crate::services::crypto::PasswordHash; -use crate::services::db::ClientRow; -use crate::services::{db, id}; - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -struct ClientResponse { - client_id: Uuid, - alias: Box, - client_type: ClientType, - allowed_scopes: Box<[Box]>, - default_scopes: Option]>>, - is_trusted: bool, -} - -impl From for ClientResponse { - fn from(value: ClientRow) -> Self { - Self { - client_id: value.id, - alias: value.alias.into_boxed_str(), - client_type: value.client_type, - allowed_scopes: value - .allowed_scopes - .split_whitespace() - .map(Box::from) - .collect(), - default_scopes: value - .default_scopes - .map(|s| s.split_whitespace().map(Box::from).collect()), - is_trusted: value.is_trusted, - } - } -} - -#[derive(Debug, Clone, Copy, Error)] -#[error("No client with the given client ID was found")] -struct ClientNotFound { - id: Uuid, -} - -impl ResponseError for ClientNotFound { - fn status_code(&self) -> StatusCode { - StatusCode::NOT_FOUND - } -} - -impl ClientNotFound { - fn new(id: Uuid) -> Self { - Self { id } - } -} - -#[get("/{client_id}")] -async fn get_client( - client_id: web::Path, - db: web::Data, -) -> Result { - let db = db.as_ref(); - let id = *client_id; - - let Some(client) = db::get_client_response(db, id).await.unwrap() else { - yeet!(ClientNotFound::new(id)) - }; - - let redirect_uris_link = format!("; rel=\"redirect-uris\""); - let response: ClientResponse = client.into(); - let response = HttpResponse::Ok() - .append_header((header::LINK, redirect_uris_link)) - .json(response); - Ok(response) -} - -#[get("/{client_id}/alias")] -async fn get_client_alias( - client_id: web::Path, - db: web::Data, -) -> Result { - let db = db.as_ref(); - let id = *client_id; - - let Some(alias) = db::get_client_alias(db, id).await.unwrap() else { - yeet!(ClientNotFound::new(id)) - }; - - Ok(HttpResponse::Ok().json(alias)) -} - -#[get("/{client_id}/type")] -async fn get_client_type( - client_id: web::Path, - db: web::Data, -) -> Result { - let db = db.as_ref(); - let id = *client_id; - - let Some(client_type) = db::get_client_type(db, id).await.unwrap() else { - yeet!(ClientNotFound::new(id)) - }; - - Ok(HttpResponse::Ok().json(client_type)) -} - -#[get("/{client_id}/redirect-uris")] -async fn get_client_redirect_uris( - client_id: web::Path, - db: web::Data, -) -> Result { - let db = db.as_ref(); - let id = *client_id; - - if !db::client_id_exists(db, id).await.unwrap() { - yeet!(ClientNotFound::new(id)) - }; - - let redirect_uris = db::get_client_redirect_uris(db, id).await.unwrap(); - - Ok(HttpResponse::Ok().json(redirect_uris)) -} - -#[get("/{client_id}/allowed-scopes")] -async fn get_client_allowed_scopes( - client_id: web::Path, - db: web::Data, -) -> Result { - let db = db.as_ref(); - let id = *client_id; - - let Some(allowed_scopes) = db::get_client_allowed_scopes(db, id).await.unwrap() else { - yeet!(ClientNotFound::new(id)) - }; - - let allowed_scopes = allowed_scopes.split_whitespace().collect::>(); - - Ok(HttpResponse::Ok().json(allowed_scopes)) -} - -#[get("/{client_id}/default-scopes")] -async fn get_client_default_scopes( - client_id: web::Path, - db: web::Data, -) -> Result { - let db = db.as_ref(); - let id = *client_id; - - let Some(default_scopes) = db::get_client_default_scopes(db, id).await.unwrap() else { - yeet!(ClientNotFound::new(id)) - }; - - let default_scopes = default_scopes.map(|scopes| { - scopes - .split_whitespace() - .map(Box::from) - .collect::]>>() - }); - - Ok(HttpResponse::Ok().json(default_scopes)) -} - -#[get("/{client_id}/is-trusted")] -async fn get_client_is_trusted( - client_id: web::Path, - db: web::Data, -) -> Result { - let db = db.as_ref(); - let id = *client_id; - - let Some(is_trusted) = db::is_client_trusted(db, id).await.unwrap() else { - yeet!(ClientNotFound::new(id)) - }; - - Ok(HttpResponse::Ok().json(is_trusted)) -} - -#[derive(Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -struct ClientRequest { - alias: Box, - ty: ClientType, - redirect_uris: Box<[Url]>, - secret: Option>, - allowed_scopes: Box<[Box]>, - default_scopes: Option]>>, - trusted: bool, -} - -#[derive(Debug, Clone, Error)] -#[error("The given client alias is already taken")] -struct AliasTakenError { - alias: Box, -} - -impl ResponseError for AliasTakenError { - fn status_code(&self) -> StatusCode { - StatusCode::CONFLICT - } -} - -impl AliasTakenError { - fn new(alias: &str) -> Self { - Self { - alias: Box::from(alias), - } - } -} - -#[post("")] -async fn create_client( - body: web::Json, - db: web::Data, -) -> Result { - let db = db.get_ref(); - let alias = &body.alias; - - if db::client_alias_exists(db, &alias).await.unwrap() { - yeet!(AliasTakenError::new(&alias).into()); - } - - let id = id::new_id(db, db::client_id_exists).await.unwrap(); - let client = Client::new( - id, - &alias, - body.ty, - body.secret.as_deref(), - body.allowed_scopes.clone(), - body.default_scopes.clone(), - &body.redirect_uris, - body.trusted, - ) - .map_err(|e| e.unwrap())?; - - let transaction = db.begin().await.unwrap(); - db::create_client(transaction, &client).await.unwrap(); - - let response = HttpResponse::Created() - .insert_header((header::LOCATION, format!("clients/{id}"))) - .finish(); - Ok(response) -} - -#[derive(Debug, Clone, Error)] -enum UpdateClientError { - #[error(transparent)] - NotFound(#[from] ClientNotFound), - #[error(transparent)] - ClientError(#[from] CreateClientError), - #[error(transparent)] - AliasTaken(#[from] AliasTakenError), -} - -impl ResponseError for UpdateClientError { - fn status_code(&self) -> StatusCode { - match self { - Self::NotFound(e) => e.status_code(), - Self::ClientError(e) => e.status_code(), - Self::AliasTaken(e) => e.status_code(), - } - } -} - -#[put("/{id}")] -async fn update_client( - id: web::Path, - body: web::Json, - db: web::Data, -) -> Result { - let db = db.get_ref(); - let id = *id; - let alias = &body.alias; - - let Some(old_alias) = db::get_client_alias(db, id).await.unwrap() else { - yeet!(ClientNotFound::new(id).into()) - }; - if old_alias != alias.clone() && db::client_alias_exists(db, &alias).await.unwrap() { - yeet!(AliasTakenError::new(&alias).into()); - } - - let client = Client::new( - id, - &alias, - body.ty, - body.secret.as_deref(), - body.allowed_scopes.clone(), - body.default_scopes.clone(), - &body.redirect_uris, - body.trusted, - ) - .map_err(|e| e.unwrap())?; - - let transaction = db.begin().await.unwrap(); - db::update_client(transaction, &client).await.unwrap(); - - let response = HttpResponse::NoContent().finish(); - Ok(response) -} - -#[put("/{id}/alias")] -async fn update_client_alias( - id: web::Path, - body: web::Json>, - db: web::Data, -) -> Result { - let db = db.get_ref(); - let id = *id; - let alias = body.0; - - let Some(old_alias) = db::get_client_alias(db, id).await.unwrap() else { - yeet!(ClientNotFound::new(id).into()) - }; - if old_alias == alias { - return Ok(HttpResponse::NoContent().finish()); - } - if db::client_alias_exists(db, &alias).await.unwrap() { - yeet!(AliasTakenError::new(&alias).into()); - } - - db::update_client_alias(db, id, &alias).await.unwrap(); - - let response = HttpResponse::NoContent().finish(); - Ok(response) -} - -#[put("/{id}/type")] -async fn update_client_type( - id: web::Path, - body: web::Json, - db: web::Data, -) -> Result { - let db = db.get_ref(); - let id = *id; - let ty = body.0; - - if !db::client_id_exists(db, id).await.unwrap() { - yeet!(ClientNotFound::new(id).into()); - } - - db::update_client_type(db, id, ty).await.unwrap(); - - Ok(HttpResponse::NoContent().finish()) -} - -#[put("/{id}/allowed-scopes")] -async fn update_client_allowed_scopes( - id: web::Path, - body: web::Json]>>, - db: web::Data, -) -> Result { - let db = db.get_ref(); - let id = *id; - let allowed_scopes = body.0.join(" "); - - if !db::client_id_exists(db, id).await.unwrap() { - yeet!(ClientNotFound::new(id).into()); - } - - db::update_client_allowed_scopes(db, id, &allowed_scopes) - .await - .unwrap(); - - Ok(HttpResponse::NoContent().finish()) -} - -#[put("/{id}/default-scopes")] -async fn update_client_default_scopes( - id: web::Path, - body: web::Json]>>>, - db: web::Data, -) -> Result { - let db = db.get_ref(); - let id = *id; - let default_scopes = body.0.map(|s| s.join(" ")); - - if !db::client_id_exists(db, id).await.unwrap() { - yeet!(ClientNotFound::new(id).into()); - } - - db::update_client_default_scopes(db, id, default_scopes) - .await - .unwrap(); - - Ok(HttpResponse::NoContent().finish()) -} - -#[put("/{id}/is-trusted")] -async fn update_client_is_trusted( - id: web::Path, - body: web::Json, - db: web::Data, -) -> Result { - let db = db.get_ref(); - let id = *id; - let is_trusted = *body; - - if !db::client_id_exists(db, id).await.unwrap() { - yeet!(ClientNotFound::new(id).into()); - } - - db::update_client_trusted(db, id, is_trusted).await.unwrap(); - - Ok(HttpResponse::NoContent().finish()) -} - -#[put("/{id}/redirect-uris")] -async fn update_client_redirect_uris( - id: web::Path, - body: web::Json>, - db: web::Data, -) -> Result { - let db = db.get_ref(); - let id = *id; - - for uri in body.0.iter() { - if uri.scheme() != "https" { - yeet!(CreateClientError::NonHttpsUri.into()); - } - - if uri.fragment().is_some() { - yeet!(CreateClientError::UriFragment.into()) - } - } - - if !db::client_id_exists(db, id).await.unwrap() { - yeet!(ClientNotFound::new(id).into()); - } - - let transaction = db.begin().await.unwrap(); - db::update_client_redirect_uris(transaction, id, &body.0) - .await - .unwrap(); - - Ok(HttpResponse::NoContent().finish()) -} - -#[put("{id}/secret")] -async fn update_client_secret( - id: web::Path, - body: web::Json>>, - db: web::Data, -) -> Result { - let db = db.get_ref(); - let id = *id; - - let Some(client_type) = db::get_client_type(db, id).await.unwrap() else { - yeet!(ClientNotFound::new(id).into()) - }; - - if client_type == ClientType::Confidential && body.is_none() { - yeet!(CreateClientError::NoSecret.into()) - } - - let secret = body.0.map(|s| PasswordHash::new(&s).unwrap()); - db::update_client_secret(db, id, secret).await.unwrap(); - - Ok(HttpResponse::NoContent().finish()) -} - -pub fn service() -> Scope { - web::scope("/clients") - .service(get_client) - .service(get_client_alias) - .service(get_client_type) - .service(get_client_allowed_scopes) - .service(get_client_default_scopes) - .service(get_client_redirect_uris) - .service(get_client_is_trusted) - .service(create_client) - .service(update_client) - .service(update_client_alias) - .service(update_client_type) - .service(update_client_allowed_scopes) - .service(update_client_default_scopes) - .service(update_client_redirect_uris) - .service(update_client_secret) -} +use actix_web::http::{header, StatusCode}; +use actix_web::{get, post, put, web, HttpResponse, ResponseError, Scope}; +use raise::yeet; +use serde::{Deserialize, Serialize}; +use sqlx::MySqlPool; +use thiserror::Error; +use url::Url; +use uuid::Uuid; + +use crate::models::client::{Client, ClientType, CreateClientError}; +use crate::services::crypto::PasswordHash; +use crate::services::db::ClientRow; +use crate::services::{db, id}; + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +struct ClientResponse { + client_id: Uuid, + alias: Box, + client_type: ClientType, + allowed_scopes: Box<[Box]>, + default_scopes: Option]>>, + is_trusted: bool, +} + +impl From for ClientResponse { + fn from(value: ClientRow) -> Self { + Self { + client_id: value.id, + alias: value.alias.into_boxed_str(), + client_type: value.client_type, + allowed_scopes: value + .allowed_scopes + .split_whitespace() + .map(Box::from) + .collect(), + default_scopes: value + .default_scopes + .map(|s| s.split_whitespace().map(Box::from).collect()), + is_trusted: value.is_trusted, + } + } +} + +#[derive(Debug, Clone, Copy, Error)] +#[error("No client with the given client ID was found")] +struct ClientNotFound { + id: Uuid, +} + +impl ResponseError for ClientNotFound { + fn status_code(&self) -> StatusCode { + StatusCode::NOT_FOUND + } +} + +impl ClientNotFound { + fn new(id: Uuid) -> Self { + Self { id } + } +} + +#[get("/{client_id}")] +async fn get_client( + client_id: web::Path, + db: web::Data, +) -> Result { + let db = db.as_ref(); + let id = *client_id; + + let Some(client) = db::get_client_response(db, id).await.unwrap() else { + yeet!(ClientNotFound::new(id)) + }; + + let redirect_uris_link = format!("; rel=\"redirect-uris\""); + let response: ClientResponse = client.into(); + let response = HttpResponse::Ok() + .append_header((header::LINK, redirect_uris_link)) + .json(response); + Ok(response) +} + +#[get("/{client_id}/alias")] +async fn get_client_alias( + client_id: web::Path, + db: web::Data, +) -> Result { + let db = db.as_ref(); + let id = *client_id; + + let Some(alias) = db::get_client_alias(db, id).await.unwrap() else { + yeet!(ClientNotFound::new(id)) + }; + + Ok(HttpResponse::Ok().json(alias)) +} + +#[get("/{client_id}/type")] +async fn get_client_type( + client_id: web::Path, + db: web::Data, +) -> Result { + let db = db.as_ref(); + let id = *client_id; + + let Some(client_type) = db::get_client_type(db, id).await.unwrap() else { + yeet!(ClientNotFound::new(id)) + }; + + Ok(HttpResponse::Ok().json(client_type)) +} + +#[get("/{client_id}/redirect-uris")] +async fn get_client_redirect_uris( + client_id: web::Path, + db: web::Data, +) -> Result { + let db = db.as_ref(); + let id = *client_id; + + if !db::client_id_exists(db, id).await.unwrap() { + yeet!(ClientNotFound::new(id)) + }; + + let redirect_uris = db::get_client_redirect_uris(db, id).await.unwrap(); + + Ok(HttpResponse::Ok().json(redirect_uris)) +} + +#[get("/{client_id}/allowed-scopes")] +async fn get_client_allowed_scopes( + client_id: web::Path, + db: web::Data, +) -> Result { + let db = db.as_ref(); + let id = *client_id; + + let Some(allowed_scopes) = db::get_client_allowed_scopes(db, id).await.unwrap() else { + yeet!(ClientNotFound::new(id)) + }; + + let allowed_scopes = allowed_scopes.split_whitespace().collect::>(); + + Ok(HttpResponse::Ok().json(allowed_scopes)) +} + +#[get("/{client_id}/default-scopes")] +async fn get_client_default_scopes( + client_id: web::Path, + db: web::Data, +) -> Result { + let db = db.as_ref(); + let id = *client_id; + + let Some(default_scopes) = db::get_client_default_scopes(db, id).await.unwrap() else { + yeet!(ClientNotFound::new(id)) + }; + + let default_scopes = default_scopes.map(|scopes| { + scopes + .split_whitespace() + .map(Box::from) + .collect::]>>() + }); + + Ok(HttpResponse::Ok().json(default_scopes)) +} + +#[get("/{client_id}/is-trusted")] +async fn get_client_is_trusted( + client_id: web::Path, + db: web::Data, +) -> Result { + let db = db.as_ref(); + let id = *client_id; + + let Some(is_trusted) = db::is_client_trusted(db, id).await.unwrap() else { + yeet!(ClientNotFound::new(id)) + }; + + Ok(HttpResponse::Ok().json(is_trusted)) +} + +#[derive(Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +struct ClientRequest { + alias: Box, + ty: ClientType, + redirect_uris: Box<[Url]>, + secret: Option>, + allowed_scopes: Box<[Box]>, + default_scopes: Option]>>, + trusted: bool, +} + +#[derive(Debug, Clone, Error)] +#[error("The given client alias is already taken")] +struct AliasTakenError { + alias: Box, +} + +impl ResponseError for AliasTakenError { + fn status_code(&self) -> StatusCode { + StatusCode::CONFLICT + } +} + +impl AliasTakenError { + fn new(alias: &str) -> Self { + Self { + alias: Box::from(alias), + } + } +} + +#[post("")] +async fn create_client( + body: web::Json, + db: web::Data, +) -> Result { + let db = db.get_ref(); + let alias = &body.alias; + + if db::client_alias_exists(db, &alias).await.unwrap() { + yeet!(AliasTakenError::new(&alias).into()); + } + + let id = id::new_id(db, db::client_id_exists).await.unwrap(); + let client = Client::new( + id, + &alias, + body.ty, + body.secret.as_deref(), + body.allowed_scopes.clone(), + body.default_scopes.clone(), + &body.redirect_uris, + body.trusted, + ) + .map_err(|e| e.unwrap())?; + + let transaction = db.begin().await.unwrap(); + db::create_client(transaction, &client).await.unwrap(); + + let response = HttpResponse::Created() + .insert_header((header::LOCATION, format!("clients/{id}"))) + .finish(); + Ok(response) +} + +#[derive(Debug, Clone, Error)] +enum UpdateClientError { + #[error(transparent)] + NotFound(#[from] ClientNotFound), + #[error(transparent)] + ClientError(#[from] CreateClientError), + #[error(transparent)] + AliasTaken(#[from] AliasTakenError), +} + +impl ResponseError for UpdateClientError { + fn status_code(&self) -> StatusCode { + match self { + Self::NotFound(e) => e.status_code(), + Self::ClientError(e) => e.status_code(), + Self::AliasTaken(e) => e.status_code(), + } + } +} + +#[put("/{id}")] +async fn update_client( + id: web::Path, + body: web::Json, + db: web::Data, +) -> Result { + let db = db.get_ref(); + let id = *id; + let alias = &body.alias; + + let Some(old_alias) = db::get_client_alias(db, id).await.unwrap() else { + yeet!(ClientNotFound::new(id).into()) + }; + if old_alias != alias.clone() && db::client_alias_exists(db, &alias).await.unwrap() { + yeet!(AliasTakenError::new(&alias).into()); + } + + let client = Client::new( + id, + &alias, + body.ty, + body.secret.as_deref(), + body.allowed_scopes.clone(), + body.default_scopes.clone(), + &body.redirect_uris, + body.trusted, + ) + .map_err(|e| e.unwrap())?; + + let transaction = db.begin().await.unwrap(); + db::update_client(transaction, &client).await.unwrap(); + + let response = HttpResponse::NoContent().finish(); + Ok(response) +} + +#[put("/{id}/alias")] +async fn update_client_alias( + id: web::Path, + body: web::Json>, + db: web::Data, +) -> Result { + let db = db.get_ref(); + let id = *id; + let alias = body.0; + + let Some(old_alias) = db::get_client_alias(db, id).await.unwrap() else { + yeet!(ClientNotFound::new(id).into()) + }; + if old_alias == alias { + return Ok(HttpResponse::NoContent().finish()); + } + if db::client_alias_exists(db, &alias).await.unwrap() { + yeet!(AliasTakenError::new(&alias).into()); + } + + db::update_client_alias(db, id, &alias).await.unwrap(); + + let response = HttpResponse::NoContent().finish(); + Ok(response) +} + +#[put("/{id}/type")] +async fn update_client_type( + id: web::Path, + body: web::Json, + db: web::Data, +) -> Result { + let db = db.get_ref(); + let id = *id; + let ty = body.0; + + if !db::client_id_exists(db, id).await.unwrap() { + yeet!(ClientNotFound::new(id).into()); + } + + db::update_client_type(db, id, ty).await.unwrap(); + + Ok(HttpResponse::NoContent().finish()) +} + +#[put("/{id}/allowed-scopes")] +async fn update_client_allowed_scopes( + id: web::Path, + body: web::Json]>>, + db: web::Data, +) -> Result { + let db = db.get_ref(); + let id = *id; + let allowed_scopes = body.0.join(" "); + + if !db::client_id_exists(db, id).await.unwrap() { + yeet!(ClientNotFound::new(id).into()); + } + + db::update_client_allowed_scopes(db, id, &allowed_scopes) + .await + .unwrap(); + + Ok(HttpResponse::NoContent().finish()) +} + +#[put("/{id}/default-scopes")] +async fn update_client_default_scopes( + id: web::Path, + body: web::Json]>>>, + db: web::Data, +) -> Result { + let db = db.get_ref(); + let id = *id; + let default_scopes = body.0.map(|s| s.join(" ")); + + if !db::client_id_exists(db, id).await.unwrap() { + yeet!(ClientNotFound::new(id).into()); + } + + db::update_client_default_scopes(db, id, default_scopes) + .await + .unwrap(); + + Ok(HttpResponse::NoContent().finish()) +} + +#[put("/{id}/is-trusted")] +async fn update_client_is_trusted( + id: web::Path, + body: web::Json, + db: web::Data, +) -> Result { + let db = db.get_ref(); + let id = *id; + let is_trusted = *body; + + if !db::client_id_exists(db, id).await.unwrap() { + yeet!(ClientNotFound::new(id).into()); + } + + db::update_client_trusted(db, id, is_trusted).await.unwrap(); + + Ok(HttpResponse::NoContent().finish()) +} + +#[put("/{id}/redirect-uris")] +async fn update_client_redirect_uris( + id: web::Path, + body: web::Json>, + db: web::Data, +) -> Result { + let db = db.get_ref(); + let id = *id; + + for uri in body.0.iter() { + if uri.scheme() != "https" { + yeet!(CreateClientError::NonHttpsUri.into()); + } + + if uri.fragment().is_some() { + yeet!(CreateClientError::UriFragment.into()) + } + } + + if !db::client_id_exists(db, id).await.unwrap() { + yeet!(ClientNotFound::new(id).into()); + } + + let transaction = db.begin().await.unwrap(); + db::update_client_redirect_uris(transaction, id, &body.0) + .await + .unwrap(); + + Ok(HttpResponse::NoContent().finish()) +} + +#[put("{id}/secret")] +async fn update_client_secret( + id: web::Path, + body: web::Json>>, + db: web::Data, +) -> Result { + let db = db.get_ref(); + let id = *id; + + let Some(client_type) = db::get_client_type(db, id).await.unwrap() else { + yeet!(ClientNotFound::new(id).into()) + }; + + if client_type == ClientType::Confidential && body.is_none() { + yeet!(CreateClientError::NoSecret.into()) + } + + let secret = body.0.map(|s| PasswordHash::new(&s).unwrap()); + db::update_client_secret(db, id, secret).await.unwrap(); + + Ok(HttpResponse::NoContent().finish()) +} + +pub fn service() -> Scope { + web::scope("/clients") + .service(get_client) + .service(get_client_alias) + .service(get_client_type) + .service(get_client_allowed_scopes) + .service(get_client_default_scopes) + .service(get_client_redirect_uris) + .service(get_client_is_trusted) + .service(create_client) + .service(update_client) + .service(update_client_alias) + .service(update_client_type) + .service(update_client_allowed_scopes) + .service(update_client_default_scopes) + .service(update_client_redirect_uris) + .service(update_client_secret) +} diff --git a/src/api/liveops.rs b/src/api/liveops.rs index d4bf129..2caf6e3 100644 --- a/src/api/liveops.rs +++ b/src/api/liveops.rs @@ -1,11 +1,11 @@ -use actix_web::{get, web, HttpResponse, Scope}; - -/// Simple ping -#[get("/ping")] -async fn ping() -> HttpResponse { - HttpResponse::Ok().finish() -} - -pub fn service() -> Scope { - web::scope("/liveops").service(ping) -} +use actix_web::{get, web, HttpResponse, Scope}; + +/// Simple ping +#[get("/ping")] +async fn ping() -> HttpResponse { + HttpResponse::Ok().finish() +} + +pub fn service() -> Scope { + web::scope("/liveops").service(ping) +} diff --git a/src/api/mod.rs b/src/api/mod.rs index 0ab4037..9059e71 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,13 +1,13 @@ -mod clients; -mod liveops; -mod oauth; -mod ops; -mod users; - -pub use clients::service as clients; -pub use liveops::service as liveops; -pub use oauth::service as oauth; -pub use ops::service as ops; -pub use users::service as users; - -pub use oauth::AuthorizationParameters; +mod clients; +mod liveops; +mod oauth; +mod ops; +mod users; + +pub use clients::service as clients; +pub use liveops::service as liveops; +pub use oauth::service as oauth; +pub use ops::service as ops; +pub use users::service as users; + +pub use oauth::AuthorizationParameters; diff --git a/src/api/oauth.rs b/src/api/oauth.rs index f1aa012..3422d2f 100644 --- a/src/api/oauth.rs +++ b/src/api/oauth.rs @@ -1,926 +1,926 @@ -use std::ops::Deref; -use std::str::FromStr; - -use actix_web::http::{header, StatusCode}; -use actix_web::{ - get, post, web, HttpRequest, HttpResponse, HttpResponseBuilder, ResponseError, Scope, -}; -use chrono::Duration; -use exun::{Expect, RawUnexpected, ResultErrorExt, UnexpectedError}; -use raise::yeet; -use serde::{Deserialize, Serialize}; -use sqlx::MySqlPool; -use tera::Tera; -use thiserror::Error; -use unic_langid::subtags::Language; -use url::Url; -use uuid::Uuid; - -use crate::models::client::ClientType; -use crate::resources::{languages, templates}; -use crate::scopes; -use crate::services::jwt::VerifyJwtError; -use crate::services::{authorization, config, db, jwt}; - -const REALLY_BAD_ERROR_PAGE: &str = "Internal Server ErrorInternal Server Error"; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -enum ResponseType { - Code, - Token, - #[serde(other)] - Unsupported, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AuthorizationParameters { - response_type: ResponseType, - client_id: Box, - redirect_uri: Option, - scope: Option>, - state: Option>, -} - -#[derive(Clone, Deserialize)] -struct AuthorizeCredentials { - username: Box, - password: Box, -} - -#[derive(Clone, Serialize)] -struct AuthCodeResponse { - code: Box, - state: Option>, -} - -#[derive(Clone, Serialize)] -struct AuthTokenResponse { - access_token: Box, - token_type: &'static str, - expires_in: i64, - scope: Box, - state: Option>, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)] -#[serde(rename_all = "camelCase")] -enum AuthorizeErrorType { - InvalidRequest, - UnauthorizedClient, - AccessDenied, - UnsupportedResponseType, - InvalidScope, - ServerError, - TemporarilyUnavailable, -} - -#[derive(Debug, Clone, Error, Serialize)] -#[error("{error_description}")] -struct AuthorizeError { - error: AuthorizeErrorType, - error_description: Box, - // TODO error uri - state: Option>, - #[serde(skip)] - redirect_uri: Url, -} - -impl AuthorizeError { - fn no_scope(redirect_uri: Url, state: Option>) -> Self { - Self { - error: AuthorizeErrorType::InvalidScope, - error_description: Box::from( - "No scope was provided, and the client does not have a default scope", - ), - state, - redirect_uri, - } - } - - fn unsupported_response_type(redirect_uri: Url, state: Option>) -> Self { - Self { - error: AuthorizeErrorType::UnsupportedResponseType, - error_description: Box::from("The given response type is not supported"), - state, - redirect_uri, - } - } - - fn invalid_scope(redirect_uri: Url, state: Option>) -> Self { - Self { - error: AuthorizeErrorType::InvalidScope, - error_description: Box::from("The given scope exceeds what the client is allowed"), - state, - redirect_uri, - } - } - - fn internal_server_error(redirect_uri: Url, state: Option>) -> Self { - Self { - error: AuthorizeErrorType::ServerError, - error_description: "An unexpected error occurred".into(), - state, - redirect_uri, - } - } -} - -impl ResponseError for AuthorizeError { - fn error_response(&self) -> HttpResponse { - let query = Some(serde_urlencoded::to_string(self).unwrap()); - let query = query.as_deref(); - let mut url = self.redirect_uri.clone(); - url.set_query(query); - - HttpResponse::Found() - .insert_header((header::LOCATION, url.as_str())) - .finish() - } -} - -fn error_page( - tera: &Tera, - translations: &languages::Translations, - error: templates::ErrorPage, -) -> Result { - // TODO find a better way of doing languages - let language = Language::from_str("en").unwrap(); - let translations = translations.clone(); - let page = templates::error_page(&tera, language, translations, error)?; - Ok(page) -} - -async fn get_redirect_uri( - redirect_uri: &Option, - db: &MySqlPool, - client_id: Uuid, -) -> Result> { - if let Some(uri) = &redirect_uri { - let redirect_uri = uri.clone(); - if !db::client_has_redirect_uri(db, client_id, &redirect_uri) - .await - .map_err(|e| UnexpectedError::from(e)) - .unexpect()? - { - yeet!(Expect::Expected(templates::ErrorPage::InvalidRedirectUri)); - } - - Ok(redirect_uri) - } else { - let redirect_uris = db::get_client_redirect_uris(db, client_id) - .await - .map_err(|e| UnexpectedError::from(e)) - .unexpect()?; - if redirect_uris.len() != 1 { - yeet!(Expect::Expected(templates::ErrorPage::MissingRedirectUri)); - } - - Ok(redirect_uris.get(0).unwrap().clone()) - } -} - -async fn get_scope( - scope: &Option>, - db: &MySqlPool, - client_id: Uuid, - redirect_uri: &Url, - state: &Option>, -) -> Result, Expect> { - let scope = if let Some(scope) = &scope { - scope.clone() - } else { - let default_scopes = db::get_client_default_scopes(db, client_id) - .await - .unwrap() - .unwrap(); - let Some(scope) = default_scopes else { - yeet!(AuthorizeError::no_scope(redirect_uri.clone(), state.clone()).into()) - }; - scope - }; - - // verify scope is valid - let allowed_scopes = db::get_client_allowed_scopes(db, client_id) - .await - .unwrap() - .unwrap(); - if !scopes::is_subset_of(&scope, &allowed_scopes) { - yeet!(AuthorizeError::invalid_scope(redirect_uri.clone(), state.clone()).into()); - } - - Ok(scope) -} - -async fn authenticate_user( - db: &MySqlPool, - username: &str, - password: &str, -) -> Result, RawUnexpected> { - let Some(user) = db::get_user_by_username(db, username).await? else { - return Ok(None); - }; - - if user.check_password(password)? { - Ok(Some(user.id)) - } else { - Ok(None) - } -} - -#[post("/authorize")] -async fn authorize( - db: web::Data, - req: web::Query, - credentials: web::Json, - tera: web::Data, - translations: web::Data, -) -> Result { - // TODO protect against brute force attacks - let db = db.get_ref(); - let Ok(client_id) = db::get_client_id_by_alias(db, &req.client_id).await else { - let page = error_page(&tera, &translations, templates::ErrorPage::InternalServerError).unwrap_or_else(|_| String::from(REALLY_BAD_ERROR_PAGE)); - return Ok(HttpResponse::InternalServerError().content_type("text/html").body(page)); - }; - let Some(client_id) = client_id else { - let page = error_page(&tera, &translations, templates::ErrorPage::ClientNotFound).unwrap_or_else(|_| String::from(REALLY_BAD_ERROR_PAGE)); - return Ok(HttpResponse::NotFound().content_type("text/html").body(page)); - }; - let Ok(config) = config::get_config() else { - let page = error_page(&tera, &translations, templates::ErrorPage::InternalServerError).unwrap_or_else(|_| String::from(REALLY_BAD_ERROR_PAGE)); - return Ok(HttpResponse::InternalServerError().content_type("text/html").body(page)); - }; - - let self_id = config.url; - let state = req.state.clone(); - - // get redirect uri - let mut redirect_uri = match get_redirect_uri(&req.redirect_uri, db, client_id).await { - Ok(uri) => uri, - Err(e) => { - let e = e - .expected() - .unwrap_or(templates::ErrorPage::InternalServerError); - let page = error_page(&tera, &translations, e) - .unwrap_or_else(|_| String::from(REALLY_BAD_ERROR_PAGE)); - return Ok(HttpResponse::BadRequest() - .content_type("text/html") - .body(page)); - } - }; - - // authenticate user - let Some(user_id) = authenticate_user(db, &credentials.username, &credentials.password) - .await - .unwrap() else - { - let language = Language::from_str("en").unwrap(); - let translations = translations.get_ref().clone(); - let page = templates::login_error_page(&tera, &req, language, translations).unwrap_or_else(|_| String::from(REALLY_BAD_ERROR_PAGE)); - return Ok(HttpResponse::Ok().content_type("text/html").body(page)); - }; - - let internal_server_error = - AuthorizeError::internal_server_error(redirect_uri.clone(), state.clone()); - - // get scope - let scope = match get_scope(&req.scope, db, client_id, &redirect_uri, &state).await { - Ok(scope) => scope, - Err(e) => { - let e = e.expected().unwrap_or(internal_server_error); - return Err(e); - } - }; - - match req.response_type { - ResponseType::Code => { - // create auth code - let code = - jwt::Claims::auth_code(db, self_id, client_id, user_id, &scope, &redirect_uri) - .await - .map_err(|_| internal_server_error.clone())?; - let code = code.to_jwt().map_err(|_| internal_server_error.clone())?; - - let response = AuthCodeResponse { code, state }; - let query = - Some(serde_urlencoded::to_string(response).map_err(|_| internal_server_error)?); - let query = query.as_deref(); - redirect_uri.set_query(query); - - Ok(HttpResponse::Found() - .append_header((header::LOCATION, redirect_uri.as_str())) - .finish()) - } - ResponseType::Token => { - // create access token - let duration = Duration::hours(1); - let access_token = - jwt::Claims::access_token(db, None, self_id, client_id, user_id, duration, &scope) - .await - .map_err(|_| internal_server_error.clone())?; - - let access_token = access_token - .to_jwt() - .map_err(|_| internal_server_error.clone())?; - let expires_in = duration.num_seconds(); - let token_type = "bearer"; - let response = AuthTokenResponse { - access_token, - expires_in, - token_type, - scope, - state, - }; - - let fragment = Some( - serde_urlencoded::to_string(response).map_err(|_| internal_server_error.clone())?, - ); - let fragment = fragment.as_deref(); - redirect_uri.set_fragment(fragment); - - Ok(HttpResponse::Found() - .append_header((header::LOCATION, redirect_uri.as_str())) - .finish()) - } - _ => Err(AuthorizeError::invalid_scope(redirect_uri, state)), - } -} - -#[get("/authorize")] -async fn authorize_page( - db: web::Data, - tera: web::Data, - translations: web::Data, - request: HttpRequest, -) -> Result { - let Ok(language) = Language::from_str("en") else { - let page = String::from(REALLY_BAD_ERROR_PAGE); - return Ok(HttpResponse::InternalServerError() - .content_type("text/html") - .body(page)); - }; - let translations = translations.get_ref().clone(); - - let params = request.query_string(); - let params = serde_urlencoded::from_str::(params); - let Ok(params) = params else { - let page = error_page( - &tera, - &translations, - templates::ErrorPage::InvalidRequest, - ) - .unwrap_or_else(|_| String::from(REALLY_BAD_ERROR_PAGE)); - return Ok(HttpResponse::BadRequest() - .content_type("text/html") - .body(page)); - }; - - let db = db.get_ref(); - let Ok(client_id) = db::get_client_id_by_alias(db, ¶ms.client_id).await else { - let page = templates::error_page( - &tera, - language, - translations, - templates::ErrorPage::InternalServerError, - ) - .unwrap_or_else(|_| String::from(REALLY_BAD_ERROR_PAGE)); - return Ok(HttpResponse::InternalServerError() - .content_type("text/html") - .body(page)); - }; - let Some(client_id) = client_id else { - let page = templates::error_page( - &tera, - language, - translations, - templates::ErrorPage::ClientNotFound, - ) - .unwrap_or_else(|_| String::from(REALLY_BAD_ERROR_PAGE)); - return Ok(HttpResponse::NotFound() - .content_type("text/html") - .body(page)); - }; - - // verify redirect uri - let redirect_uri = match get_redirect_uri(¶ms.redirect_uri, db, client_id).await { - Ok(uri) => uri, - Err(e) => { - let e = e - .expected() - .unwrap_or(templates::ErrorPage::InternalServerError); - let page = error_page(&tera, &translations, e) - .unwrap_or_else(|_| String::from(REALLY_BAD_ERROR_PAGE)); - return Ok(HttpResponse::BadRequest() - .content_type("text/html") - .body(page)); - } - }; - - let state = ¶ms.state; - let internal_server_error = - AuthorizeError::internal_server_error(redirect_uri.clone(), state.clone()); - - // verify scope - let _ = match get_scope(¶ms.scope, db, client_id, &redirect_uri, ¶ms.state).await { - Ok(scope) => scope, - Err(e) => { - let e = e.expected().unwrap_or(internal_server_error); - return Err(e); - } - }; - - // verify response type - if params.response_type == ResponseType::Unsupported { - return Err(AuthorizeError::unsupported_response_type( - redirect_uri, - params.state, - )); - } - - // TODO find a better way of doing languages - let language = Language::from_str("en").unwrap(); - let page = templates::login_page(&tera, ¶ms, language, translations).unwrap(); - Ok(HttpResponse::Ok().content_type("text/html").body(page)) -} - -#[derive(Clone, Deserialize)] -#[serde(tag = "grant_type")] -#[serde(rename_all = "snake_case")] -enum GrantType { - AuthorizationCode { - code: Box, - redirect_uri: Url, - #[serde(rename = "client_id")] - client_alias: Box, - }, - Password { - username: Box, - password: Box, - scope: Option>, - }, - ClientCredentials { - scope: Option>, - }, - RefreshToken { - refresh_token: Box, - scope: Option>, - }, - #[serde(other)] - Unsupported, -} - -#[derive(Clone, Deserialize)] -struct TokenRequest { - #[serde(flatten)] - grant_type: GrantType, - // TODO support optional client credentials in here -} - -#[derive(Clone, Serialize)] -struct TokenResponse { - access_token: Box, - token_type: Box, - expires_in: i64, - refresh_token: Option>, - scope: Box, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "snake_case")] -enum TokenErrorType { - InvalidRequest, - InvalidClient, - InvalidGrant, - UnauthorizedClient, - UnsupportedGrantType, - InvalidScope, -} - -#[derive(Debug, Clone, Error, Serialize)] -#[error("{error_description}")] -struct TokenError { - #[serde(skip)] - status_code: StatusCode, - error: TokenErrorType, - error_description: Box, - // TODO error uri -} - -impl TokenError { - fn invalid_request() -> Self { - // TODO make this description better, and all the other ones while you're at it - Self { - status_code: StatusCode::BAD_REQUEST, - error: TokenErrorType::InvalidRequest, - error_description: "Invalid request".into(), - } - } - - fn unsupported_grant_type() -> Self { - Self { - status_code: StatusCode::BAD_REQUEST, - error: TokenErrorType::UnsupportedGrantType, - error_description: "The given grant type is not supported".into(), - } - } - - fn bad_auth_code(error: VerifyJwtError) -> Self { - Self { - status_code: StatusCode::BAD_REQUEST, - error: TokenErrorType::InvalidGrant, - error_description: error.to_string().into_boxed_str(), - } - } - - fn no_authorization() -> Self { - Self { - status_code: StatusCode::UNAUTHORIZED, - error: TokenErrorType::InvalidClient, - error_description: Box::from( - "Client credentials must be provided in the HTTP Authorization header", - ), - } - } - - fn client_not_found(alias: &str) -> Self { - Self { - status_code: StatusCode::UNAUTHORIZED, - error: TokenErrorType::InvalidClient, - error_description: format!("No client with the client id: {alias} was found") - .into_boxed_str(), - } - } - - fn mismatch_client_id() -> Self { - Self { - status_code: StatusCode::UNAUTHORIZED, - error: TokenErrorType::InvalidClient, - error_description: Box::from("The client ID in the Authorization header is not the same as the client ID in the request body"), - } - } - - fn incorrect_client_secret() -> Self { - Self { - status_code: StatusCode::UNAUTHORIZED, - error: TokenErrorType::InvalidClient, - error_description: "The client secret is incorrect".into(), - } - } - - fn client_not_confidential(alias: &str) -> Self { - Self { - status_code: StatusCode::BAD_REQUEST, - error: TokenErrorType::UnauthorizedClient, - error_description: format!("Only a confidential client may be used with this endpoint. The {alias} client is a public client.") - .into_boxed_str(), - } - } - - fn no_scope() -> Self { - Self { - status_code: StatusCode::BAD_REQUEST, - error: TokenErrorType::InvalidScope, - error_description: Box::from( - "No scope was provided, and the client doesn't have a default scope", - ), - } - } - - fn excessive_scope() -> Self { - Self { - status_code: StatusCode::BAD_REQUEST, - error: TokenErrorType::InvalidScope, - error_description: Box::from( - "The given scope exceeds what the client is allowed to have", - ), - } - } - - fn bad_refresh_token(err: VerifyJwtError) -> Self { - Self { - status_code: StatusCode::BAD_REQUEST, - error: TokenErrorType::InvalidGrant, - error_description: err.to_string().into_boxed_str(), - } - } - - fn untrusted_client() -> Self { - Self { - status_code: StatusCode::UNAUTHORIZED, - error: TokenErrorType::InvalidClient, - error_description: "Only trusted clients may use this grant".into(), - } - } - - fn incorrect_user_credentials() -> Self { - Self { - status_code: StatusCode::BAD_REQUEST, - error: TokenErrorType::InvalidRequest, - error_description: "The given credentials are incorrect".into(), - } - } -} - -impl ResponseError for TokenError { - fn error_response(&self) -> HttpResponse { - let cache_control = header::CacheControl(vec![header::CacheDirective::NoStore]); - - let mut builder = HttpResponseBuilder::new(self.status_code); - - if self.status_code.as_u16() == 401 { - builder.insert_header((header::WWW_AUTHENTICATE, "Basic charset=\"UTF-8\"")); - } - - builder - .insert_header(cache_control) - .insert_header((header::PRAGMA, "no-cache")) - .json(self.clone()) - } -} - -#[post("/token")] -async fn token( - db: web::Data, - req: web::Bytes, - authorization: Option>, -) -> HttpResponse { - // TODO protect against brute force attacks - let db = db.get_ref(); - let request = serde_json::from_slice::(&req); - let Ok(request) = request else { - return TokenError::invalid_request().error_response(); - }; - let config = config::get_config().unwrap(); - - let self_id = config.url; - let duration = Duration::hours(1); - let token_type = Box::from("bearer"); - let cache_control = header::CacheControl(vec![header::CacheDirective::NoStore]); - - match request.grant_type { - GrantType::AuthorizationCode { - code, - redirect_uri, - client_alias, - } => { - let Some(client_id) = db::get_client_id_by_alias(db, &client_alias).await.unwrap() else { - return TokenError::client_not_found(&client_alias).error_response(); - }; - - // validate auth code - let claims = - match jwt::verify_auth_code(db, &code, &self_id, client_id, redirect_uri).await { - Ok(claims) => claims, - Err(err) => { - let err = err.unwrap(); - return TokenError::bad_auth_code(err).error_response(); - } - }; - - // verify client, if the client has credentials - if let Some(hash) = db::get_client_secret(db, client_id).await.unwrap() { - let Some(authorization) = authorization else { - return TokenError::no_authorization().error_response(); - }; - - if authorization.username() != client_alias.deref() { - return TokenError::mismatch_client_id().error_response(); - } - if !hash.check_password(authorization.password()).unwrap() { - return TokenError::incorrect_client_secret().error_response(); - } - } - - let access_token = jwt::Claims::access_token( - db, - Some(claims.id()), - self_id, - client_id, - claims.subject(), - duration, - claims.scopes(), - ) - .await - .unwrap(); - - let expires_in = access_token.expires_in(); - let refresh_token = jwt::Claims::refresh_token(db, &access_token).await.unwrap(); - let scope = access_token.scopes().into(); - - let access_token = access_token.to_jwt().unwrap(); - let refresh_token = Some(refresh_token.to_jwt().unwrap()); - - let response = TokenResponse { - access_token, - token_type, - expires_in, - refresh_token, - scope, - }; - HttpResponse::Ok() - .insert_header(cache_control) - .insert_header((header::PRAGMA, "no-cache")) - .json(response) - } - GrantType::Password { - username, - password, - scope, - } => { - let Some(authorization) = authorization else { - return TokenError::no_authorization().error_response(); - }; - let client_alias = authorization.username(); - let Some(client_id) = db::get_client_id_by_alias(db, client_alias).await.unwrap() else { - return TokenError::client_not_found(client_alias).error_response(); - }; - - let trusted = db::is_client_trusted(db, client_id).await.unwrap().unwrap(); - if !trusted { - return TokenError::untrusted_client().error_response(); - } - - // verify client - let hash = db::get_client_secret(db, client_id).await.unwrap().unwrap(); - if !hash.check_password(authorization.password()).unwrap() { - return TokenError::incorrect_client_secret().error_response(); - } - - // authenticate user - let Some(user_id) = authenticate_user(db, &username, &password).await.unwrap() else { - return TokenError::incorrect_user_credentials().error_response(); - }; - - // verify scope - let allowed_scopes = db::get_client_allowed_scopes(db, client_id) - .await - .unwrap() - .unwrap(); - let scope = if let Some(scope) = &scope { - scope.clone() - } else { - let default_scopes = db::get_client_default_scopes(db, client_id) - .await - .unwrap() - .unwrap(); - let Some(scope) = default_scopes else { - return TokenError::no_scope().error_response(); - }; - scope - }; - if !scopes::is_subset_of(&scope, &allowed_scopes) { - return TokenError::excessive_scope().error_response(); - } - - let access_token = - jwt::Claims::access_token(db, None, self_id, client_id, user_id, duration, &scope) - .await - .unwrap(); - let refresh_token = jwt::Claims::refresh_token(db, &access_token).await.unwrap(); - - let expires_in = access_token.expires_in(); - let scope = access_token.scopes().into(); - let access_token = access_token.to_jwt().unwrap(); - let refresh_token = Some(refresh_token.to_jwt().unwrap()); - - let response = TokenResponse { - access_token, - token_type, - expires_in, - refresh_token, - scope, - }; - HttpResponse::Ok() - .insert_header(cache_control) - .insert_header((header::PRAGMA, "no-cache")) - .json(response) - } - GrantType::ClientCredentials { scope } => { - let Some(authorization) = authorization else { - return TokenError::no_authorization().error_response(); - }; - let client_alias = authorization.username(); - let Some(client_id) = db::get_client_id_by_alias(db, client_alias).await.unwrap() else { - return TokenError::client_not_found(client_alias).error_response(); - }; - - let ty = db::get_client_type(db, client_id).await.unwrap().unwrap(); - if ty != ClientType::Confidential { - return TokenError::client_not_confidential(client_alias).error_response(); - } - - // verify client - let hash = db::get_client_secret(db, client_id).await.unwrap().unwrap(); - if !hash.check_password(authorization.password()).unwrap() { - return TokenError::incorrect_client_secret().error_response(); - } - - // verify scope - let allowed_scopes = db::get_client_allowed_scopes(db, client_id) - .await - .unwrap() - .unwrap(); - let scope = if let Some(scope) = &scope { - scope.clone() - } else { - let default_scopes = db::get_client_default_scopes(db, client_id) - .await - .unwrap() - .unwrap(); - let Some(scope) = default_scopes else { - return TokenError::no_scope().error_response(); - }; - scope - }; - if !scopes::is_subset_of(&scope, &allowed_scopes) { - return TokenError::excessive_scope().error_response(); - } - - let access_token = jwt::Claims::access_token( - db, None, self_id, client_id, client_id, duration, &scope, - ) - .await - .unwrap(); - - let expires_in = access_token.expires_in(); - let scope = access_token.scopes().into(); - let access_token = access_token.to_jwt().unwrap(); - - let response = TokenResponse { - access_token, - token_type, - expires_in, - refresh_token: None, - scope, - }; - HttpResponse::Ok() - .insert_header(cache_control) - .insert_header((header::PRAGMA, "no-cache")) - .json(response) - } - GrantType::RefreshToken { - refresh_token, - scope, - } => { - let client_id: Option; - if let Some(authorization) = authorization { - let client_alias = authorization.username(); - let Some(id) = db::get_client_id_by_alias(db, client_alias).await.unwrap() else { - return TokenError::client_not_found(client_alias).error_response(); - }; - client_id = Some(id); - } else { - client_id = None; - } - - let claims = - match jwt::verify_refresh_token(db, &refresh_token, &self_id, client_id).await { - Ok(claims) => claims, - Err(e) => { - let e = e.unwrap(); - return TokenError::bad_refresh_token(e).error_response(); - } - }; - - let scope = if let Some(scope) = scope { - if !scopes::is_subset_of(&scope, claims.scopes()) { - return TokenError::excessive_scope().error_response(); - } - - scope - } else { - claims.scopes().into() - }; - - let exp_time = Duration::hours(1); - let access_token = jwt::Claims::refreshed_access_token(db, &claims, exp_time) - .await - .unwrap(); - let refresh_token = jwt::Claims::refresh_token(db, &claims).await.unwrap(); - - let access_token = access_token.to_jwt().unwrap(); - let refresh_token = Some(refresh_token.to_jwt().unwrap()); - let expires_in = exp_time.num_seconds(); - - let response = TokenResponse { - access_token, - token_type, - expires_in, - refresh_token, - scope, - }; - HttpResponse::Ok() - .insert_header(cache_control) - .insert_header((header::PRAGMA, "no-cache")) - .json(response) - } - _ => TokenError::unsupported_grant_type().error_response(), - } -} - -pub fn service() -> Scope { - web::scope("/oauth") - .service(authorize_page) - .service(authorize) - .service(token) -} +use std::ops::Deref; +use std::str::FromStr; + +use actix_web::http::{header, StatusCode}; +use actix_web::{ + get, post, web, HttpRequest, HttpResponse, HttpResponseBuilder, ResponseError, Scope, +}; +use chrono::Duration; +use exun::{Expect, RawUnexpected, ResultErrorExt, UnexpectedError}; +use raise::yeet; +use serde::{Deserialize, Serialize}; +use sqlx::MySqlPool; +use tera::Tera; +use thiserror::Error; +use unic_langid::subtags::Language; +use url::Url; +use uuid::Uuid; + +use crate::models::client::ClientType; +use crate::resources::{languages, templates}; +use crate::scopes; +use crate::services::jwt::VerifyJwtError; +use crate::services::{authorization, config, db, jwt}; + +const REALLY_BAD_ERROR_PAGE: &str = "Internal Server ErrorInternal Server Error"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +enum ResponseType { + Code, + Token, + #[serde(other)] + Unsupported, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AuthorizationParameters { + response_type: ResponseType, + client_id: Box, + redirect_uri: Option, + scope: Option>, + state: Option>, +} + +#[derive(Clone, Deserialize)] +struct AuthorizeCredentials { + username: Box, + password: Box, +} + +#[derive(Clone, Serialize)] +struct AuthCodeResponse { + code: Box, + state: Option>, +} + +#[derive(Clone, Serialize)] +struct AuthTokenResponse { + access_token: Box, + token_type: &'static str, + expires_in: i64, + scope: Box, + state: Option>, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)] +#[serde(rename_all = "camelCase")] +enum AuthorizeErrorType { + InvalidRequest, + UnauthorizedClient, + AccessDenied, + UnsupportedResponseType, + InvalidScope, + ServerError, + TemporarilyUnavailable, +} + +#[derive(Debug, Clone, Error, Serialize)] +#[error("{error_description}")] +struct AuthorizeError { + error: AuthorizeErrorType, + error_description: Box, + // TODO error uri + state: Option>, + #[serde(skip)] + redirect_uri: Url, +} + +impl AuthorizeError { + fn no_scope(redirect_uri: Url, state: Option>) -> Self { + Self { + error: AuthorizeErrorType::InvalidScope, + error_description: Box::from( + "No scope was provided, and the client does not have a default scope", + ), + state, + redirect_uri, + } + } + + fn unsupported_response_type(redirect_uri: Url, state: Option>) -> Self { + Self { + error: AuthorizeErrorType::UnsupportedResponseType, + error_description: Box::from("The given response type is not supported"), + state, + redirect_uri, + } + } + + fn invalid_scope(redirect_uri: Url, state: Option>) -> Self { + Self { + error: AuthorizeErrorType::InvalidScope, + error_description: Box::from("The given scope exceeds what the client is allowed"), + state, + redirect_uri, + } + } + + fn internal_server_error(redirect_uri: Url, state: Option>) -> Self { + Self { + error: AuthorizeErrorType::ServerError, + error_description: "An unexpected error occurred".into(), + state, + redirect_uri, + } + } +} + +impl ResponseError for AuthorizeError { + fn error_response(&self) -> HttpResponse { + let query = Some(serde_urlencoded::to_string(self).unwrap()); + let query = query.as_deref(); + let mut url = self.redirect_uri.clone(); + url.set_query(query); + + HttpResponse::Found() + .insert_header((header::LOCATION, url.as_str())) + .finish() + } +} + +fn error_page( + tera: &Tera, + translations: &languages::Translations, + error: templates::ErrorPage, +) -> Result { + // TODO find a better way of doing languages + let language = Language::from_str("en").unwrap(); + let translations = translations.clone(); + let page = templates::error_page(&tera, language, translations, error)?; + Ok(page) +} + +async fn get_redirect_uri( + redirect_uri: &Option, + db: &MySqlPool, + client_id: Uuid, +) -> Result> { + if let Some(uri) = &redirect_uri { + let redirect_uri = uri.clone(); + if !db::client_has_redirect_uri(db, client_id, &redirect_uri) + .await + .map_err(|e| UnexpectedError::from(e)) + .unexpect()? + { + yeet!(Expect::Expected(templates::ErrorPage::InvalidRedirectUri)); + } + + Ok(redirect_uri) + } else { + let redirect_uris = db::get_client_redirect_uris(db, client_id) + .await + .map_err(|e| UnexpectedError::from(e)) + .unexpect()?; + if redirect_uris.len() != 1 { + yeet!(Expect::Expected(templates::ErrorPage::MissingRedirectUri)); + } + + Ok(redirect_uris.get(0).unwrap().clone()) + } +} + +async fn get_scope( + scope: &Option>, + db: &MySqlPool, + client_id: Uuid, + redirect_uri: &Url, + state: &Option>, +) -> Result, Expect> { + let scope = if let Some(scope) = &scope { + scope.clone() + } else { + let default_scopes = db::get_client_default_scopes(db, client_id) + .await + .unwrap() + .unwrap(); + let Some(scope) = default_scopes else { + yeet!(AuthorizeError::no_scope(redirect_uri.clone(), state.clone()).into()) + }; + scope + }; + + // verify scope is valid + let allowed_scopes = db::get_client_allowed_scopes(db, client_id) + .await + .unwrap() + .unwrap(); + if !scopes::is_subset_of(&scope, &allowed_scopes) { + yeet!(AuthorizeError::invalid_scope(redirect_uri.clone(), state.clone()).into()); + } + + Ok(scope) +} + +async fn authenticate_user( + db: &MySqlPool, + username: &str, + password: &str, +) -> Result, RawUnexpected> { + let Some(user) = db::get_user_by_username(db, username).await? else { + return Ok(None); + }; + + if user.check_password(password)? { + Ok(Some(user.id)) + } else { + Ok(None) + } +} + +#[post("/authorize")] +async fn authorize( + db: web::Data, + req: web::Query, + credentials: web::Json, + tera: web::Data, + translations: web::Data, +) -> Result { + // TODO protect against brute force attacks + let db = db.get_ref(); + let Ok(client_id) = db::get_client_id_by_alias(db, &req.client_id).await else { + let page = error_page(&tera, &translations, templates::ErrorPage::InternalServerError).unwrap_or_else(|_| String::from(REALLY_BAD_ERROR_PAGE)); + return Ok(HttpResponse::InternalServerError().content_type("text/html").body(page)); + }; + let Some(client_id) = client_id else { + let page = error_page(&tera, &translations, templates::ErrorPage::ClientNotFound).unwrap_or_else(|_| String::from(REALLY_BAD_ERROR_PAGE)); + return Ok(HttpResponse::NotFound().content_type("text/html").body(page)); + }; + let Ok(config) = config::get_config() else { + let page = error_page(&tera, &translations, templates::ErrorPage::InternalServerError).unwrap_or_else(|_| String::from(REALLY_BAD_ERROR_PAGE)); + return Ok(HttpResponse::InternalServerError().content_type("text/html").body(page)); + }; + + let self_id = config.url; + let state = req.state.clone(); + + // get redirect uri + let mut redirect_uri = match get_redirect_uri(&req.redirect_uri, db, client_id).await { + Ok(uri) => uri, + Err(e) => { + let e = e + .expected() + .unwrap_or(templates::ErrorPage::InternalServerError); + let page = error_page(&tera, &translations, e) + .unwrap_or_else(|_| String::from(REALLY_BAD_ERROR_PAGE)); + return Ok(HttpResponse::BadRequest() + .content_type("text/html") + .body(page)); + } + }; + + // authenticate user + let Some(user_id) = authenticate_user(db, &credentials.username, &credentials.password) + .await + .unwrap() else + { + let language = Language::from_str("en").unwrap(); + let translations = translations.get_ref().clone(); + let page = templates::login_error_page(&tera, &req, language, translations).unwrap_or_else(|_| String::from(REALLY_BAD_ERROR_PAGE)); + return Ok(HttpResponse::Ok().content_type("text/html").body(page)); + }; + + let internal_server_error = + AuthorizeError::internal_server_error(redirect_uri.clone(), state.clone()); + + // get scope + let scope = match get_scope(&req.scope, db, client_id, &redirect_uri, &state).await { + Ok(scope) => scope, + Err(e) => { + let e = e.expected().unwrap_or(internal_server_error); + return Err(e); + } + }; + + match req.response_type { + ResponseType::Code => { + // create auth code + let code = + jwt::Claims::auth_code(db, self_id, client_id, user_id, &scope, &redirect_uri) + .await + .map_err(|_| internal_server_error.clone())?; + let code = code.to_jwt().map_err(|_| internal_server_error.clone())?; + + let response = AuthCodeResponse { code, state }; + let query = + Some(serde_urlencoded::to_string(response).map_err(|_| internal_server_error)?); + let query = query.as_deref(); + redirect_uri.set_query(query); + + Ok(HttpResponse::Found() + .append_header((header::LOCATION, redirect_uri.as_str())) + .finish()) + } + ResponseType::Token => { + // create access token + let duration = Duration::hours(1); + let access_token = + jwt::Claims::access_token(db, None, self_id, client_id, user_id, duration, &scope) + .await + .map_err(|_| internal_server_error.clone())?; + + let access_token = access_token + .to_jwt() + .map_err(|_| internal_server_error.clone())?; + let expires_in = duration.num_seconds(); + let token_type = "bearer"; + let response = AuthTokenResponse { + access_token, + expires_in, + token_type, + scope, + state, + }; + + let fragment = Some( + serde_urlencoded::to_string(response).map_err(|_| internal_server_error.clone())?, + ); + let fragment = fragment.as_deref(); + redirect_uri.set_fragment(fragment); + + Ok(HttpResponse::Found() + .append_header((header::LOCATION, redirect_uri.as_str())) + .finish()) + } + _ => Err(AuthorizeError::invalid_scope(redirect_uri, state)), + } +} + +#[get("/authorize")] +async fn authorize_page( + db: web::Data, + tera: web::Data, + translations: web::Data, + request: HttpRequest, +) -> Result { + let Ok(language) = Language::from_str("en") else { + let page = String::from(REALLY_BAD_ERROR_PAGE); + return Ok(HttpResponse::InternalServerError() + .content_type("text/html") + .body(page)); + }; + let translations = translations.get_ref().clone(); + + let params = request.query_string(); + let params = serde_urlencoded::from_str::(params); + let Ok(params) = params else { + let page = error_page( + &tera, + &translations, + templates::ErrorPage::InvalidRequest, + ) + .unwrap_or_else(|_| String::from(REALLY_BAD_ERROR_PAGE)); + return Ok(HttpResponse::BadRequest() + .content_type("text/html") + .body(page)); + }; + + let db = db.get_ref(); + let Ok(client_id) = db::get_client_id_by_alias(db, ¶ms.client_id).await else { + let page = templates::error_page( + &tera, + language, + translations, + templates::ErrorPage::InternalServerError, + ) + .unwrap_or_else(|_| String::from(REALLY_BAD_ERROR_PAGE)); + return Ok(HttpResponse::InternalServerError() + .content_type("text/html") + .body(page)); + }; + let Some(client_id) = client_id else { + let page = templates::error_page( + &tera, + language, + translations, + templates::ErrorPage::ClientNotFound, + ) + .unwrap_or_else(|_| String::from(REALLY_BAD_ERROR_PAGE)); + return Ok(HttpResponse::NotFound() + .content_type("text/html") + .body(page)); + }; + + // verify redirect uri + let redirect_uri = match get_redirect_uri(¶ms.redirect_uri, db, client_id).await { + Ok(uri) => uri, + Err(e) => { + let e = e + .expected() + .unwrap_or(templates::ErrorPage::InternalServerError); + let page = error_page(&tera, &translations, e) + .unwrap_or_else(|_| String::from(REALLY_BAD_ERROR_PAGE)); + return Ok(HttpResponse::BadRequest() + .content_type("text/html") + .body(page)); + } + }; + + let state = ¶ms.state; + let internal_server_error = + AuthorizeError::internal_server_error(redirect_uri.clone(), state.clone()); + + // verify scope + let _ = match get_scope(¶ms.scope, db, client_id, &redirect_uri, ¶ms.state).await { + Ok(scope) => scope, + Err(e) => { + let e = e.expected().unwrap_or(internal_server_error); + return Err(e); + } + }; + + // verify response type + if params.response_type == ResponseType::Unsupported { + return Err(AuthorizeError::unsupported_response_type( + redirect_uri, + params.state, + )); + } + + // TODO find a better way of doing languages + let language = Language::from_str("en").unwrap(); + let page = templates::login_page(&tera, ¶ms, language, translations).unwrap(); + Ok(HttpResponse::Ok().content_type("text/html").body(page)) +} + +#[derive(Clone, Deserialize)] +#[serde(tag = "grant_type")] +#[serde(rename_all = "snake_case")] +enum GrantType { + AuthorizationCode { + code: Box, + redirect_uri: Url, + #[serde(rename = "client_id")] + client_alias: Box, + }, + Password { + username: Box, + password: Box, + scope: Option>, + }, + ClientCredentials { + scope: Option>, + }, + RefreshToken { + refresh_token: Box, + scope: Option>, + }, + #[serde(other)] + Unsupported, +} + +#[derive(Clone, Deserialize)] +struct TokenRequest { + #[serde(flatten)] + grant_type: GrantType, + // TODO support optional client credentials in here +} + +#[derive(Clone, Serialize)] +struct TokenResponse { + access_token: Box, + token_type: Box, + expires_in: i64, + refresh_token: Option>, + scope: Box, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "snake_case")] +enum TokenErrorType { + InvalidRequest, + InvalidClient, + InvalidGrant, + UnauthorizedClient, + UnsupportedGrantType, + InvalidScope, +} + +#[derive(Debug, Clone, Error, Serialize)] +#[error("{error_description}")] +struct TokenError { + #[serde(skip)] + status_code: StatusCode, + error: TokenErrorType, + error_description: Box, + // TODO error uri +} + +impl TokenError { + fn invalid_request() -> Self { + // TODO make this description better, and all the other ones while you're at it + Self { + status_code: StatusCode::BAD_REQUEST, + error: TokenErrorType::InvalidRequest, + error_description: "Invalid request".into(), + } + } + + fn unsupported_grant_type() -> Self { + Self { + status_code: StatusCode::BAD_REQUEST, + error: TokenErrorType::UnsupportedGrantType, + error_description: "The given grant type is not supported".into(), + } + } + + fn bad_auth_code(error: VerifyJwtError) -> Self { + Self { + status_code: StatusCode::BAD_REQUEST, + error: TokenErrorType::InvalidGrant, + error_description: error.to_string().into_boxed_str(), + } + } + + fn no_authorization() -> Self { + Self { + status_code: StatusCode::UNAUTHORIZED, + error: TokenErrorType::InvalidClient, + error_description: Box::from( + "Client credentials must be provided in the HTTP Authorization header", + ), + } + } + + fn client_not_found(alias: &str) -> Self { + Self { + status_code: StatusCode::UNAUTHORIZED, + error: TokenErrorType::InvalidClient, + error_description: format!("No client with the client id: {alias} was found") + .into_boxed_str(), + } + } + + fn mismatch_client_id() -> Self { + Self { + status_code: StatusCode::UNAUTHORIZED, + error: TokenErrorType::InvalidClient, + error_description: Box::from("The client ID in the Authorization header is not the same as the client ID in the request body"), + } + } + + fn incorrect_client_secret() -> Self { + Self { + status_code: StatusCode::UNAUTHORIZED, + error: TokenErrorType::InvalidClient, + error_description: "The client secret is incorrect".into(), + } + } + + fn client_not_confidential(alias: &str) -> Self { + Self { + status_code: StatusCode::BAD_REQUEST, + error: TokenErrorType::UnauthorizedClient, + error_description: format!("Only a confidential client may be used with this endpoint. The {alias} client is a public client.") + .into_boxed_str(), + } + } + + fn no_scope() -> Self { + Self { + status_code: StatusCode::BAD_REQUEST, + error: TokenErrorType::InvalidScope, + error_description: Box::from( + "No scope was provided, and the client doesn't have a default scope", + ), + } + } + + fn excessive_scope() -> Self { + Self { + status_code: StatusCode::BAD_REQUEST, + error: TokenErrorType::InvalidScope, + error_description: Box::from( + "The given scope exceeds what the client is allowed to have", + ), + } + } + + fn bad_refresh_token(err: VerifyJwtError) -> Self { + Self { + status_code: StatusCode::BAD_REQUEST, + error: TokenErrorType::InvalidGrant, + error_description: err.to_string().into_boxed_str(), + } + } + + fn untrusted_client() -> Self { + Self { + status_code: StatusCode::UNAUTHORIZED, + error: TokenErrorType::InvalidClient, + error_description: "Only trusted clients may use this grant".into(), + } + } + + fn incorrect_user_credentials() -> Self { + Self { + status_code: StatusCode::BAD_REQUEST, + error: TokenErrorType::InvalidRequest, + error_description: "The given credentials are incorrect".into(), + } + } +} + +impl ResponseError for TokenError { + fn error_response(&self) -> HttpResponse { + let cache_control = header::CacheControl(vec![header::CacheDirective::NoStore]); + + let mut builder = HttpResponseBuilder::new(self.status_code); + + if self.status_code.as_u16() == 401 { + builder.insert_header((header::WWW_AUTHENTICATE, "Basic charset=\"UTF-8\"")); + } + + builder + .insert_header(cache_control) + .insert_header((header::PRAGMA, "no-cache")) + .json(self.clone()) + } +} + +#[post("/token")] +async fn token( + db: web::Data, + req: web::Bytes, + authorization: Option>, +) -> HttpResponse { + // TODO protect against brute force attacks + let db = db.get_ref(); + let request = serde_json::from_slice::(&req); + let Ok(request) = request else { + return TokenError::invalid_request().error_response(); + }; + let config = config::get_config().unwrap(); + + let self_id = config.url; + let duration = Duration::hours(1); + let token_type = Box::from("bearer"); + let cache_control = header::CacheControl(vec![header::CacheDirective::NoStore]); + + match request.grant_type { + GrantType::AuthorizationCode { + code, + redirect_uri, + client_alias, + } => { + let Some(client_id) = db::get_client_id_by_alias(db, &client_alias).await.unwrap() else { + return TokenError::client_not_found(&client_alias).error_response(); + }; + + // validate auth code + let claims = + match jwt::verify_auth_code(db, &code, &self_id, client_id, redirect_uri).await { + Ok(claims) => claims, + Err(err) => { + let err = err.unwrap(); + return TokenError::bad_auth_code(err).error_response(); + } + }; + + // verify client, if the client has credentials + if let Some(hash) = db::get_client_secret(db, client_id).await.unwrap() { + let Some(authorization) = authorization else { + return TokenError::no_authorization().error_response(); + }; + + if authorization.username() != client_alias.deref() { + return TokenError::mismatch_client_id().error_response(); + } + if !hash.check_password(authorization.password()).unwrap() { + return TokenError::incorrect_client_secret().error_response(); + } + } + + let access_token = jwt::Claims::access_token( + db, + Some(claims.id()), + self_id, + client_id, + claims.subject(), + duration, + claims.scopes(), + ) + .await + .unwrap(); + + let expires_in = access_token.expires_in(); + let refresh_token = jwt::Claims::refresh_token(db, &access_token).await.unwrap(); + let scope = access_token.scopes().into(); + + let access_token = access_token.to_jwt().unwrap(); + let refresh_token = Some(refresh_token.to_jwt().unwrap()); + + let response = TokenResponse { + access_token, + token_type, + expires_in, + refresh_token, + scope, + }; + HttpResponse::Ok() + .insert_header(cache_control) + .insert_header((header::PRAGMA, "no-cache")) + .json(response) + } + GrantType::Password { + username, + password, + scope, + } => { + let Some(authorization) = authorization else { + return TokenError::no_authorization().error_response(); + }; + let client_alias = authorization.username(); + let Some(client_id) = db::get_client_id_by_alias(db, client_alias).await.unwrap() else { + return TokenError::client_not_found(client_alias).error_response(); + }; + + let trusted = db::is_client_trusted(db, client_id).await.unwrap().unwrap(); + if !trusted { + return TokenError::untrusted_client().error_response(); + } + + // verify client + let hash = db::get_client_secret(db, client_id).await.unwrap().unwrap(); + if !hash.check_password(authorization.password()).unwrap() { + return TokenError::incorrect_client_secret().error_response(); + } + + // authenticate user + let Some(user_id) = authenticate_user(db, &username, &password).await.unwrap() else { + return TokenError::incorrect_user_credentials().error_response(); + }; + + // verify scope + let allowed_scopes = db::get_client_allowed_scopes(db, client_id) + .await + .unwrap() + .unwrap(); + let scope = if let Some(scope) = &scope { + scope.clone() + } else { + let default_scopes = db::get_client_default_scopes(db, client_id) + .await + .unwrap() + .unwrap(); + let Some(scope) = default_scopes else { + return TokenError::no_scope().error_response(); + }; + scope + }; + if !scopes::is_subset_of(&scope, &allowed_scopes) { + return TokenError::excessive_scope().error_response(); + } + + let access_token = + jwt::Claims::access_token(db, None, self_id, client_id, user_id, duration, &scope) + .await + .unwrap(); + let refresh_token = jwt::Claims::refresh_token(db, &access_token).await.unwrap(); + + let expires_in = access_token.expires_in(); + let scope = access_token.scopes().into(); + let access_token = access_token.to_jwt().unwrap(); + let refresh_token = Some(refresh_token.to_jwt().unwrap()); + + let response = TokenResponse { + access_token, + token_type, + expires_in, + refresh_token, + scope, + }; + HttpResponse::Ok() + .insert_header(cache_control) + .insert_header((header::PRAGMA, "no-cache")) + .json(response) + } + GrantType::ClientCredentials { scope } => { + let Some(authorization) = authorization else { + return TokenError::no_authorization().error_response(); + }; + let client_alias = authorization.username(); + let Some(client_id) = db::get_client_id_by_alias(db, client_alias).await.unwrap() else { + return TokenError::client_not_found(client_alias).error_response(); + }; + + let ty = db::get_client_type(db, client_id).await.unwrap().unwrap(); + if ty != ClientType::Confidential { + return TokenError::client_not_confidential(client_alias).error_response(); + } + + // verify client + let hash = db::get_client_secret(db, client_id).await.unwrap().unwrap(); + if !hash.check_password(authorization.password()).unwrap() { + return TokenError::incorrect_client_secret().error_response(); + } + + // verify scope + let allowed_scopes = db::get_client_allowed_scopes(db, client_id) + .await + .unwrap() + .unwrap(); + let scope = if let Some(scope) = &scope { + scope.clone() + } else { + let default_scopes = db::get_client_default_scopes(db, client_id) + .await + .unwrap() + .unwrap(); + let Some(scope) = default_scopes else { + return TokenError::no_scope().error_response(); + }; + scope + }; + if !scopes::is_subset_of(&scope, &allowed_scopes) { + return TokenError::excessive_scope().error_response(); + } + + let access_token = jwt::Claims::access_token( + db, None, self_id, client_id, client_id, duration, &scope, + ) + .await + .unwrap(); + + let expires_in = access_token.expires_in(); + let scope = access_token.scopes().into(); + let access_token = access_token.to_jwt().unwrap(); + + let response = TokenResponse { + access_token, + token_type, + expires_in, + refresh_token: None, + scope, + }; + HttpResponse::Ok() + .insert_header(cache_control) + .insert_header((header::PRAGMA, "no-cache")) + .json(response) + } + GrantType::RefreshToken { + refresh_token, + scope, + } => { + let client_id: Option; + if let Some(authorization) = authorization { + let client_alias = authorization.username(); + let Some(id) = db::get_client_id_by_alias(db, client_alias).await.unwrap() else { + return TokenError::client_not_found(client_alias).error_response(); + }; + client_id = Some(id); + } else { + client_id = None; + } + + let claims = + match jwt::verify_refresh_token(db, &refresh_token, &self_id, client_id).await { + Ok(claims) => claims, + Err(e) => { + let e = e.unwrap(); + return TokenError::bad_refresh_token(e).error_response(); + } + }; + + let scope = if let Some(scope) = scope { + if !scopes::is_subset_of(&scope, claims.scopes()) { + return TokenError::excessive_scope().error_response(); + } + + scope + } else { + claims.scopes().into() + }; + + let exp_time = Duration::hours(1); + let access_token = jwt::Claims::refreshed_access_token(db, &claims, exp_time) + .await + .unwrap(); + let refresh_token = jwt::Claims::refresh_token(db, &claims).await.unwrap(); + + let access_token = access_token.to_jwt().unwrap(); + let refresh_token = Some(refresh_token.to_jwt().unwrap()); + let expires_in = exp_time.num_seconds(); + + let response = TokenResponse { + access_token, + token_type, + expires_in, + refresh_token, + scope, + }; + HttpResponse::Ok() + .insert_header(cache_control) + .insert_header((header::PRAGMA, "no-cache")) + .json(response) + } + _ => TokenError::unsupported_grant_type().error_response(), + } +} + +pub fn service() -> Scope { + web::scope("/oauth") + .service(authorize_page) + .service(authorize) + .service(token) +} diff --git a/src/api/ops.rs b/src/api/ops.rs index 555bb1b..2164f1f 100644 --- a/src/api/ops.rs +++ b/src/api/ops.rs @@ -1,70 +1,70 @@ -use std::str::FromStr; - -use actix_web::{get, http::StatusCode, post, web, HttpResponse, ResponseError, Scope}; -use raise::yeet; -use serde::Deserialize; -use sqlx::MySqlPool; -use tera::Tera; -use thiserror::Error; -use unic_langid::subtags::Language; - -use crate::resources::{languages, templates}; -use crate::services::db; - -/// A request to login -#[derive(Debug, Clone, Deserialize)] -struct LoginRequest { - username: Box, - password: Box, -} - -/// An error occurred when authenticating, because either the username or -/// password was invalid. -#[derive(Debug, Clone, Error)] -enum LoginFailure { - #[error("No user found with the given username")] - UserNotFound { username: Box }, - #[error("The given password is incorrect")] - IncorrectPassword { username: Box }, -} - -impl ResponseError for LoginFailure { - fn status_code(&self) -> actix_web::http::StatusCode { - match self { - Self::UserNotFound { .. } => StatusCode::NOT_FOUND, - Self::IncorrectPassword { .. } => StatusCode::UNAUTHORIZED, - } - } -} - -/// Returns `200` if login was successful. -/// Returns `404` if the username is invalid. -/// Returns `401` if the password was invalid. -#[post("/login")] -async fn login( - body: web::Json, - conn: web::Data, -) -> Result { - let conn = conn.get_ref(); - - let user = db::get_user_by_username(conn, &body.username) - .await - .unwrap(); - let Some(user) = user else { - yeet!(LoginFailure::UserNotFound{ username: body.username.clone() }); - }; - - let good_password = user.check_password(&body.password).unwrap(); - let response = if good_password { - HttpResponse::Ok().finish() - } else { - yeet!(LoginFailure::IncorrectPassword { - username: body.username.clone() - }); - }; - Ok(response) -} - -pub fn service() -> Scope { - web::scope("").service(login) -} +use std::str::FromStr; + +use actix_web::{get, http::StatusCode, post, web, HttpResponse, ResponseError, Scope}; +use raise::yeet; +use serde::Deserialize; +use sqlx::MySqlPool; +use tera::Tera; +use thiserror::Error; +use unic_langid::subtags::Language; + +use crate::resources::{languages, templates}; +use crate::services::db; + +/// A request to login +#[derive(Debug, Clone, Deserialize)] +struct LoginRequest { + username: Box, + password: Box, +} + +/// An error occurred when authenticating, because either the username or +/// password was invalid. +#[derive(Debug, Clone, Error)] +enum LoginFailure { + #[error("No user found with the given username")] + UserNotFound { username: Box }, + #[error("The given password is incorrect")] + IncorrectPassword { username: Box }, +} + +impl ResponseError for LoginFailure { + fn status_code(&self) -> actix_web::http::StatusCode { + match self { + Self::UserNotFound { .. } => StatusCode::NOT_FOUND, + Self::IncorrectPassword { .. } => StatusCode::UNAUTHORIZED, + } + } +} + +/// Returns `200` if login was successful. +/// Returns `404` if the username is invalid. +/// Returns `401` if the password was invalid. +#[post("/login")] +async fn login( + body: web::Json, + conn: web::Data, +) -> Result { + let conn = conn.get_ref(); + + let user = db::get_user_by_username(conn, &body.username) + .await + .unwrap(); + let Some(user) = user else { + yeet!(LoginFailure::UserNotFound{ username: body.username.clone() }); + }; + + let good_password = user.check_password(&body.password).unwrap(); + let response = if good_password { + HttpResponse::Ok().finish() + } else { + yeet!(LoginFailure::IncorrectPassword { + username: body.username.clone() + }); + }; + Ok(response) +} + +pub fn service() -> Scope { + web::scope("").service(login) +} diff --git a/src/api/users.rs b/src/api/users.rs index 391a059..da2a0d0 100644 --- a/src/api/users.rs +++ b/src/api/users.rs @@ -1,272 +1,272 @@ -use actix_web::http::{header, StatusCode}; -use actix_web::{get, post, put, web, HttpResponse, ResponseError, Scope}; -use raise::yeet; -use serde::{Deserialize, Serialize}; -use sqlx::MySqlPool; -use thiserror::Error; -use uuid::Uuid; - -use crate::models::user::User; -use crate::services::crypto::PasswordHash; -use crate::services::{db, id}; - -/// Just a username. No password hash, because that'd be tempting fate. -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -struct UserResponse { - id: Uuid, - username: Box, -} - -impl From for UserResponse { - fn from(user: User) -> Self { - Self { - id: user.id, - username: user.username, - } - } -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -struct SearchUsers { - username: Option>, - limit: Option, - offset: Option, -} - -#[get("")] -async fn search_users(params: web::Query, conn: web::Data) -> HttpResponse { - let conn = conn.get_ref(); - - let username = params.username.clone().unwrap_or_default(); - let offset = params.offset.unwrap_or_default(); - - let results: Box<[UserResponse]> = if let Some(limit) = params.limit { - db::search_users_limit(conn, &username, offset, limit) - .await - .unwrap() - .iter() - .cloned() - .map(|u| u.into()) - .collect() - } else { - db::search_users(conn, &username) - .await - .unwrap() - .into_iter() - .skip(offset as usize) - .cloned() - .map(|u| u.into()) - .collect() - }; - - let response = HttpResponse::Ok().json(results); - response -} - -#[derive(Debug, Clone, Error)] -#[error("No user with the given ID exists")] -struct UserNotFoundError { - user_id: Uuid, -} - -impl ResponseError for UserNotFoundError { - fn status_code(&self) -> StatusCode { - StatusCode::NOT_FOUND - } -} - -#[get("/{user_id}")] -async fn get_user( - user_id: web::Path, - conn: web::Data, -) -> Result { - let conn = conn.get_ref(); - - let id = user_id.to_owned(); - let username = db::get_username(conn, id).await.unwrap(); - - let Some(username) = username else { - yeet!(UserNotFoundError { user_id: id }); - }; - - let response = UserResponse { id, username }; - let response = HttpResponse::Ok().json(response); - Ok(response) -} - -#[get("/{user_id}/username")] -async fn get_username( - user_id: web::Path, - conn: web::Data, -) -> Result { - let conn = conn.get_ref(); - - let user_id = user_id.to_owned(); - let username = db::get_username(conn, user_id).await.unwrap(); - - let Some(username) = username else { - yeet!(UserNotFoundError { user_id }); - }; - - let response = HttpResponse::Ok().json(username); - Ok(response) -} - -/// A request to create or update user information -#[derive(Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -struct UserRequest { - username: Box, - password: Box, -} - -#[derive(Debug, Clone, Error)] -#[error("An account with the given username already exists.")] -struct UsernameTakenError { - username: Box, -} - -impl ResponseError for UsernameTakenError { - fn status_code(&self) -> StatusCode { - StatusCode::CONFLICT - } -} - -#[post("")] -async fn create_user( - body: web::Json, - conn: web::Data, -) -> Result { - let conn = conn.get_ref(); - - let user_id = id::new_id(conn, db::user_id_exists).await.unwrap(); - let username = body.username.clone(); - let password = PasswordHash::new(&body.password).unwrap(); - - if db::username_is_used(conn, &body.username).await.unwrap() { - yeet!(UsernameTakenError { username }); - } - - let user = User { - id: user_id, - username, - password, - }; - - db::create_user(conn, &user).await.unwrap(); - - let response = HttpResponse::Created() - .insert_header((header::LOCATION, format!("users/{user_id}"))) - .finish(); - Ok(response) -} - -#[derive(Debug, Clone, Error)] -enum UpdateUserError { - #[error(transparent)] - UsernameTaken(#[from] UsernameTakenError), - #[error(transparent)] - NotFound(#[from] UserNotFoundError), -} - -impl ResponseError for UpdateUserError { - fn status_code(&self) -> StatusCode { - match self { - Self::UsernameTaken(e) => e.status_code(), - Self::NotFound(e) => e.status_code(), - } - } -} - -#[put("/{user_id}")] -async fn update_user( - user_id: web::Path, - body: web::Json, - conn: web::Data, -) -> Result { - let conn = conn.get_ref(); - - let user_id = user_id.to_owned(); - let username = body.username.clone(); - let password = PasswordHash::new(&body.password).unwrap(); - - let old_username = db::get_username(conn, user_id).await.unwrap().unwrap(); - if username != old_username && db::username_is_used(conn, &body.username).await.unwrap() { - yeet!(UsernameTakenError { username }.into()) - } - - if !db::user_id_exists(conn, user_id).await.unwrap() { - yeet!(UserNotFoundError { user_id }.into()) - } - - let user = User { - id: user_id, - username, - password, - }; - - db::update_user(conn, &user).await.unwrap(); - - let response = HttpResponse::NoContent().finish(); - Ok(response) -} - -#[put("/{user_id}/username")] -async fn update_username( - user_id: web::Path, - body: web::Json>, - conn: web::Data, -) -> Result { - let conn = conn.get_ref(); - - let user_id = user_id.to_owned(); - let username = body.clone(); - - let old_username = db::get_username(conn, user_id).await.unwrap().unwrap(); - if username != old_username && db::username_is_used(conn, &body).await.unwrap() { - yeet!(UsernameTakenError { username }.into()) - } - - if !db::user_id_exists(conn, user_id).await.unwrap() { - yeet!(UserNotFoundError { user_id }.into()) - } - - db::update_username(conn, user_id, &body).await.unwrap(); - - let response = HttpResponse::NoContent().finish(); - Ok(response) -} - -#[put("/{user_id}/password")] -async fn update_password( - user_id: web::Path, - body: web::Json>, - conn: web::Data, -) -> Result { - let conn = conn.get_ref(); - - let user_id = user_id.to_owned(); - let password = PasswordHash::new(&body).unwrap(); - - if !db::user_id_exists(conn, user_id).await.unwrap() { - yeet!(UserNotFoundError { user_id }) - } - - db::update_password(conn, user_id, &password).await.unwrap(); - - let response = HttpResponse::NoContent().finish(); - Ok(response) -} - -pub fn service() -> Scope { - web::scope("/users") - .service(search_users) - .service(get_user) - .service(get_username) - .service(create_user) - .service(update_user) - .service(update_username) - .service(update_password) -} +use actix_web::http::{header, StatusCode}; +use actix_web::{get, post, put, web, HttpResponse, ResponseError, Scope}; +use raise::yeet; +use serde::{Deserialize, Serialize}; +use sqlx::MySqlPool; +use thiserror::Error; +use uuid::Uuid; + +use crate::models::user::User; +use crate::services::crypto::PasswordHash; +use crate::services::{db, id}; + +/// Just a username. No password hash, because that'd be tempting fate. +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +struct UserResponse { + id: Uuid, + username: Box, +} + +impl From for UserResponse { + fn from(user: User) -> Self { + Self { + id: user.id, + username: user.username, + } + } +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +struct SearchUsers { + username: Option>, + limit: Option, + offset: Option, +} + +#[get("")] +async fn search_users(params: web::Query, conn: web::Data) -> HttpResponse { + let conn = conn.get_ref(); + + let username = params.username.clone().unwrap_or_default(); + let offset = params.offset.unwrap_or_default(); + + let results: Box<[UserResponse]> = if let Some(limit) = params.limit { + db::search_users_limit(conn, &username, offset, limit) + .await + .unwrap() + .iter() + .cloned() + .map(|u| u.into()) + .collect() + } else { + db::search_users(conn, &username) + .await + .unwrap() + .into_iter() + .skip(offset as usize) + .cloned() + .map(|u| u.into()) + .collect() + }; + + let response = HttpResponse::Ok().json(results); + response +} + +#[derive(Debug, Clone, Error)] +#[error("No user with the given ID exists")] +struct UserNotFoundError { + user_id: Uuid, +} + +impl ResponseError for UserNotFoundError { + fn status_code(&self) -> StatusCode { + StatusCode::NOT_FOUND + } +} + +#[get("/{user_id}")] +async fn get_user( + user_id: web::Path, + conn: web::Data, +) -> Result { + let conn = conn.get_ref(); + + let id = user_id.to_owned(); + let username = db::get_username(conn, id).await.unwrap(); + + let Some(username) = username else { + yeet!(UserNotFoundError { user_id: id }); + }; + + let response = UserResponse { id, username }; + let response = HttpResponse::Ok().json(response); + Ok(response) +} + +#[get("/{user_id}/username")] +async fn get_username( + user_id: web::Path, + conn: web::Data, +) -> Result { + let conn = conn.get_ref(); + + let user_id = user_id.to_owned(); + let username = db::get_username(conn, user_id).await.unwrap(); + + let Some(username) = username else { + yeet!(UserNotFoundError { user_id }); + }; + + let response = HttpResponse::Ok().json(username); + Ok(response) +} + +/// A request to create or update user information +#[derive(Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +struct UserRequest { + username: Box, + password: Box, +} + +#[derive(Debug, Clone, Error)] +#[error("An account with the given username already exists.")] +struct UsernameTakenError { + username: Box, +} + +impl ResponseError for UsernameTakenError { + fn status_code(&self) -> StatusCode { + StatusCode::CONFLICT + } +} + +#[post("")] +async fn create_user( + body: web::Json, + conn: web::Data, +) -> Result { + let conn = conn.get_ref(); + + let user_id = id::new_id(conn, db::user_id_exists).await.unwrap(); + let username = body.username.clone(); + let password = PasswordHash::new(&body.password).unwrap(); + + if db::username_is_used(conn, &body.username).await.unwrap() { + yeet!(UsernameTakenError { username }); + } + + let user = User { + id: user_id, + username, + password, + }; + + db::create_user(conn, &user).await.unwrap(); + + let response = HttpResponse::Created() + .insert_header((header::LOCATION, format!("users/{user_id}"))) + .finish(); + Ok(response) +} + +#[derive(Debug, Clone, Error)] +enum UpdateUserError { + #[error(transparent)] + UsernameTaken(#[from] UsernameTakenError), + #[error(transparent)] + NotFound(#[from] UserNotFoundError), +} + +impl ResponseError for UpdateUserError { + fn status_code(&self) -> StatusCode { + match self { + Self::UsernameTaken(e) => e.status_code(), + Self::NotFound(e) => e.status_code(), + } + } +} + +#[put("/{user_id}")] +async fn update_user( + user_id: web::Path, + body: web::Json, + conn: web::Data, +) -> Result { + let conn = conn.get_ref(); + + let user_id = user_id.to_owned(); + let username = body.username.clone(); + let password = PasswordHash::new(&body.password).unwrap(); + + let old_username = db::get_username(conn, user_id).await.unwrap().unwrap(); + if username != old_username && db::username_is_used(conn, &body.username).await.unwrap() { + yeet!(UsernameTakenError { username }.into()) + } + + if !db::user_id_exists(conn, user_id).await.unwrap() { + yeet!(UserNotFoundError { user_id }.into()) + } + + let user = User { + id: user_id, + username, + password, + }; + + db::update_user(conn, &user).await.unwrap(); + + let response = HttpResponse::NoContent().finish(); + Ok(response) +} + +#[put("/{user_id}/username")] +async fn update_username( + user_id: web::Path, + body: web::Json>, + conn: web::Data, +) -> Result { + let conn = conn.get_ref(); + + let user_id = user_id.to_owned(); + let username = body.clone(); + + let old_username = db::get_username(conn, user_id).await.unwrap().unwrap(); + if username != old_username && db::username_is_used(conn, &body).await.unwrap() { + yeet!(UsernameTakenError { username }.into()) + } + + if !db::user_id_exists(conn, user_id).await.unwrap() { + yeet!(UserNotFoundError { user_id }.into()) + } + + db::update_username(conn, user_id, &body).await.unwrap(); + + let response = HttpResponse::NoContent().finish(); + Ok(response) +} + +#[put("/{user_id}/password")] +async fn update_password( + user_id: web::Path, + body: web::Json>, + conn: web::Data, +) -> Result { + let conn = conn.get_ref(); + + let user_id = user_id.to_owned(); + let password = PasswordHash::new(&body).unwrap(); + + if !db::user_id_exists(conn, user_id).await.unwrap() { + yeet!(UserNotFoundError { user_id }) + } + + db::update_password(conn, user_id, &password).await.unwrap(); + + let response = HttpResponse::NoContent().finish(); + Ok(response) +} + +pub fn service() -> Scope { + web::scope("/users") + .service(search_users) + .service(get_user) + .service(get_username) + .service(create_user) + .service(update_user) + .service(update_username) + .service(update_password) +} diff --git a/src/main.rs b/src/main.rs index e946161..e403798 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,108 +1,108 @@ -use std::time::Duration; - -use actix_web::http::header::{self, HeaderValue}; -use actix_web::middleware::{ErrorHandlerResponse, ErrorHandlers, Logger, NormalizePath}; -use actix_web::web::Data; -use actix_web::{dev, App, HttpServer}; - -use bpaf::Bpaf; -use exun::*; - -mod api; -mod models; -mod resources; -mod scopes; -mod services; - -use resources::*; -use services::*; -use sqlx::MySqlPool; - -fn error_content_language( - mut res: dev::ServiceResponse, -) -> actix_web::Result> { - res.response_mut() - .headers_mut() - .insert(header::CONTENT_LANGUAGE, HeaderValue::from_static("en")); - - Ok(ErrorHandlerResponse::Response(res.map_into_right_body())) -} - -async fn delete_expired_tokens(db: MySqlPool) { - let db = db.clone(); - let mut interval = actix_rt::time::interval(Duration::from_secs(60 * 20)); - loop { - interval.tick().await; - if let Err(e) = db::delete_expired_auth_codes(&db).await { - log::error!("{}", e); - } - if let Err(e) = db::delete_expired_access_tokens(&db).await { - log::error!("{}", e); - } - if let Err(e) = db::delete_expired_refresh_tokens(&db).await { - log::error!("{}", e); - } - } -} - -#[derive(Debug, Clone, Bpaf)] -#[bpaf(options, version)] -struct Opts { - /// The environment that the server is running in. Must be one of: local, - /// dev, staging, prod. - #[bpaf( - env("LOCKDAGGER_ENVIRONMENT"), - fallback(config::Environment::Local), - display_fallback - )] - env: config::Environment, -} - -#[actix_web::main] -async fn main() -> Result<(), RawUnexpected> { - // load the environment file, but only in debug mode - #[cfg(debug_assertions)] - dotenv::dotenv()?; - - let args = opts().run(); - config::set_environment(args.env); - - // initialize the database - let db_url = secrets::database_url()?; - let sql_pool = db::initialize(&db_url).await?; - - let tera = templates::initialize()?; - - let translations = languages::initialize()?; - - actix_rt::spawn(delete_expired_tokens(sql_pool.clone())); - - // start the server - HttpServer::new(move || { - App::new() - // middleware - .wrap(ErrorHandlers::new().default_handler(error_content_language)) - .wrap(NormalizePath::trim()) - .wrap(Logger::new("\"%r\" %s %Dms")) - // app shared state - .app_data(Data::new(sql_pool.clone())) - .app_data(Data::new(tera.clone())) - .app_data(Data::new(translations.clone())) - // frontend services - .service(style::get_css) - .service(scripts::get_js) - .service(languages::languages()) - // api services - .service(api::liveops()) - .service(api::users()) - .service(api::clients()) - .service(api::oauth()) - .service(api::ops()) - }) - .shutdown_timeout(1) - .bind(("127.0.0.1", 8080))? - .run() - .await?; - - Ok(()) -} +use std::time::Duration; + +use actix_web::http::header::{self, HeaderValue}; +use actix_web::middleware::{ErrorHandlerResponse, ErrorHandlers, Logger, NormalizePath}; +use actix_web::web::Data; +use actix_web::{dev, App, HttpServer}; + +use bpaf::Bpaf; +use exun::*; + +mod api; +mod models; +mod resources; +mod scopes; +mod services; + +use resources::*; +use services::*; +use sqlx::MySqlPool; + +fn error_content_language( + mut res: dev::ServiceResponse, +) -> actix_web::Result> { + res.response_mut() + .headers_mut() + .insert(header::CONTENT_LANGUAGE, HeaderValue::from_static("en")); + + Ok(ErrorHandlerResponse::Response(res.map_into_right_body())) +} + +async fn delete_expired_tokens(db: MySqlPool) { + let db = db.clone(); + let mut interval = actix_rt::time::interval(Duration::from_secs(60 * 20)); + loop { + interval.tick().await; + if let Err(e) = db::delete_expired_auth_codes(&db).await { + log::error!("{}", e); + } + if let Err(e) = db::delete_expired_access_tokens(&db).await { + log::error!("{}", e); + } + if let Err(e) = db::delete_expired_refresh_tokens(&db).await { + log::error!("{}", e); + } + } +} + +#[derive(Debug, Clone, Bpaf)] +#[bpaf(options, version)] +struct Opts { + /// The environment that the server is running in. Must be one of: local, + /// dev, staging, prod. + #[bpaf( + env("LOCKDAGGER_ENVIRONMENT"), + fallback(config::Environment::Local), + display_fallback + )] + env: config::Environment, +} + +#[actix_web::main] +async fn main() -> Result<(), RawUnexpected> { + // load the environment file, but only in debug mode + #[cfg(debug_assertions)] + dotenv::dotenv()?; + + let args = opts().run(); + config::set_environment(args.env); + + // initialize the database + let db_url = secrets::database_url()?; + let sql_pool = db::initialize(&db_url).await?; + + let tera = templates::initialize()?; + + let translations = languages::initialize()?; + + actix_rt::spawn(delete_expired_tokens(sql_pool.clone())); + + // start the server + HttpServer::new(move || { + App::new() + // middleware + .wrap(ErrorHandlers::new().default_handler(error_content_language)) + .wrap(NormalizePath::trim()) + .wrap(Logger::new("\"%r\" %s %Dms")) + // app shared state + .app_data(Data::new(sql_pool.clone())) + .app_data(Data::new(tera.clone())) + .app_data(Data::new(translations.clone())) + // frontend services + .service(style::get_css) + .service(scripts::get_js) + .service(languages::languages()) + // api services + .service(api::liveops()) + .service(api::users()) + .service(api::clients()) + .service(api::oauth()) + .service(api::ops()) + }) + .shutdown_timeout(1) + .bind(("127.0.0.1", 8080))? + .run() + .await?; + + Ok(()) +} diff --git a/src/models/client.rs b/src/models/client.rs index 38be37f..6d0c909 100644 --- a/src/models/client.rs +++ b/src/models/client.rs @@ -1,165 +1,165 @@ -use std::{hash::Hash, marker::PhantomData}; - -use actix_web::{http::StatusCode, ResponseError}; -use exun::{Expect, RawUnexpected}; -use raise::yeet; -use serde::{Deserialize, Serialize}; -use thiserror::Error; -use url::Url; -use uuid::Uuid; - -use crate::services::crypto::PasswordHash; - -/// There are two types of clients, based on their ability to maintain the -/// security of their client credentials. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] -#[sqlx(rename_all = "lowercase")] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub enum ClientType { - /// A client that is capable of maintaining the confidentiality of their - /// credentials, or capable of secure client authentication using other - /// means. An example would be a secure server with restricted access to - /// the client credentials. - Confidential, - /// A client that is incapable of maintaining the confidentiality of their - /// credentials and cannot authenticate securely by any other means, such - /// as an installed application, or a web-browser based application. - Public, -} - -#[derive(Debug, Clone)] -pub struct Client { - id: Uuid, - ty: ClientType, - alias: Box, - secret: Option, - allowed_scopes: Box<[Box]>, - default_scopes: Option]>>, - redirect_uris: Box<[Url]>, - trusted: bool, -} - -impl PartialEq for Client { - fn eq(&self, other: &Self) -> bool { - self.id == other.id - } -} - -impl Eq for Client {} - -impl Hash for Client { - fn hash(&self, state: &mut H) { - state.write_u128(self.id.as_u128()) - } -} - -#[derive(Debug, Clone, Copy, Error)] -#[error("Confidential clients must have a secret, but it was not provided")] -pub enum CreateClientError { - #[error("Confidential clients must have a secret, but it was not provided")] - NoSecret, - #[error("Only confidential clients may be trusted")] - TrustedError, - #[error("Redirect URIs must not include a fragment component")] - UriFragment, - #[error("Redirect URIs must use HTTPS")] - NonHttpsUri, -} - -impl ResponseError for CreateClientError { - fn status_code(&self) -> StatusCode { - StatusCode::BAD_REQUEST - } -} - -impl Client { - pub fn new( - id: Uuid, - alias: &str, - ty: ClientType, - secret: Option<&str>, - allowed_scopes: Box<[Box]>, - default_scopes: Option]>>, - redirect_uris: &[Url], - trusted: bool, - ) -> Result> { - let secret = if let Some(secret) = secret { - Some(PasswordHash::new(secret)?) - } else { - None - }; - - if ty == ClientType::Confidential && secret.is_none() { - yeet!(CreateClientError::NoSecret.into()); - } - - if ty == ClientType::Public && trusted { - yeet!(CreateClientError::TrustedError.into()); - } - - for redirect_uri in redirect_uris { - if redirect_uri.scheme() != "https" { - yeet!(CreateClientError::NonHttpsUri.into()) - } - - if redirect_uri.fragment().is_some() { - yeet!(CreateClientError::UriFragment.into()) - } - } - - Ok(Self { - id, - alias: Box::from(alias), - ty, - secret, - allowed_scopes, - default_scopes, - redirect_uris: redirect_uris.into_iter().cloned().collect(), - trusted, - }) - } - - pub fn id(&self) -> Uuid { - self.id - } - - pub fn alias(&self) -> &str { - &self.alias - } - - pub fn client_type(&self) -> ClientType { - self.ty - } - - pub fn redirect_uris(&self) -> &[Url] { - &self.redirect_uris - } - - pub fn secret_hash(&self) -> Option<&[u8]> { - self.secret.as_ref().map(|s| s.hash()) - } - - pub fn secret_salt(&self) -> Option<&[u8]> { - self.secret.as_ref().map(|s| s.salt()) - } - - pub fn secret_version(&self) -> Option { - self.secret.as_ref().map(|s| s.version()) - } - - pub fn allowed_scopes(&self) -> String { - self.allowed_scopes.join(" ") - } - - pub fn default_scopes(&self) -> Option { - self.default_scopes.clone().map(|s| s.join(" ")) - } - - pub fn is_trusted(&self) -> bool { - self.trusted - } - - pub fn check_secret(&self, secret: &str) -> Option> { - self.secret.as_ref().map(|s| s.check_password(secret)) - } -} +use std::{hash::Hash, marker::PhantomData}; + +use actix_web::{http::StatusCode, ResponseError}; +use exun::{Expect, RawUnexpected}; +use raise::yeet; +use serde::{Deserialize, Serialize}; +use thiserror::Error; +use url::Url; +use uuid::Uuid; + +use crate::services::crypto::PasswordHash; + +/// There are two types of clients, based on their ability to maintain the +/// security of their client credentials. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] +#[sqlx(rename_all = "lowercase")] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum ClientType { + /// A client that is capable of maintaining the confidentiality of their + /// credentials, or capable of secure client authentication using other + /// means. An example would be a secure server with restricted access to + /// the client credentials. + Confidential, + /// A client that is incapable of maintaining the confidentiality of their + /// credentials and cannot authenticate securely by any other means, such + /// as an installed application, or a web-browser based application. + Public, +} + +#[derive(Debug, Clone)] +pub struct Client { + id: Uuid, + ty: ClientType, + alias: Box, + secret: Option, + allowed_scopes: Box<[Box]>, + default_scopes: Option]>>, + redirect_uris: Box<[Url]>, + trusted: bool, +} + +impl PartialEq for Client { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Eq for Client {} + +impl Hash for Client { + fn hash(&self, state: &mut H) { + state.write_u128(self.id.as_u128()) + } +} + +#[derive(Debug, Clone, Copy, Error)] +#[error("Confidential clients must have a secret, but it was not provided")] +pub enum CreateClientError { + #[error("Confidential clients must have a secret, but it was not provided")] + NoSecret, + #[error("Only confidential clients may be trusted")] + TrustedError, + #[error("Redirect URIs must not include a fragment component")] + UriFragment, + #[error("Redirect URIs must use HTTPS")] + NonHttpsUri, +} + +impl ResponseError for CreateClientError { + fn status_code(&self) -> StatusCode { + StatusCode::BAD_REQUEST + } +} + +impl Client { + pub fn new( + id: Uuid, + alias: &str, + ty: ClientType, + secret: Option<&str>, + allowed_scopes: Box<[Box]>, + default_scopes: Option]>>, + redirect_uris: &[Url], + trusted: bool, + ) -> Result> { + let secret = if let Some(secret) = secret { + Some(PasswordHash::new(secret)?) + } else { + None + }; + + if ty == ClientType::Confidential && secret.is_none() { + yeet!(CreateClientError::NoSecret.into()); + } + + if ty == ClientType::Public && trusted { + yeet!(CreateClientError::TrustedError.into()); + } + + for redirect_uri in redirect_uris { + if redirect_uri.scheme() != "https" { + yeet!(CreateClientError::NonHttpsUri.into()) + } + + if redirect_uri.fragment().is_some() { + yeet!(CreateClientError::UriFragment.into()) + } + } + + Ok(Self { + id, + alias: Box::from(alias), + ty, + secret, + allowed_scopes, + default_scopes, + redirect_uris: redirect_uris.into_iter().cloned().collect(), + trusted, + }) + } + + pub fn id(&self) -> Uuid { + self.id + } + + pub fn alias(&self) -> &str { + &self.alias + } + + pub fn client_type(&self) -> ClientType { + self.ty + } + + pub fn redirect_uris(&self) -> &[Url] { + &self.redirect_uris + } + + pub fn secret_hash(&self) -> Option<&[u8]> { + self.secret.as_ref().map(|s| s.hash()) + } + + pub fn secret_salt(&self) -> Option<&[u8]> { + self.secret.as_ref().map(|s| s.salt()) + } + + pub fn secret_version(&self) -> Option { + self.secret.as_ref().map(|s| s.version()) + } + + pub fn allowed_scopes(&self) -> String { + self.allowed_scopes.join(" ") + } + + pub fn default_scopes(&self) -> Option { + self.default_scopes.clone().map(|s| s.join(" ")) + } + + pub fn is_trusted(&self) -> bool { + self.trusted + } + + pub fn check_secret(&self, secret: &str) -> Option> { + self.secret.as_ref().map(|s| s.check_password(secret)) + } +} diff --git a/src/models/mod.rs b/src/models/mod.rs index 633f846..1379893 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,2 +1,2 @@ -pub mod client; -pub mod user; +pub mod client; +pub mod user; diff --git a/src/models/user.rs b/src/models/user.rs index 8555ee2..493a267 100644 --- a/src/models/user.rs +++ b/src/models/user.rs @@ -1,49 +1,49 @@ -use std::hash::Hash; - -use exun::RawUnexpected; -use uuid::Uuid; - -use crate::services::crypto::PasswordHash; - -#[derive(Debug, Clone)] -pub struct User { - pub id: Uuid, - pub username: Box, - pub password: PasswordHash, -} - -impl PartialEq for User { - fn eq(&self, other: &Self) -> bool { - self.id == other.id - } -} - -impl Eq for User {} - -impl Hash for User { - fn hash(&self, state: &mut H) { - state.write_u128(self.id.as_u128()) - } -} - -impl User { - pub fn username(&self) -> &str { - &self.username - } - - pub fn password_hash(&self) -> &[u8] { - self.password.hash() - } - - pub fn password_salt(&self) -> &[u8] { - self.password.salt() - } - - pub fn password_version(&self) -> u8 { - self.password.version() - } - - pub fn check_password(&self, password: &str) -> Result { - self.password.check_password(password) - } -} +use std::hash::Hash; + +use exun::RawUnexpected; +use uuid::Uuid; + +use crate::services::crypto::PasswordHash; + +#[derive(Debug, Clone)] +pub struct User { + pub id: Uuid, + pub username: Box, + pub password: PasswordHash, +} + +impl PartialEq for User { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Eq for User {} + +impl Hash for User { + fn hash(&self, state: &mut H) { + state.write_u128(self.id.as_u128()) + } +} + +impl User { + pub fn username(&self) -> &str { + &self.username + } + + pub fn password_hash(&self) -> &[u8] { + self.password.hash() + } + + pub fn password_salt(&self) -> &[u8] { + self.password.salt() + } + + pub fn password_version(&self) -> u8 { + self.password.version() + } + + pub fn check_password(&self, password: &str) -> Result { + self.password.check_password(password) + } +} diff --git a/src/resources/languages.rs b/src/resources/languages.rs index 8ef7553..b01daf9 100644 --- a/src/resources/languages.rs +++ b/src/resources/languages.rs @@ -1,67 +1,67 @@ -use std::collections::HashMap; -use std::path::PathBuf; - -use actix_web::{get, web, HttpResponse, Scope}; -use exun::RawUnexpected; -use ini::{Ini, Properties}; -use raise::yeet; -use unic_langid::subtags::Language; - -#[derive(Debug, Clone, PartialEq)] -pub struct Translations { - languages: HashMap, -} - -pub fn initialize() -> Result { - let mut translations = Translations { - languages: HashMap::new(), - }; - translations.refresh()?; - Ok(translations) -} - -impl Translations { - pub fn languages(&self) -> Box<[Language]> { - self.languages.keys().cloned().collect() - } - - pub fn get_message(&self, language: Language, key: &str) -> Option { - Some(self.languages.get(&language)?.get(key)?.to_owned()) - } - - pub fn refresh(&mut self) -> Result<(), RawUnexpected> { - let mut languages = HashMap::with_capacity(1); - for entry in PathBuf::from("static/languages").read_dir()? { - let entry = entry?; - if entry.file_type()?.is_dir() { - continue; - } - - let path = entry.path(); - let path = path.to_string_lossy(); - let Some(language) = path.as_bytes().get(0..2) else { yeet!(RawUnexpected::msg(format!("{} not long enough to be a language name", path))) }; - let language = Language::from_bytes(language)?; - let messages = Ini::load_from_file(entry.path())?.general_section().clone(); - - languages.insert(language, messages); - } - - self.languages = languages; - Ok(()) - } -} - -#[get("")] -pub async fn all_languages(translations: web::Data) -> HttpResponse { - HttpResponse::Ok().json( - translations - .languages() - .into_iter() - .map(|l| l.as_str()) - .collect::>(), - ) -} - -pub fn languages() -> Scope { - web::scope("/languages").service(all_languages) -} +use std::collections::HashMap; +use std::path::PathBuf; + +use actix_web::{get, web, HttpResponse, Scope}; +use exun::RawUnexpected; +use ini::{Ini, Properties}; +use raise::yeet; +use unic_langid::subtags::Language; + +#[derive(Debug, Clone, PartialEq)] +pub struct Translations { + languages: HashMap, +} + +pub fn initialize() -> Result { + let mut translations = Translations { + languages: HashMap::new(), + }; + translations.refresh()?; + Ok(translations) +} + +impl Translations { + pub fn languages(&self) -> Box<[Language]> { + self.languages.keys().cloned().collect() + } + + pub fn get_message(&self, language: Language, key: &str) -> Option { + Some(self.languages.get(&language)?.get(key)?.to_owned()) + } + + pub fn refresh(&mut self) -> Result<(), RawUnexpected> { + let mut languages = HashMap::with_capacity(1); + for entry in PathBuf::from("static/languages").read_dir()? { + let entry = entry?; + if entry.file_type()?.is_dir() { + continue; + } + + let path = entry.path(); + let path = path.to_string_lossy(); + let Some(language) = path.as_bytes().get(0..2) else { yeet!(RawUnexpected::msg(format!("{} not long enough to be a language name", path))) }; + let language = Language::from_bytes(language)?; + let messages = Ini::load_from_file(entry.path())?.general_section().clone(); + + languages.insert(language, messages); + } + + self.languages = languages; + Ok(()) + } +} + +#[get("")] +pub async fn all_languages(translations: web::Data) -> HttpResponse { + HttpResponse::Ok().json( + translations + .languages() + .into_iter() + .map(|l| l.as_str()) + .collect::>(), + ) +} + +pub fn languages() -> Scope { + web::scope("/languages").service(all_languages) +} diff --git a/src/resources/mod.rs b/src/resources/mod.rs index 9251d2c..d9f14ba 100644 --- a/src/resources/mod.rs +++ b/src/resources/mod.rs @@ -1,4 +1,4 @@ -pub mod languages; -pub mod scripts; -pub mod style; -pub mod templates; +pub mod languages; +pub mod scripts; +pub mod style; +pub mod templates; diff --git a/src/resources/scripts.rs b/src/resources/scripts.rs index 1b27859..66b9693 100644 --- a/src/resources/scripts.rs +++ b/src/resources/scripts.rs @@ -1,38 +1,38 @@ -use std::path::Path; - -use actix_web::{get, http::StatusCode, web, HttpResponse, ResponseError}; -use exun::{Expect, ResultErrorExt}; -use path_clean::clean; -use raise::yeet; -use serde::Serialize; -use thiserror::Error; - -#[derive(Debug, Clone, Error, Serialize)] -pub enum LoadScriptError { - #[error("The requested script does not exist")] - FileNotFound(Box), -} - -impl ResponseError for LoadScriptError { - fn status_code(&self) -> StatusCode { - match self { - Self::FileNotFound(..) => StatusCode::NOT_FOUND, - } - } -} - -fn load(script: &str) -> Result> { - let path = clean(format!("static/scripts/{}.js", script)); - if !path.exists() { - yeet!(LoadScriptError::FileNotFound(path.into()).into()); - } - let js = std::fs::read_to_string(format!("static/scripts/{}.js", script)).unexpect()?; - Ok(js) -} - -#[get("/{script}.js")] -pub async fn get_js(script: web::Path>) -> Result { - let js = load(&script).map_err(|e| e.unwrap())?; - let response = HttpResponse::Ok().content_type("text/javascript").body(js); - Ok(response) -} +use std::path::Path; + +use actix_web::{get, http::StatusCode, web, HttpResponse, ResponseError}; +use exun::{Expect, ResultErrorExt}; +use path_clean::clean; +use raise::yeet; +use serde::Serialize; +use thiserror::Error; + +#[derive(Debug, Clone, Error, Serialize)] +pub enum LoadScriptError { + #[error("The requested script does not exist")] + FileNotFound(Box), +} + +impl ResponseError for LoadScriptError { + fn status_code(&self) -> StatusCode { + match self { + Self::FileNotFound(..) => StatusCode::NOT_FOUND, + } + } +} + +fn load(script: &str) -> Result> { + let path = clean(format!("static/scripts/{}.js", script)); + if !path.exists() { + yeet!(LoadScriptError::FileNotFound(path.into()).into()); + } + let js = std::fs::read_to_string(format!("static/scripts/{}.js", script)).unexpect()?; + Ok(js) +} + +#[get("/{script}.js")] +pub async fn get_js(script: web::Path>) -> Result { + let js = load(&script).map_err(|e| e.unwrap())?; + let response = HttpResponse::Ok().content_type("text/javascript").body(js); + Ok(response) +} diff --git a/src/resources/style.rs b/src/resources/style.rs index 3ea56d2..8b21dc4 100644 --- a/src/resources/style.rs +++ b/src/resources/style.rs @@ -1,54 +1,54 @@ -use std::path::Path; - -use actix_web::{get, http::StatusCode, web, HttpResponse, ResponseError}; -use exun::{Expect, ResultErrorExt}; -use grass::OutputStyle; -use path_clean::clean; -use raise::yeet; -use serde::Serialize; -use thiserror::Error; - -fn output_style() -> OutputStyle { - if cfg!(debug_assertions) { - OutputStyle::Expanded - } else { - OutputStyle::Compressed - } -} - -fn options() -> grass::Options<'static> { - grass::Options::default() - .load_path("static/style") - .style(output_style()) -} - -#[derive(Debug, Clone, Error, Serialize)] -pub enum LoadStyleError { - #[error("The requested stylesheet was not found")] - FileNotFound(Box), -} - -impl ResponseError for LoadStyleError { - fn status_code(&self) -> StatusCode { - match self { - Self::FileNotFound(..) => StatusCode::NOT_FOUND, - } - } -} - -pub fn load(stylesheet: &str) -> Result> { - let options = options(); - let path = clean(format!("static/style/{}.scss", stylesheet)); - if !path.exists() { - yeet!(LoadStyleError::FileNotFound(path.into()).into()); - } - let css = grass::from_path(format!("static/style/{}.scss", stylesheet), &options).unexpect()?; - Ok(css) -} - -#[get("/{stylesheet}.css")] -pub async fn get_css(stylesheet: web::Path>) -> Result { - let css = load(&stylesheet).map_err(|e| e.unwrap())?; - let response = HttpResponse::Ok().content_type("text/css").body(css); - Ok(response) -} +use std::path::Path; + +use actix_web::{get, http::StatusCode, web, HttpResponse, ResponseError}; +use exun::{Expect, ResultErrorExt}; +use grass::OutputStyle; +use path_clean::clean; +use raise::yeet; +use serde::Serialize; +use thiserror::Error; + +fn output_style() -> OutputStyle { + if cfg!(debug_assertions) { + OutputStyle::Expanded + } else { + OutputStyle::Compressed + } +} + +fn options() -> grass::Options<'static> { + grass::Options::default() + .load_path("static/style") + .style(output_style()) +} + +#[derive(Debug, Clone, Error, Serialize)] +pub enum LoadStyleError { + #[error("The requested stylesheet was not found")] + FileNotFound(Box), +} + +impl ResponseError for LoadStyleError { + fn status_code(&self) -> StatusCode { + match self { + Self::FileNotFound(..) => StatusCode::NOT_FOUND, + } + } +} + +pub fn load(stylesheet: &str) -> Result> { + let options = options(); + let path = clean(format!("static/style/{}.scss", stylesheet)); + if !path.exists() { + yeet!(LoadStyleError::FileNotFound(path.into()).into()); + } + let css = grass::from_path(format!("static/style/{}.scss", stylesheet), &options).unexpect()?; + Ok(css) +} + +#[get("/{stylesheet}.css")] +pub async fn get_css(stylesheet: web::Path>) -> Result { + let css = load(&stylesheet).map_err(|e| e.unwrap())?; + let response = HttpResponse::Ok().content_type("text/css").body(css); + Ok(response) +} diff --git a/src/resources/templates.rs b/src/resources/templates.rs index 9168fb9..baf2ee8 100644 --- a/src/resources/templates.rs +++ b/src/resources/templates.rs @@ -1,101 +1,101 @@ -use std::collections::HashMap; - -use exun::{RawUnexpected, ResultErrorExt}; -use raise::yeet; -use serde::Serialize; -use tera::{Function, Tera, Value}; -use unic_langid::subtags::Language; - -use crate::api::AuthorizationParameters; - -use super::languages; - -fn make_msg(language: Language, translations: languages::Translations) -> impl Function { - Box::new( - move |args: &HashMap| -> tera::Result { - let Some(key) = args.get("key") else { yeet!("No parameter 'key' provided".into()) }; - let Some(key) = key.as_str() else { yeet!(format!("{} is not a string", key).into()) }; - let Some(value) = translations.get_message(language, key) else { yeet!(format!("{} does not exist", key).into()) }; - Ok(Value::String(value)) - }, - ) -} - -fn extend_tera( - tera: &Tera, - language: Language, - translations: languages::Translations, -) -> Result { - let mut new_tera = initialize()?; - new_tera.extend(tera)?; - new_tera.register_function("msg", make_msg(language, translations)); - Ok(new_tera) -} - -pub fn initialize() -> tera::Result { - let tera = Tera::new("static/templates/*")?; - Ok(tera) -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)] -#[serde(rename_all = "camelCase")] -pub enum ErrorPage { - InvalidRequest, - ClientNotFound, - MissingRedirectUri, - InvalidRedirectUri, - InternalServerError, -} - -pub fn error_page( - tera: &Tera, - language: Language, - mut translations: languages::Translations, - error: ErrorPage, -) -> Result { - translations.refresh()?; - let mut tera = extend_tera(tera, language, translations)?; - tera.full_reload()?; - - let error = serde_variant::to_variant_name(&error)?; - let header = format!("errorHeader_{error}"); - let message = format!("errorMessage_{error}"); - - let mut context = tera::Context::new(); - context.insert("lang", language.as_str()); - context.insert("errorHeader", &header); - context.insert("errormessage", &message); - - tera.render("error.html", &context).unexpect() -} - -pub fn login_page( - tera: &Tera, - params: &AuthorizationParameters, - language: Language, - mut translations: languages::Translations, -) -> Result { - translations.refresh()?; - let mut tera = extend_tera(tera, language, translations)?; - tera.full_reload()?; - let mut context = tera::Context::new(); - context.insert("lang", language.as_str()); - context.insert("params", &serde_urlencoded::to_string(params)?); - tera.render("login.html", &context).unexpect() -} - -pub fn login_error_page( - tera: &Tera, - params: &AuthorizationParameters, - language: Language, - mut translations: languages::Translations, -) -> Result { - translations.refresh()?; - let mut tera = extend_tera(tera, language, translations)?; - tera.full_reload()?; - let mut context = tera::Context::new(); - context.insert("lang", language.as_str()); - context.insert("params", &serde_urlencoded::to_string(params)?); - context.insert("errorMessage", "loginErrorMessage"); - tera.render("login.html", &context).unexpect() -} +use std::collections::HashMap; + +use exun::{RawUnexpected, ResultErrorExt}; +use raise::yeet; +use serde::Serialize; +use tera::{Function, Tera, Value}; +use unic_langid::subtags::Language; + +use crate::api::AuthorizationParameters; + +use super::languages; + +fn make_msg(language: Language, translations: languages::Translations) -> impl Function { + Box::new( + move |args: &HashMap| -> tera::Result { + let Some(key) = args.get("key") else { yeet!("No parameter 'key' provided".into()) }; + let Some(key) = key.as_str() else { yeet!(format!("{} is not a string", key).into()) }; + let Some(value) = translations.get_message(language, key) else { yeet!(format!("{} does not exist", key).into()) }; + Ok(Value::String(value)) + }, + ) +} + +fn extend_tera( + tera: &Tera, + language: Language, + translations: languages::Translations, +) -> Result { + let mut new_tera = initialize()?; + new_tera.extend(tera)?; + new_tera.register_function("msg", make_msg(language, translations)); + Ok(new_tera) +} + +pub fn initialize() -> tera::Result { + let tera = Tera::new("static/templates/*")?; + Ok(tera) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum ErrorPage { + InvalidRequest, + ClientNotFound, + MissingRedirectUri, + InvalidRedirectUri, + InternalServerError, +} + +pub fn error_page( + tera: &Tera, + language: Language, + mut translations: languages::Translations, + error: ErrorPage, +) -> Result { + translations.refresh()?; + let mut tera = extend_tera(tera, language, translations)?; + tera.full_reload()?; + + let error = serde_variant::to_variant_name(&error)?; + let header = format!("errorHeader_{error}"); + let message = format!("errorMessage_{error}"); + + let mut context = tera::Context::new(); + context.insert("lang", language.as_str()); + context.insert("errorHeader", &header); + context.insert("errormessage", &message); + + tera.render("error.html", &context).unexpect() +} + +pub fn login_page( + tera: &Tera, + params: &AuthorizationParameters, + language: Language, + mut translations: languages::Translations, +) -> Result { + translations.refresh()?; + let mut tera = extend_tera(tera, language, translations)?; + tera.full_reload()?; + let mut context = tera::Context::new(); + context.insert("lang", language.as_str()); + context.insert("params", &serde_urlencoded::to_string(params)?); + tera.render("login.html", &context).unexpect() +} + +pub fn login_error_page( + tera: &Tera, + params: &AuthorizationParameters, + language: Language, + mut translations: languages::Translations, +) -> Result { + translations.refresh()?; + let mut tera = extend_tera(tera, language, translations)?; + tera.full_reload()?; + let mut context = tera::Context::new(); + context.insert("lang", language.as_str()); + context.insert("params", &serde_urlencoded::to_string(params)?); + context.insert("errorMessage", "loginErrorMessage"); + tera.render("login.html", &context).unexpect() +} diff --git a/src/scopes/admin.rs b/src/scopes/admin.rs index 1e13b85..31e7880 100644 --- a/src/scopes/admin.rs +++ b/src/scopes/admin.rs @@ -1,28 +1,28 @@ -use std::fmt::{self, Display}; - -use crate::models::{client::Client, user::User}; - -use super::{Action, Scope}; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct Admin; - -impl Display for Admin { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("admin") - } -} - -impl Scope for Admin { - fn parse_modifiers(_modifiers: &str) -> Result> { - Ok(Self) - } - - fn has_user_permission(&self, _: &User, _: &Action) -> bool { - true - } - - fn has_client_permission(&self, _: &User, _: &Action) -> bool { - true - } -} +use std::fmt::{self, Display}; + +use crate::models::{client::Client, user::User}; + +use super::{Action, Scope}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Admin; + +impl Display for Admin { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("admin") + } +} + +impl Scope for Admin { + fn parse_modifiers(_modifiers: &str) -> Result> { + Ok(Self) + } + + fn has_user_permission(&self, _: &User, _: &Action) -> bool { + true + } + + fn has_client_permission(&self, _: &User, _: &Action) -> bool { + true + } +} diff --git a/src/scopes/mod.rs b/src/scopes/mod.rs index fb7780f..25296fd 100644 --- a/src/scopes/mod.rs +++ b/src/scopes/mod.rs @@ -1,128 +1,128 @@ -use std::collections::HashSet; - -use self::admin::Admin; -use crate::models::{client::Client, user::User}; - -mod admin; - -/// The action which was attempted on a resource -pub enum Action { - Create(T), - Read(T), - Update(T, T), - Delete(T), -} - -trait ScopeSuperSet { - fn is_superset_of(&self, other: &Self) -> bool; -} - -trait Scope: ToString { - /// Parse a scope of the format: `{Scope::NAME}:{modifiers}` - fn parse_modifiers(modifiers: &str) -> Result> - where - Self: Sized; - - /// Returns `true` if and only if the given `user` is allowed to take the - /// given `action` with this scope - fn has_user_permission(&self, user: &User, action: &Action) -> bool; - - // Returns `true` if and only if the given `user` is allowed to take the - /// given `action` with this scope - fn has_client_permission(&self, user: &User, action: &Action) -> bool; -} - -pub struct ParseScopeError { - scope: Box, - error: ParseScopeErrorType, -} - -impl ParseScopeError { - fn invalid_type(scope: &str, scope_type: &str) -> Self { - let scope = scope.into(); - let error = ParseScopeErrorType::InvalidType(scope_type.into()); - Self { scope, error } - } -} - -pub enum ParseScopeErrorType { - InvalidType(Box), - InvalidModifiers(Box), -} - -fn parse_scope(scope: &str) -> Result, ParseScopeError> { - let mut split = scope.split(':'); - let scope_type = split.next().unwrap(); - let _modifiers: String = split.collect(); - - match scope_type { - "admin" => Ok(Box::new(Admin)), - _ => Err(ParseScopeError::invalid_type(scope, scope_type)), - } -} - -fn parse_scopes(scopes: &str) -> Result>, ParseScopeError> { - scopes - .split_whitespace() - .map(|scope| parse_scope(scope)) - .collect() -} - -fn parse_scopes_errors( - results: &[Result, ParseScopeError>], -) -> Vec<&ParseScopeError> { - let mut errors = Vec::with_capacity(results.len()); - for result in results { - if let Err(pse) = result { - errors.push(pse) - } - } - - errors -} - -/// Returns `true` if and only if all values in `left_scopes` are contained in -/// `right_scopes`. -pub fn is_subset_of(left_scopes: &str, right_scopes: &str) -> bool { - let right_scopes: HashSet<&str> = right_scopes.split_whitespace().collect(); - - for scope in left_scopes.split_whitespace() { - if !right_scopes.contains(scope) { - return false; - } - } - - true -} - -pub fn has_user_permission( - user: User, - action: Action, - client_scopes: &str, -) -> Result { - let scopes = parse_scopes(client_scopes)?; - - for scope in scopes { - if scope.has_user_permission(&user, &action) { - return Ok(true); - } - } - - Ok(false) -} - -pub fn has_client_permission( - user: User, - action: Action, - client_scopes: &str, -) -> Result { - let scopes = parse_scopes(client_scopes)?; - - for scope in scopes { - if scope.has_client_permission(&user, &action) { - return Ok(true); - } - } - - Ok(false) -} +use std::collections::HashSet; + +use self::admin::Admin; +use crate::models::{client::Client, user::User}; + +mod admin; + +/// The action which was attempted on a resource +pub enum Action { + Create(T), + Read(T), + Update(T, T), + Delete(T), +} + +trait ScopeSuperSet { + fn is_superset_of(&self, other: &Self) -> bool; +} + +trait Scope: ToString { + /// Parse a scope of the format: `{Scope::NAME}:{modifiers}` + fn parse_modifiers(modifiers: &str) -> Result> + where + Self: Sized; + + /// Returns `true` if and only if the given `user` is allowed to take the + /// given `action` with this scope + fn has_user_permission(&self, user: &User, action: &Action) -> bool; + + // Returns `true` if and only if the given `user` is allowed to take the + /// given `action` with this scope + fn has_client_permission(&self, user: &User, action: &Action) -> bool; +} + +pub struct ParseScopeError { + scope: Box, + error: ParseScopeErrorType, +} + +impl ParseScopeError { + fn invalid_type(scope: &str, scope_type: &str) -> Self { + let scope = scope.into(); + let error = ParseScopeErrorType::InvalidType(scope_type.into()); + Self { scope, error } + } +} + +pub enum ParseScopeErrorType { + InvalidType(Box), + InvalidModifiers(Box), +} + +fn parse_scope(scope: &str) -> Result, ParseScopeError> { + let mut split = scope.split(':'); + let scope_type = split.next().unwrap(); + let _modifiers: String = split.collect(); + + match scope_type { + "admin" => Ok(Box::new(Admin)), + _ => Err(ParseScopeError::invalid_type(scope, scope_type)), + } +} + +fn parse_scopes(scopes: &str) -> Result>, ParseScopeError> { + scopes + .split_whitespace() + .map(|scope| parse_scope(scope)) + .collect() +} + +fn parse_scopes_errors( + results: &[Result, ParseScopeError>], +) -> Vec<&ParseScopeError> { + let mut errors = Vec::with_capacity(results.len()); + for result in results { + if let Err(pse) = result { + errors.push(pse) + } + } + + errors +} + +/// Returns `true` if and only if all values in `left_scopes` are contained in +/// `right_scopes`. +pub fn is_subset_of(left_scopes: &str, right_scopes: &str) -> bool { + let right_scopes: HashSet<&str> = right_scopes.split_whitespace().collect(); + + for scope in left_scopes.split_whitespace() { + if !right_scopes.contains(scope) { + return false; + } + } + + true +} + +pub fn has_user_permission( + user: User, + action: Action, + client_scopes: &str, +) -> Result { + let scopes = parse_scopes(client_scopes)?; + + for scope in scopes { + if scope.has_user_permission(&user, &action) { + return Ok(true); + } + } + + Ok(false) +} + +pub fn has_client_permission( + user: User, + action: Action, + client_scopes: &str, +) -> Result { + let scopes = parse_scopes(client_scopes)?; + + for scope in scopes { + if scope.has_client_permission(&user, &action) { + return Ok(true); + } + } + + Ok(false) +} diff --git a/src/services/authorization.rs b/src/services/authorization.rs index bfbbb5a..4e6ef35 100644 --- a/src/services/authorization.rs +++ b/src/services/authorization.rs @@ -1,82 +1,82 @@ -use actix_web::{ - error::ParseError, - http::header::{self, Header, HeaderName, HeaderValue, InvalidHeaderValue, TryIntoHeaderValue}, -}; -use base64::Engine; -use raise::yeet; - -#[derive(Clone)] -pub struct BasicAuthorization { - username: Box, - password: Box, -} - -impl TryIntoHeaderValue for BasicAuthorization { - type Error = InvalidHeaderValue; - - fn try_into_value(self) -> Result { - let username = self.username; - let password = self.password; - let utf8 = format!("{username}:{password}"); - let b64 = base64::engine::general_purpose::STANDARD.encode(utf8); - let value = format!("Basic {b64}"); - HeaderValue::from_str(&value) - } -} - -impl Header for BasicAuthorization { - fn name() -> HeaderName { - header::AUTHORIZATION - } - - fn parse(msg: &M) -> Result { - let Some(value) = msg.headers().get(Self::name()) else { - yeet!(ParseError::Header) - }; - - let Ok(value) = value.to_str() else { - yeet!(ParseError::Header) - }; - - if !value.starts_with("Basic") { - yeet!(ParseError::Header); - } - - let value: String = value - .chars() - .skip(5) - .skip_while(|ch| ch.is_whitespace()) - .collect(); - - if value.is_empty() { - yeet!(ParseError::Header); - } - - let Ok(bytes) = base64::engine::general_purpose::STANDARD.decode(value) else { - yeet!(ParseError::Header) - }; - - let Ok(value) = String::from_utf8(bytes) else { - yeet!(ParseError::Header) - }; - - let mut parts = value.split(':'); - let username = Box::from(parts.next().unwrap()); - let Some(password) = parts.next() else { - yeet!(ParseError::Header) - }; - let password = Box::from(password); - - Ok(Self { username, password }) - } -} - -impl BasicAuthorization { - pub fn username(&self) -> &str { - &self.username - } - - pub fn password(&self) -> &str { - &self.password - } -} +use actix_web::{ + error::ParseError, + http::header::{self, Header, HeaderName, HeaderValue, InvalidHeaderValue, TryIntoHeaderValue}, +}; +use base64::Engine; +use raise::yeet; + +#[derive(Clone)] +pub struct BasicAuthorization { + username: Box, + password: Box, +} + +impl TryIntoHeaderValue for BasicAuthorization { + type Error = InvalidHeaderValue; + + fn try_into_value(self) -> Result { + let username = self.username; + let password = self.password; + let utf8 = format!("{username}:{password}"); + let b64 = base64::engine::general_purpose::STANDARD.encode(utf8); + let value = format!("Basic {b64}"); + HeaderValue::from_str(&value) + } +} + +impl Header for BasicAuthorization { + fn name() -> HeaderName { + header::AUTHORIZATION + } + + fn parse(msg: &M) -> Result { + let Some(value) = msg.headers().get(Self::name()) else { + yeet!(ParseError::Header) + }; + + let Ok(value) = value.to_str() else { + yeet!(ParseError::Header) + }; + + if !value.starts_with("Basic") { + yeet!(ParseError::Header); + } + + let value: String = value + .chars() + .skip(5) + .skip_while(|ch| ch.is_whitespace()) + .collect(); + + if value.is_empty() { + yeet!(ParseError::Header); + } + + let Ok(bytes) = base64::engine::general_purpose::STANDARD.decode(value) else { + yeet!(ParseError::Header) + }; + + let Ok(value) = String::from_utf8(bytes) else { + yeet!(ParseError::Header) + }; + + let mut parts = value.split(':'); + let username = Box::from(parts.next().unwrap()); + let Some(password) = parts.next() else { + yeet!(ParseError::Header) + }; + let password = Box::from(password); + + Ok(Self { username, password }) + } +} + +impl BasicAuthorization { + pub fn username(&self) -> &str { + &self.username + } + + pub fn password(&self) -> &str { + &self.password + } +} diff --git a/src/services/config.rs b/src/services/config.rs index 6468126..932f38f 100644 --- a/src/services/config.rs +++ b/src/services/config.rs @@ -1,74 +1,74 @@ -use std::{ - fmt::{self, Display}, - str::FromStr, -}; - -use exun::RawUnexpected; -use parking_lot::RwLock; -use serde::Deserialize; -use thiserror::Error; -use url::Url; - -static ENVIRONMENT: RwLock = RwLock::new(Environment::Local); - -#[derive(Debug, Clone, Deserialize)] -pub struct Config { - pub id: Box, - pub url: Url, -} - -pub fn get_config() -> Result { - let env = get_environment(); - let path = format!("static/config/{env}.toml"); - let string = std::fs::read_to_string(path)?; - let config = toml::from_str(&string)?; - Ok(config) -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum Environment { - Local, - Dev, - Staging, - Production, -} - -impl Display for Environment { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Local => f.write_str("local"), - Self::Dev => f.write_str("dev"), - Self::Staging => f.write_str("staging"), - Self::Production => f.write_str("prod"), - } - } -} - -#[derive(Debug, Clone, Error)] -#[error("Expected one of the following environments: local, dev, staging, prod. Found {string}")] -pub struct ParseEnvironmentError { - string: Box, -} - -impl FromStr for Environment { - type Err = ParseEnvironmentError; - - fn from_str(s: &str) -> Result { - match s { - "local" => Ok(Self::Local), - "dev" => Ok(Self::Dev), - "staging" => Ok(Self::Staging), - "prod" => Ok(Self::Production), - _ => Err(ParseEnvironmentError { string: s.into() }), - } - } -} - -pub fn set_environment(env: Environment) { - let mut env_ptr = ENVIRONMENT.write(); - *env_ptr = env; -} - -fn get_environment() -> Environment { - ENVIRONMENT.read().clone() -} +use std::{ + fmt::{self, Display}, + str::FromStr, +}; + +use exun::RawUnexpected; +use parking_lot::RwLock; +use serde::Deserialize; +use thiserror::Error; +use url::Url; + +static ENVIRONMENT: RwLock = RwLock::new(Environment::Local); + +#[derive(Debug, Clone, Deserialize)] +pub struct Config { + pub id: Box, + pub url: Url, +} + +pub fn get_config() -> Result { + let env = get_environment(); + let path = format!("static/config/{env}.toml"); + let string = std::fs::read_to_string(path)?; + let config = toml::from_str(&string)?; + Ok(config) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Environment { + Local, + Dev, + Staging, + Production, +} + +impl Display for Environment { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Local => f.write_str("local"), + Self::Dev => f.write_str("dev"), + Self::Staging => f.write_str("staging"), + Self::Production => f.write_str("prod"), + } + } +} + +#[derive(Debug, Clone, Error)] +#[error("Expected one of the following environments: local, dev, staging, prod. Found {string}")] +pub struct ParseEnvironmentError { + string: Box, +} + +impl FromStr for Environment { + type Err = ParseEnvironmentError; + + fn from_str(s: &str) -> Result { + match s { + "local" => Ok(Self::Local), + "dev" => Ok(Self::Dev), + "staging" => Ok(Self::Staging), + "prod" => Ok(Self::Production), + _ => Err(ParseEnvironmentError { string: s.into() }), + } + } +} + +pub fn set_environment(env: Environment) { + let mut env_ptr = ENVIRONMENT.write(); + *env_ptr = env; +} + +fn get_environment() -> Environment { + ENVIRONMENT.read().clone() +} diff --git a/src/services/crypto.rs b/src/services/crypto.rs index 5fce403..0107374 100644 --- a/src/services/crypto.rs +++ b/src/services/crypto.rs @@ -1,97 +1,97 @@ -use std::hash::Hash; - -use argon2::{hash_raw, verify_raw}; -use exun::RawUnexpected; - -use crate::services::secrets::pepper; - -/// The configuration used for hashing and verifying passwords -/// -/// # Example -/// -/// ``` -/// use crate::services::secrets; -/// -/// let pepper = secrets::pepper(); -/// let config = config(&pepper); -/// ``` -fn config<'a>(pepper: &'a [u8]) -> argon2::Config<'a> { - argon2::Config { - hash_length: 32, - lanes: 4, - mem_cost: 5333, - time_cost: 4, - secret: pepper, - - ad: &[], - thread_mode: argon2::ThreadMode::Sequential, - variant: argon2::Variant::Argon2i, - version: argon2::Version::Version13, - } -} - -/// A password hash and salt for a user -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct PasswordHash { - hash: Box<[u8]>, - salt: Box<[u8]>, - version: u8, -} - -impl Hash for PasswordHash { - fn hash(&self, state: &mut H) { - state.write(&self.hash) - } -} - -impl PasswordHash { - /// Hash a password using Argon2 - pub fn new(password: &str) -> Result { - let password = password.as_bytes(); - - let salt: [u8; 32] = rand::random(); - let salt = Box::from(salt); - let pepper = pepper()?; - let hash = hash_raw(password, &salt, &config(&pepper))?.into_boxed_slice(); - - Ok(Self { - hash, - salt, - version: 0, - }) - } - - /// Create this structure from a given hash and salt - pub fn from_fields(hash: &[u8], salt: &[u8], version: u8) -> Self { - Self { - hash: Box::from(hash), - salt: Box::from(salt), - version, - } - } - - /// Get the password hash - pub fn hash(&self) -> &[u8] { - &self.hash - } - - /// Get the salt used for the hash - pub fn salt(&self) -> &[u8] { - &self.salt - } - - pub fn version(&self) -> u8 { - self.version - } - - /// Check if the given password is the one that was hashed - pub fn check_password(&self, password: &str) -> Result { - let pepper = pepper()?; - Ok(verify_raw( - password.as_bytes(), - &self.salt, - &self.hash, - &config(&pepper), - )?) - } -} +use std::hash::Hash; + +use argon2::{hash_raw, verify_raw}; +use exun::RawUnexpected; + +use crate::services::secrets::pepper; + +/// The configuration used for hashing and verifying passwords +/// +/// # Example +/// +/// ``` +/// use crate::services::secrets; +/// +/// let pepper = secrets::pepper(); +/// let config = config(&pepper); +/// ``` +fn config<'a>(pepper: &'a [u8]) -> argon2::Config<'a> { + argon2::Config { + hash_length: 32, + lanes: 4, + mem_cost: 5333, + time_cost: 4, + secret: pepper, + + ad: &[], + thread_mode: argon2::ThreadMode::Sequential, + variant: argon2::Variant::Argon2i, + version: argon2::Version::Version13, + } +} + +/// A password hash and salt for a user +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PasswordHash { + hash: Box<[u8]>, + salt: Box<[u8]>, + version: u8, +} + +impl Hash for PasswordHash { + fn hash(&self, state: &mut H) { + state.write(&self.hash) + } +} + +impl PasswordHash { + /// Hash a password using Argon2 + pub fn new(password: &str) -> Result { + let password = password.as_bytes(); + + let salt: [u8; 32] = rand::random(); + let salt = Box::from(salt); + let pepper = pepper()?; + let hash = hash_raw(password, &salt, &config(&pepper))?.into_boxed_slice(); + + Ok(Self { + hash, + salt, + version: 0, + }) + } + + /// Create this structure from a given hash and salt + pub fn from_fields(hash: &[u8], salt: &[u8], version: u8) -> Self { + Self { + hash: Box::from(hash), + salt: Box::from(salt), + version, + } + } + + /// Get the password hash + pub fn hash(&self) -> &[u8] { + &self.hash + } + + /// Get the salt used for the hash + pub fn salt(&self) -> &[u8] { + &self.salt + } + + pub fn version(&self) -> u8 { + self.version + } + + /// Check if the given password is the one that was hashed + pub fn check_password(&self, password: &str) -> Result { + let pepper = pepper()?; + Ok(verify_raw( + password.as_bytes(), + &self.salt, + &self.hash, + &config(&pepper), + )?) + } +} diff --git a/src/services/db.rs b/src/services/db.rs index f811d79..e3cb48b 100644 --- a/src/services/db.rs +++ b/src/services/db.rs @@ -1,15 +1,15 @@ -use exun::{RawUnexpected, ResultErrorExt}; -use sqlx::MySqlPool; - -mod client; -mod jwt; -mod user; - -pub use self::jwt::*; -pub use client::*; -pub use user::*; - -/// Intialize the connection pool -pub async fn initialize(db_url: &str) -> Result { - MySqlPool::connect(db_url).await.unexpect() -} +use exun::{RawUnexpected, ResultErrorExt}; +use sqlx::MySqlPool; + +mod client; +mod jwt; +mod user; + +pub use self::jwt::*; +pub use client::*; +pub use user::*; + +/// Intialize the connection pool +pub async fn initialize(db_url: &str) -> Result { + MySqlPool::connect(db_url).await.unexpect() +} diff --git a/src/services/db/client.rs b/src/services/db/client.rs index b8942e9..1ad97b1 100644 --- a/src/services/db/client.rs +++ b/src/services/db/client.rs @@ -1,392 +1,392 @@ -use std::str::FromStr; - -use exun::{RawUnexpected, ResultErrorExt}; -use sqlx::{ - mysql::MySqlQueryResult, query, query_as, query_scalar, Executor, FromRow, MySql, Transaction, -}; -use url::Url; -use uuid::Uuid; - -use crate::{ - models::client::{Client, ClientType}, - services::crypto::PasswordHash, -}; - -#[derive(Debug, Clone, FromRow)] -pub struct ClientRow { - pub id: Uuid, - pub alias: String, - pub client_type: ClientType, - pub allowed_scopes: String, - pub default_scopes: Option, - pub is_trusted: bool, -} - -#[derive(Clone, FromRow)] -struct HashRow { - secret_hash: Option>, - secret_salt: Option>, - secret_version: Option, -} - -pub async fn client_id_exists<'c>( - executor: impl Executor<'c, Database = MySql>, - id: Uuid, -) -> Result { - query_scalar!( - r"SELECT EXISTS(SELECT id FROM clients WHERE id = ?) as `e: bool`", - id - ) - .fetch_one(executor) - .await - .unexpect() -} - -pub async fn client_alias_exists<'c>( - executor: impl Executor<'c, Database = MySql>, - alias: &str, -) -> Result { - query_scalar!( - "SELECT EXISTS(SELECT alias FROM clients WHERE alias = ?) as `e: bool`", - alias - ) - .fetch_one(executor) - .await - .unexpect() -} - -pub async fn get_client_id_by_alias<'c>( - executor: impl Executor<'c, Database = MySql>, - alias: &str, -) -> Result, RawUnexpected> { - query_scalar!( - "SELECT id as `id: Uuid` FROM clients WHERE alias = ?", - alias - ) - .fetch_optional(executor) - .await - .unexpect() -} - -pub async fn get_client_response<'c>( - executor: impl Executor<'c, Database = MySql>, - id: Uuid, -) -> Result, RawUnexpected> { - let record = query_as!( - ClientRow, - r"SELECT id as `id: Uuid`, - alias, - type as `client_type: ClientType`, - allowed_scopes, - default_scopes, - trusted as `is_trusted: bool` - FROM clients WHERE id = ?", - id - ) - .fetch_optional(executor) - .await?; - - Ok(record) -} - -pub async fn get_client_alias<'c>( - executor: impl Executor<'c, Database = MySql>, - id: Uuid, -) -> Result>, RawUnexpected> { - let alias = query_scalar!("SELECT alias FROM clients WHERE id = ?", id) - .fetch_optional(executor) - .await - .unexpect()?; - - Ok(alias.map(String::into_boxed_str)) -} - -pub async fn get_client_type<'c>( - executor: impl Executor<'c, Database = MySql>, - id: Uuid, -) -> Result, RawUnexpected> { - let ty = query_scalar!( - "SELECT type as `type: ClientType` FROM clients WHERE id = ?", - id - ) - .fetch_optional(executor) - .await - .unexpect()?; - - Ok(ty) -} - -pub async fn get_client_allowed_scopes<'c>( - executor: impl Executor<'c, Database = MySql>, - id: Uuid, -) -> Result>, RawUnexpected> { - let scopes = query_scalar!("SELECT allowed_scopes FROM clients WHERE id = ?", id) - .fetch_optional(executor) - .await?; - - Ok(scopes.map(Box::from)) -} - -pub async fn get_client_default_scopes<'c>( - executor: impl Executor<'c, Database = MySql>, - id: Uuid, -) -> Result>>, RawUnexpected> { - let scopes = query_scalar!("SELECT default_scopes FROM clients WHERE id = ?", id) - .fetch_optional(executor) - .await?; - - Ok(scopes.map(|s| s.map(Box::from))) -} - -pub async fn get_client_secret<'c>( - executor: impl Executor<'c, Database = MySql>, - id: Uuid, -) -> Result, RawUnexpected> { - let hash = query_as!( - HashRow, - r"SELECT secret_hash, secret_salt, secret_version - FROM clients WHERE id = ?", - id - ) - .fetch_optional(executor) - .await?; - - let Some(hash) = hash else { return Ok(None) }; - let Some(version) = hash.secret_version else { return Ok(None) }; - let Some(salt) = hash.secret_hash else { return Ok(None) }; - let Some(hash) = hash.secret_salt else { return Ok(None) }; - - let hash = PasswordHash::from_fields(&hash, &salt, version as u8); - Ok(Some(hash)) -} - -pub async fn is_client_trusted<'c>( - executor: impl Executor<'c, Database = MySql>, - id: Uuid, -) -> Result, RawUnexpected> { - query_scalar!("SELECT trusted as `t: bool` FROM clients WHERE id = ?", id) - .fetch_optional(executor) - .await - .unexpect() -} - -pub async fn get_client_redirect_uris<'c>( - executor: impl Executor<'c, Database = MySql>, - id: Uuid, -) -> Result, RawUnexpected> { - let uris = query_scalar!( - "SELECT redirect_uri FROM client_redirect_uris WHERE client_id = ?", - id - ) - .fetch_all(executor) - .await - .unexpect()?; - - uris.into_iter() - .map(|s| Url::from_str(&s).unexpect()) - .collect() -} - -pub async fn client_has_redirect_uri<'c>( - executor: impl Executor<'c, Database = MySql>, - id: Uuid, - url: &Url, -) -> Result { - query_scalar!( - r"SELECT EXISTS( - SELECT redirect_uri - FROM client_redirect_uris - WHERE client_id = ? AND redirect_uri = ? - ) as `e: bool`", - id, - url.to_string() - ) - .fetch_one(executor) - .await - .unexpect() -} - -async fn delete_client_redirect_uris<'c>( - executor: impl Executor<'c, Database = MySql>, - id: Uuid, -) -> Result<(), sqlx::Error> { - query!("DELETE FROM client_redirect_uris WHERE client_id = ?", id) - .execute(executor) - .await?; - Ok(()) -} - -async fn create_client_redirect_uris<'c>( - mut transaction: Transaction<'c, MySql>, - client_id: Uuid, - uris: &[Url], -) -> Result<(), sqlx::Error> { - for uri in uris { - query!( - r"INSERT INTO client_redirect_uris (client_id, redirect_uri) - VALUES ( ?, ?)", - client_id, - uri.to_string() - ) - .execute(&mut transaction) - .await?; - } - - transaction.commit().await?; - - Ok(()) -} - -pub async fn create_client<'c>( - mut transaction: Transaction<'c, MySql>, - client: &Client, -) -> Result<(), sqlx::Error> { - query!( - r"INSERT INTO clients (id, alias, type, secret_hash, secret_salt, secret_version, allowed_scopes, default_scopes) - VALUES ( ?, ?, ?, ?, ?, ?, ?, ?)", - client.id(), - client.alias(), - client.client_type(), - client.secret_hash(), - client.secret_salt(), - client.secret_version(), - client.allowed_scopes(), - client.default_scopes() - ) - .execute(&mut transaction) - .await?; - - create_client_redirect_uris(transaction, client.id(), client.redirect_uris()).await?; - - Ok(()) -} - -pub async fn update_client<'c>( - mut transaction: Transaction<'c, MySql>, - client: &Client, -) -> Result<(), sqlx::Error> { - query!( - r"UPDATE clients SET - alias = ?, - type = ?, - secret_hash = ?, - secret_salt = ?, - secret_version = ?, - allowed_scopes = ?, - default_scopes = ? - WHERE id = ?", - client.client_type(), - client.alias(), - client.secret_hash(), - client.secret_salt(), - client.secret_version(), - client.allowed_scopes(), - client.default_scopes(), - client.id() - ) - .execute(&mut transaction) - .await?; - - update_client_redirect_uris(transaction, client.id(), client.redirect_uris()).await?; - - Ok(()) -} - -pub async fn update_client_alias<'c>( - executor: impl Executor<'c, Database = MySql>, - id: Uuid, - alias: &str, -) -> Result { - query!("UPDATE clients SET alias = ? WHERE id = ?", alias, id) - .execute(executor) - .await -} - -pub async fn update_client_type<'c>( - executor: impl Executor<'c, Database = MySql>, - id: Uuid, - ty: ClientType, -) -> Result { - query!("UPDATE clients SET type = ? WHERE id = ?", ty, id) - .execute(executor) - .await -} - -pub async fn update_client_allowed_scopes<'c>( - executor: impl Executor<'c, Database = MySql>, - id: Uuid, - allowed_scopes: &str, -) -> Result { - query!( - "UPDATE clients SET allowed_scopes = ? WHERE id = ?", - allowed_scopes, - id - ) - .execute(executor) - .await -} - -pub async fn update_client_default_scopes<'c>( - executor: impl Executor<'c, Database = MySql>, - id: Uuid, - default_scopes: Option, -) -> Result { - query!( - "UPDATE clients SET default_scopes = ? WHERE id = ?", - default_scopes, - id - ) - .execute(executor) - .await -} - -pub async fn update_client_trusted<'c>( - executor: impl Executor<'c, Database = MySql>, - id: Uuid, - is_trusted: bool, -) -> Result { - query!( - "UPDATE clients SET trusted = ? WHERE id = ?", - is_trusted, - id - ) - .execute(executor) - .await -} - -pub async fn update_client_redirect_uris<'c>( - mut transaction: Transaction<'c, MySql>, - id: Uuid, - uris: &[Url], -) -> Result<(), sqlx::Error> { - delete_client_redirect_uris(&mut transaction, id).await?; - create_client_redirect_uris(transaction, id, uris).await?; - Ok(()) -} - -pub async fn update_client_secret<'c>( - executor: impl Executor<'c, Database = MySql>, - id: Uuid, - secret: Option, -) -> Result { - if let Some(secret) = secret { - query!( - "UPDATE clients SET secret_hash = ?, secret_salt = ?, secret_version = ? WHERE id = ?", - secret.hash(), - secret.salt(), - secret.version(), - id - ) - .execute(executor) - .await - } else { - query!( - r"UPDATE clients - SET secret_hash = NULL, secret_salt = NULL, secret_version = NULL - WHERE id = ?", - id - ) - .execute(executor) - .await - } -} +use std::str::FromStr; + +use exun::{RawUnexpected, ResultErrorExt}; +use sqlx::{ + mysql::MySqlQueryResult, query, query_as, query_scalar, Executor, FromRow, MySql, Transaction, +}; +use url::Url; +use uuid::Uuid; + +use crate::{ + models::client::{Client, ClientType}, + services::crypto::PasswordHash, +}; + +#[derive(Debug, Clone, FromRow)] +pub struct ClientRow { + pub id: Uuid, + pub alias: String, + pub client_type: ClientType, + pub allowed_scopes: String, + pub default_scopes: Option, + pub is_trusted: bool, +} + +#[derive(Clone, FromRow)] +struct HashRow { + secret_hash: Option>, + secret_salt: Option>, + secret_version: Option, +} + +pub async fn client_id_exists<'c>( + executor: impl Executor<'c, Database = MySql>, + id: Uuid, +) -> Result { + query_scalar!( + r"SELECT EXISTS(SELECT id FROM clients WHERE id = ?) as `e: bool`", + id + ) + .fetch_one(executor) + .await + .unexpect() +} + +pub async fn client_alias_exists<'c>( + executor: impl Executor<'c, Database = MySql>, + alias: &str, +) -> Result { + query_scalar!( + "SELECT EXISTS(SELECT alias FROM clients WHERE alias = ?) as `e: bool`", + alias + ) + .fetch_one(executor) + .await + .unexpect() +} + +pub async fn get_client_id_by_alias<'c>( + executor: impl Executor<'c, Database = MySql>, + alias: &str, +) -> Result, RawUnexpected> { + query_scalar!( + "SELECT id as `id: Uuid` FROM clients WHERE alias = ?", + alias + ) + .fetch_optional(executor) + .await + .unexpect() +} + +pub async fn get_client_response<'c>( + executor: impl Executor<'c, Database = MySql>, + id: Uuid, +) -> Result, RawUnexpected> { + let record = query_as!( + ClientRow, + r"SELECT id as `id: Uuid`, + alias, + type as `client_type: ClientType`, + allowed_scopes, + default_scopes, + trusted as `is_trusted: bool` + FROM clients WHERE id = ?", + id + ) + .fetch_optional(executor) + .await?; + + Ok(record) +} + +pub async fn get_client_alias<'c>( + executor: impl Executor<'c, Database = MySql>, + id: Uuid, +) -> Result>, RawUnexpected> { + let alias = query_scalar!("SELECT alias FROM clients WHERE id = ?", id) + .fetch_optional(executor) + .await + .unexpect()?; + + Ok(alias.map(String::into_boxed_str)) +} + +pub async fn get_client_type<'c>( + executor: impl Executor<'c, Database = MySql>, + id: Uuid, +) -> Result, RawUnexpected> { + let ty = query_scalar!( + "SELECT type as `type: ClientType` FROM clients WHERE id = ?", + id + ) + .fetch_optional(executor) + .await + .unexpect()?; + + Ok(ty) +} + +pub async fn get_client_allowed_scopes<'c>( + executor: impl Executor<'c, Database = MySql>, + id: Uuid, +) -> Result>, RawUnexpected> { + let scopes = query_scalar!("SELECT allowed_scopes FROM clients WHERE id = ?", id) + .fetch_optional(executor) + .await?; + + Ok(scopes.map(Box::from)) +} + +pub async fn get_client_default_scopes<'c>( + executor: impl Executor<'c, Database = MySql>, + id: Uuid, +) -> Result>>, RawUnexpected> { + let scopes = query_scalar!("SELECT default_scopes FROM clients WHERE id = ?", id) + .fetch_optional(executor) + .await?; + + Ok(scopes.map(|s| s.map(Box::from))) +} + +pub async fn get_client_secret<'c>( + executor: impl Executor<'c, Database = MySql>, + id: Uuid, +) -> Result, RawUnexpected> { + let hash = query_as!( + HashRow, + r"SELECT secret_hash, secret_salt, secret_version + FROM clients WHERE id = ?", + id + ) + .fetch_optional(executor) + .await?; + + let Some(hash) = hash else { return Ok(None) }; + let Some(version) = hash.secret_version else { return Ok(None) }; + let Some(salt) = hash.secret_hash else { return Ok(None) }; + let Some(hash) = hash.secret_salt else { return Ok(None) }; + + let hash = PasswordHash::from_fields(&hash, &salt, version as u8); + Ok(Some(hash)) +} + +pub async fn is_client_trusted<'c>( + executor: impl Executor<'c, Database = MySql>, + id: Uuid, +) -> Result, RawUnexpected> { + query_scalar!("SELECT trusted as `t: bool` FROM clients WHERE id = ?", id) + .fetch_optional(executor) + .await + .unexpect() +} + +pub async fn get_client_redirect_uris<'c>( + executor: impl Executor<'c, Database = MySql>, + id: Uuid, +) -> Result, RawUnexpected> { + let uris = query_scalar!( + "SELECT redirect_uri FROM client_redirect_uris WHERE client_id = ?", + id + ) + .fetch_all(executor) + .await + .unexpect()?; + + uris.into_iter() + .map(|s| Url::from_str(&s).unexpect()) + .collect() +} + +pub async fn client_has_redirect_uri<'c>( + executor: impl Executor<'c, Database = MySql>, + id: Uuid, + url: &Url, +) -> Result { + query_scalar!( + r"SELECT EXISTS( + SELECT redirect_uri + FROM client_redirect_uris + WHERE client_id = ? AND redirect_uri = ? + ) as `e: bool`", + id, + url.to_string() + ) + .fetch_one(executor) + .await + .unexpect() +} + +async fn delete_client_redirect_uris<'c>( + executor: impl Executor<'c, Database = MySql>, + id: Uuid, +) -> Result<(), sqlx::Error> { + query!("DELETE FROM client_redirect_uris WHERE client_id = ?", id) + .execute(executor) + .await?; + Ok(()) +} + +async fn create_client_redirect_uris<'c>( + mut transaction: Transaction<'c, MySql>, + client_id: Uuid, + uris: &[Url], +) -> Result<(), sqlx::Error> { + for uri in uris { + query!( + r"INSERT INTO client_redirect_uris (client_id, redirect_uri) + VALUES ( ?, ?)", + client_id, + uri.to_string() + ) + .execute(&mut transaction) + .await?; + } + + transaction.commit().await?; + + Ok(()) +} + +pub async fn create_client<'c>( + mut transaction: Transaction<'c, MySql>, + client: &Client, +) -> Result<(), sqlx::Error> { + query!( + r"INSERT INTO clients (id, alias, type, secret_hash, secret_salt, secret_version, allowed_scopes, default_scopes) + VALUES ( ?, ?, ?, ?, ?, ?, ?, ?)", + client.id(), + client.alias(), + client.client_type(), + client.secret_hash(), + client.secret_salt(), + client.secret_version(), + client.allowed_scopes(), + client.default_scopes() + ) + .execute(&mut transaction) + .await?; + + create_client_redirect_uris(transaction, client.id(), client.redirect_uris()).await?; + + Ok(()) +} + +pub async fn update_client<'c>( + mut transaction: Transaction<'c, MySql>, + client: &Client, +) -> Result<(), sqlx::Error> { + query!( + r"UPDATE clients SET + alias = ?, + type = ?, + secret_hash = ?, + secret_salt = ?, + secret_version = ?, + allowed_scopes = ?, + default_scopes = ? + WHERE id = ?", + client.client_type(), + client.alias(), + client.secret_hash(), + client.secret_salt(), + client.secret_version(), + client.allowed_scopes(), + client.default_scopes(), + client.id() + ) + .execute(&mut transaction) + .await?; + + update_client_redirect_uris(transaction, client.id(), client.redirect_uris()).await?; + + Ok(()) +} + +pub async fn update_client_alias<'c>( + executor: impl Executor<'c, Database = MySql>, + id: Uuid, + alias: &str, +) -> Result { + query!("UPDATE clients SET alias = ? WHERE id = ?", alias, id) + .execute(executor) + .await +} + +pub async fn update_client_type<'c>( + executor: impl Executor<'c, Database = MySql>, + id: Uuid, + ty: ClientType, +) -> Result { + query!("UPDATE clients SET type = ? WHERE id = ?", ty, id) + .execute(executor) + .await +} + +pub async fn update_client_allowed_scopes<'c>( + executor: impl Executor<'c, Database = MySql>, + id: Uuid, + allowed_scopes: &str, +) -> Result { + query!( + "UPDATE clients SET allowed_scopes = ? WHERE id = ?", + allowed_scopes, + id + ) + .execute(executor) + .await +} + +pub async fn update_client_default_scopes<'c>( + executor: impl Executor<'c, Database = MySql>, + id: Uuid, + default_scopes: Option, +) -> Result { + query!( + "UPDATE clients SET default_scopes = ? WHERE id = ?", + default_scopes, + id + ) + .execute(executor) + .await +} + +pub async fn update_client_trusted<'c>( + executor: impl Executor<'c, Database = MySql>, + id: Uuid, + is_trusted: bool, +) -> Result { + query!( + "UPDATE clients SET trusted = ? WHERE id = ?", + is_trusted, + id + ) + .execute(executor) + .await +} + +pub async fn update_client_redirect_uris<'c>( + mut transaction: Transaction<'c, MySql>, + id: Uuid, + uris: &[Url], +) -> Result<(), sqlx::Error> { + delete_client_redirect_uris(&mut transaction, id).await?; + create_client_redirect_uris(transaction, id, uris).await?; + Ok(()) +} + +pub async fn update_client_secret<'c>( + executor: impl Executor<'c, Database = MySql>, + id: Uuid, + secret: Option, +) -> Result { + if let Some(secret) = secret { + query!( + "UPDATE clients SET secret_hash = ?, secret_salt = ?, secret_version = ? WHERE id = ?", + secret.hash(), + secret.salt(), + secret.version(), + id + ) + .execute(executor) + .await + } else { + query!( + r"UPDATE clients + SET secret_hash = NULL, secret_salt = NULL, secret_version = NULL + WHERE id = ?", + id + ) + .execute(executor) + .await + } +} diff --git a/src/services/db/jwt.rs b/src/services/db/jwt.rs index b2f1367..73d6902 100644 --- a/src/services/db/jwt.rs +++ b/src/services/db/jwt.rs @@ -1,199 +1,199 @@ -use chrono::{DateTime, Utc}; -use exun::{RawUnexpected, ResultErrorExt}; -use sqlx::{query, query_scalar, Executor, MySql}; -use uuid::Uuid; - -use crate::services::jwt::RevokedRefreshTokenReason; - -pub async fn auth_code_exists<'c>( - executor: impl Executor<'c, Database = MySql>, - jti: Uuid, -) -> Result { - query_scalar!( - "SELECT EXISTS(SELECT jti FROM auth_codes WHERE jti = ?) as `e: bool`", - jti - ) - .fetch_one(executor) - .await - .unexpect() -} - -pub async fn access_token_exists<'c>( - executor: impl Executor<'c, Database = MySql>, - jti: Uuid, -) -> Result { - query_scalar!( - "SELECT EXISTS(SELECT jti FROM access_tokens WHERE jti = ?) as `e: bool`", - jti - ) - .fetch_one(executor) - .await - .unexpect() -} - -pub async fn refresh_token_exists<'c>( - executor: impl Executor<'c, Database = MySql>, - jti: Uuid, -) -> Result { - query_scalar!( - "SELECT EXISTS(SELECT jti FROM refresh_tokens WHERE jti = ?) as `e: bool`", - jti - ) - .fetch_one(executor) - .await - .unexpect() -} - -pub async fn refresh_token_revoked<'c>( - executor: impl Executor<'c, Database = MySql>, - jti: Uuid, -) -> Result { - let result = query_scalar!( - r"SELECT EXISTS( - SELECT revoked_reason FROM refresh_tokens WHERE jti = ? and revoked_reason IS NOT NULL - ) as `e: bool`", - jti - ) - .fetch_one(executor) - .await? - .unwrap_or(true); - - Ok(result) -} - -pub async fn create_auth_code<'c>( - executor: impl Executor<'c, Database = MySql>, - jti: Uuid, - exp: DateTime, -) -> Result<(), sqlx::Error> { - query!( - r"INSERT INTO auth_codes (jti, exp) - VALUES ( ?, ?)", - jti, - exp - ) - .execute(executor) - .await?; - - Ok(()) -} - -pub async fn create_access_token<'c>( - executor: impl Executor<'c, Database = MySql>, - jti: Uuid, - auth_code: Option, - exp: DateTime, -) -> Result<(), sqlx::Error> { - query!( - r"INSERT INTO access_tokens (jti, auth_code, exp) - VALUES ( ?, ?, ?)", - jti, - auth_code, - exp - ) - .execute(executor) - .await?; - - Ok(()) -} - -pub async fn create_refresh_token<'c>( - executor: impl Executor<'c, Database = MySql>, - jti: Uuid, - auth_code: Option, - exp: DateTime, -) -> Result<(), sqlx::Error> { - query!( - r"INSERT INTO access_tokens (jti, auth_code, exp) - VALUES ( ?, ?, ?)", - jti, - auth_code, - exp - ) - .execute(executor) - .await?; - - Ok(()) -} - -pub async fn delete_auth_code<'c>( - executor: impl Executor<'c, Database = MySql>, - auth_code: Uuid, -) -> Result { - let result = query!("DELETE FROM auth_codes WHERE jti = ?", auth_code) - .execute(executor) - .await?; - - Ok(result.rows_affected() != 0) -} - -pub async fn delete_expired_auth_codes<'c>( - executor: impl Executor<'c, Database = MySql>, -) -> Result<(), RawUnexpected> { - query!("DELETE FROM auth_codes WHERE exp < ?", Utc::now()) - .execute(executor) - .await?; - - Ok(()) -} - -pub async fn delete_access_tokens_with_auth_code<'c>( - executor: impl Executor<'c, Database = MySql>, - auth_code: Uuid, -) -> Result { - let result = query!("DELETE FROM access_tokens WHERE auth_code = ?", auth_code) - .execute(executor) - .await?; - - Ok(result.rows_affected() != 0) -} - -pub async fn delete_expired_access_tokens<'c>( - executor: impl Executor<'c, Database = MySql>, -) -> Result<(), RawUnexpected> { - query!("DELETE FROM access_tokens WHERE exp < ?", Utc::now()) - .execute(executor) - .await?; - - Ok(()) -} - -pub async fn revoke_refresh_token<'c>( - executor: impl Executor<'c, Database = MySql>, - jti: Uuid, -) -> Result { - let result = query!( - "UPDATE refresh_tokens SET revoked_reason = ? WHERE jti = ?", - RevokedRefreshTokenReason::NewRefreshToken, - jti - ) - .execute(executor) - .await?; - - Ok(result.rows_affected() != 0) -} - -pub async fn revoke_refresh_tokens_with_auth_code<'c>( - executor: impl Executor<'c, Database = MySql>, - auth_code: Uuid, -) -> Result { - let result = query!( - "UPDATE refresh_tokens SET revoked_reason = ? WHERE auth_code = ?", - RevokedRefreshTokenReason::ReusedAuthorizationCode, - auth_code - ) - .execute(executor) - .await?; - - Ok(result.rows_affected() != 0) -} - -pub async fn delete_expired_refresh_tokens<'c>( - executor: impl Executor<'c, Database = MySql>, -) -> Result<(), RawUnexpected> { - query!("DELETE FROM refresh_tokens WHERE exp < ?", Utc::now()) - .execute(executor) - .await?; - - Ok(()) -} +use chrono::{DateTime, Utc}; +use exun::{RawUnexpected, ResultErrorExt}; +use sqlx::{query, query_scalar, Executor, MySql}; +use uuid::Uuid; + +use crate::services::jwt::RevokedRefreshTokenReason; + +pub async fn auth_code_exists<'c>( + executor: impl Executor<'c, Database = MySql>, + jti: Uuid, +) -> Result { + query_scalar!( + "SELECT EXISTS(SELECT jti FROM auth_codes WHERE jti = ?) as `e: bool`", + jti + ) + .fetch_one(executor) + .await + .unexpect() +} + +pub async fn access_token_exists<'c>( + executor: impl Executor<'c, Database = MySql>, + jti: Uuid, +) -> Result { + query_scalar!( + "SELECT EXISTS(SELECT jti FROM access_tokens WHERE jti = ?) as `e: bool`", + jti + ) + .fetch_one(executor) + .await + .unexpect() +} + +pub async fn refresh_token_exists<'c>( + executor: impl Executor<'c, Database = MySql>, + jti: Uuid, +) -> Result { + query_scalar!( + "SELECT EXISTS(SELECT jti FROM refresh_tokens WHERE jti = ?) as `e: bool`", + jti + ) + .fetch_one(executor) + .await + .unexpect() +} + +pub async fn refresh_token_revoked<'c>( + executor: impl Executor<'c, Database = MySql>, + jti: Uuid, +) -> Result { + let result = query_scalar!( + r"SELECT EXISTS( + SELECT revoked_reason FROM refresh_tokens WHERE jti = ? and revoked_reason IS NOT NULL + ) as `e: bool`", + jti + ) + .fetch_one(executor) + .await? + .unwrap_or(true); + + Ok(result) +} + +pub async fn create_auth_code<'c>( + executor: impl Executor<'c, Database = MySql>, + jti: Uuid, + exp: DateTime, +) -> Result<(), sqlx::Error> { + query!( + r"INSERT INTO auth_codes (jti, exp) + VALUES ( ?, ?)", + jti, + exp + ) + .execute(executor) + .await?; + + Ok(()) +} + +pub async fn create_access_token<'c>( + executor: impl Executor<'c, Database = MySql>, + jti: Uuid, + auth_code: Option, + exp: DateTime, +) -> Result<(), sqlx::Error> { + query!( + r"INSERT INTO access_tokens (jti, auth_code, exp) + VALUES ( ?, ?, ?)", + jti, + auth_code, + exp + ) + .execute(executor) + .await?; + + Ok(()) +} + +pub async fn create_refresh_token<'c>( + executor: impl Executor<'c, Database = MySql>, + jti: Uuid, + auth_code: Option, + exp: DateTime, +) -> Result<(), sqlx::Error> { + query!( + r"INSERT INTO access_tokens (jti, auth_code, exp) + VALUES ( ?, ?, ?)", + jti, + auth_code, + exp + ) + .execute(executor) + .await?; + + Ok(()) +} + +pub async fn delete_auth_code<'c>( + executor: impl Executor<'c, Database = MySql>, + auth_code: Uuid, +) -> Result { + let result = query!("DELETE FROM auth_codes WHERE jti = ?", auth_code) + .execute(executor) + .await?; + + Ok(result.rows_affected() != 0) +} + +pub async fn delete_expired_auth_codes<'c>( + executor: impl Executor<'c, Database = MySql>, +) -> Result<(), RawUnexpected> { + query!("DELETE FROM auth_codes WHERE exp < ?", Utc::now()) + .execute(executor) + .await?; + + Ok(()) +} + +pub async fn delete_access_tokens_with_auth_code<'c>( + executor: impl Executor<'c, Database = MySql>, + auth_code: Uuid, +) -> Result { + let result = query!("DELETE FROM access_tokens WHERE auth_code = ?", auth_code) + .execute(executor) + .await?; + + Ok(result.rows_affected() != 0) +} + +pub async fn delete_expired_access_tokens<'c>( + executor: impl Executor<'c, Database = MySql>, +) -> Result<(), RawUnexpected> { + query!("DELETE FROM access_tokens WHERE exp < ?", Utc::now()) + .execute(executor) + .await?; + + Ok(()) +} + +pub async fn revoke_refresh_token<'c>( + executor: impl Executor<'c, Database = MySql>, + jti: Uuid, +) -> Result { + let result = query!( + "UPDATE refresh_tokens SET revoked_reason = ? WHERE jti = ?", + RevokedRefreshTokenReason::NewRefreshToken, + jti + ) + .execute(executor) + .await?; + + Ok(result.rows_affected() != 0) +} + +pub async fn revoke_refresh_tokens_with_auth_code<'c>( + executor: impl Executor<'c, Database = MySql>, + auth_code: Uuid, +) -> Result { + let result = query!( + "UPDATE refresh_tokens SET revoked_reason = ? WHERE auth_code = ?", + RevokedRefreshTokenReason::ReusedAuthorizationCode, + auth_code + ) + .execute(executor) + .await?; + + Ok(result.rows_affected() != 0) +} + +pub async fn delete_expired_refresh_tokens<'c>( + executor: impl Executor<'c, Database = MySql>, +) -> Result<(), RawUnexpected> { + query!("DELETE FROM refresh_tokens WHERE exp < ?", Utc::now()) + .execute(executor) + .await?; + + Ok(()) +} diff --git a/src/services/db/user.rs b/src/services/db/user.rs index 09a09da..f85047a 100644 --- a/src/services/db/user.rs +++ b/src/services/db/user.rs @@ -1,236 +1,236 @@ -use exun::RawUnexpected; -use sqlx::{mysql::MySqlQueryResult, query, query_as, query_scalar, Executor, MySql}; -use uuid::Uuid; - -use crate::{models::user::User, services::crypto::PasswordHash}; - -struct UserRow { - id: Uuid, - username: String, - password_hash: Vec, - password_salt: Vec, - password_version: u32, -} - -impl TryFrom for User { - type Error = RawUnexpected; - - fn try_from(row: UserRow) -> Result { - let password = PasswordHash::from_fields( - &row.password_hash, - &row.password_salt, - row.password_version as u8, - ); - let user = User { - id: row.id, - username: row.username.into_boxed_str(), - password, - }; - Ok(user) - } -} - -/// Check if a user with a given user ID exists -pub async fn user_id_exists<'c>( - conn: impl Executor<'c, Database = MySql>, - id: Uuid, -) -> Result { - let exists = query_scalar!( - r#"SELECT EXISTS(SELECT id FROM users WHERE id = ?) as `e: bool`"#, - id - ) - .fetch_one(conn) - .await?; - - Ok(exists) -} - -/// Check if a given username is taken -pub async fn username_is_used<'c>( - conn: impl Executor<'c, Database = MySql>, - username: &str, -) -> Result { - let exists = query_scalar!( - r#"SELECT EXISTS(SELECT id FROM users WHERE username = ?) as "e: bool""#, - username - ) - .fetch_one(conn) - .await?; - - Ok(exists) -} - -/// Get a user from their ID -pub async fn get_user<'c>( - conn: impl Executor<'c, Database = MySql>, - user_id: Uuid, -) -> Result, RawUnexpected> { - let record = query_as!( - UserRow, - r"SELECT id as `id: Uuid`, username, password_hash, password_salt, password_version - FROM users WHERE id = ?", - user_id - ) - .fetch_optional(conn) - .await?; - - let Some(record) = record else { return Ok(None) }; - - Ok(Some(record.try_into()?)) -} - -/// Get a user from their username -pub async fn get_user_by_username<'c>( - conn: impl Executor<'c, Database = MySql>, - username: &str, -) -> Result, RawUnexpected> { - let record = query_as!( - UserRow, - r"SELECT id as `id: Uuid`, username, password_hash, password_salt, password_version - FROM users WHERE username = ?", - username - ) - .fetch_optional(conn) - .await?; - - let Some(record) = record else { return Ok(None) }; - - Ok(Some(record.try_into()?)) -} - -/// Search the list of users for a given username -pub async fn search_users<'c>( - conn: impl Executor<'c, Database = MySql>, - username: &str, -) -> Result, RawUnexpected> { - let records = query_as!( - UserRow, - r"SELECT id as `id: Uuid`, username, password_hash, password_salt, password_version - FROM users - WHERE LOCATE(?, username) != 0", - username, - ) - .fetch_all(conn) - .await?; - - Ok(records - .into_iter() - .map(|u| u.try_into()) - .collect::, RawUnexpected>>()?) -} - -/// Search the list of users, only returning a certain range of results -pub async fn search_users_limit<'c>( - conn: impl Executor<'c, Database = MySql>, - username: &str, - offset: u32, - limit: u32, -) -> Result, RawUnexpected> { - let records = query_as!( - UserRow, - r"SELECT id as `id: Uuid`, username, password_hash, password_salt, password_version - FROM users - WHERE LOCATE(?, username) != 0 - LIMIT ? - OFFSET ?", - username, - offset, - limit - ) - .fetch_all(conn) - .await?; - - Ok(records - .into_iter() - .map(|u| u.try_into()) - .collect::, RawUnexpected>>()?) -} - -/// Get the username of a user with a certain ID -pub async fn get_username<'c>( - conn: impl Executor<'c, Database = MySql>, - user_id: Uuid, -) -> Result>, RawUnexpected> { - let username = query_scalar!(r"SELECT username FROM users where id = ?", user_id) - .fetch_optional(conn) - .await? - .map(String::into_boxed_str); - - Ok(username) -} - -/// Create a new user -pub async fn create_user<'c>( - conn: impl Executor<'c, Database = MySql>, - user: &User, -) -> Result { - query!( - r"INSERT INTO users (id, username, password_hash, password_salt, password_version) - VALUES ( ?, ?, ?, ?, ?)", - user.id, - user.username(), - user.password_hash(), - user.password_salt(), - user.password_version() - ) - .execute(conn) - .await -} - -/// Update a user -pub async fn update_user<'c>( - conn: impl Executor<'c, Database = MySql>, - user: &User, -) -> Result { - query!( - r"UPDATE users SET - username = ?, - password_hash = ?, - password_salt = ?, - password_version = ? - WHERE id = ?", - user.username(), - user.password_hash(), - user.password_salt(), - user.password_version(), - user.id - ) - .execute(conn) - .await -} - -/// Update the username of a user with the given ID -pub async fn update_username<'c>( - conn: impl Executor<'c, Database = MySql>, - user_id: Uuid, - username: &str, -) -> Result { - query!( - r"UPDATE users SET username = ? WHERE id = ?", - username, - user_id - ) - .execute(conn) - .await -} - -/// Update the password of a user with the given ID -pub async fn update_password<'c>( - conn: impl Executor<'c, Database = MySql>, - user_id: Uuid, - password: &PasswordHash, -) -> Result { - query!( - r"UPDATE users SET - password_hash = ?, - password_salt = ?, - password_version = ? - WHERE id = ?", - password.hash(), - password.salt(), - password.version(), - user_id - ) - .execute(conn) - .await -} +use exun::RawUnexpected; +use sqlx::{mysql::MySqlQueryResult, query, query_as, query_scalar, Executor, MySql}; +use uuid::Uuid; + +use crate::{models::user::User, services::crypto::PasswordHash}; + +struct UserRow { + id: Uuid, + username: String, + password_hash: Vec, + password_salt: Vec, + password_version: u32, +} + +impl TryFrom for User { + type Error = RawUnexpected; + + fn try_from(row: UserRow) -> Result { + let password = PasswordHash::from_fields( + &row.password_hash, + &row.password_salt, + row.password_version as u8, + ); + let user = User { + id: row.id, + username: row.username.into_boxed_str(), + password, + }; + Ok(user) + } +} + +/// Check if a user with a given user ID exists +pub async fn user_id_exists<'c>( + conn: impl Executor<'c, Database = MySql>, + id: Uuid, +) -> Result { + let exists = query_scalar!( + r#"SELECT EXISTS(SELECT id FROM users WHERE id = ?) as `e: bool`"#, + id + ) + .fetch_one(conn) + .await?; + + Ok(exists) +} + +/// Check if a given username is taken +pub async fn username_is_used<'c>( + conn: impl Executor<'c, Database = MySql>, + username: &str, +) -> Result { + let exists = query_scalar!( + r#"SELECT EXISTS(SELECT id FROM users WHERE username = ?) as "e: bool""#, + username + ) + .fetch_one(conn) + .await?; + + Ok(exists) +} + +/// Get a user from their ID +pub async fn get_user<'c>( + conn: impl Executor<'c, Database = MySql>, + user_id: Uuid, +) -> Result, RawUnexpected> { + let record = query_as!( + UserRow, + r"SELECT id as `id: Uuid`, username, password_hash, password_salt, password_version + FROM users WHERE id = ?", + user_id + ) + .fetch_optional(conn) + .await?; + + let Some(record) = record else { return Ok(None) }; + + Ok(Some(record.try_into()?)) +} + +/// Get a user from their username +pub async fn get_user_by_username<'c>( + conn: impl Executor<'c, Database = MySql>, + username: &str, +) -> Result, RawUnexpected> { + let record = query_as!( + UserRow, + r"SELECT id as `id: Uuid`, username, password_hash, password_salt, password_version + FROM users WHERE username = ?", + username + ) + .fetch_optional(conn) + .await?; + + let Some(record) = record else { return Ok(None) }; + + Ok(Some(record.try_into()?)) +} + +/// Search the list of users for a given username +pub async fn search_users<'c>( + conn: impl Executor<'c, Database = MySql>, + username: &str, +) -> Result, RawUnexpected> { + let records = query_as!( + UserRow, + r"SELECT id as `id: Uuid`, username, password_hash, password_salt, password_version + FROM users + WHERE LOCATE(?, username) != 0", + username, + ) + .fetch_all(conn) + .await?; + + Ok(records + .into_iter() + .map(|u| u.try_into()) + .collect::, RawUnexpected>>()?) +} + +/// Search the list of users, only returning a certain range of results +pub async fn search_users_limit<'c>( + conn: impl Executor<'c, Database = MySql>, + username: &str, + offset: u32, + limit: u32, +) -> Result, RawUnexpected> { + let records = query_as!( + UserRow, + r"SELECT id as `id: Uuid`, username, password_hash, password_salt, password_version + FROM users + WHERE LOCATE(?, username) != 0 + LIMIT ? + OFFSET ?", + username, + offset, + limit + ) + .fetch_all(conn) + .await?; + + Ok(records + .into_iter() + .map(|u| u.try_into()) + .collect::, RawUnexpected>>()?) +} + +/// Get the username of a user with a certain ID +pub async fn get_username<'c>( + conn: impl Executor<'c, Database = MySql>, + user_id: Uuid, +) -> Result>, RawUnexpected> { + let username = query_scalar!(r"SELECT username FROM users where id = ?", user_id) + .fetch_optional(conn) + .await? + .map(String::into_boxed_str); + + Ok(username) +} + +/// Create a new user +pub async fn create_user<'c>( + conn: impl Executor<'c, Database = MySql>, + user: &User, +) -> Result { + query!( + r"INSERT INTO users (id, username, password_hash, password_salt, password_version) + VALUES ( ?, ?, ?, ?, ?)", + user.id, + user.username(), + user.password_hash(), + user.password_salt(), + user.password_version() + ) + .execute(conn) + .await +} + +/// Update a user +pub async fn update_user<'c>( + conn: impl Executor<'c, Database = MySql>, + user: &User, +) -> Result { + query!( + r"UPDATE users SET + username = ?, + password_hash = ?, + password_salt = ?, + password_version = ? + WHERE id = ?", + user.username(), + user.password_hash(), + user.password_salt(), + user.password_version(), + user.id + ) + .execute(conn) + .await +} + +/// Update the username of a user with the given ID +pub async fn update_username<'c>( + conn: impl Executor<'c, Database = MySql>, + user_id: Uuid, + username: &str, +) -> Result { + query!( + r"UPDATE users SET username = ? WHERE id = ?", + username, + user_id + ) + .execute(conn) + .await +} + +/// Update the password of a user with the given ID +pub async fn update_password<'c>( + conn: impl Executor<'c, Database = MySql>, + user_id: Uuid, + password: &PasswordHash, +) -> Result { + query!( + r"UPDATE users SET + password_hash = ?, + password_salt = ?, + password_version = ? + WHERE id = ?", + password.hash(), + password.salt(), + password.version(), + user_id + ) + .execute(conn) + .await +} diff --git a/src/services/id.rs b/src/services/id.rs index 0c665ed..e1227e4 100644 --- a/src/services/id.rs +++ b/src/services/id.rs @@ -1,27 +1,27 @@ -use std::future::Future; - -use exun::RawUnexpected; -use sqlx::{Executor, MySql}; -use uuid::Uuid; - -/// Create a unique id, handling duplicate ID's. -/// -/// The given `unique_check` parameter returns `true` if the ID is used and -/// `false` otherwise. -pub async fn new_id< - 'c, - E: Executor<'c, Database = MySql> + Clone, - F: Future>, ->( - conn: E, - unique_check: impl Fn(E, Uuid) -> F, -) -> Result { - let uuid = loop { - let uuid = Uuid::new_v4(); - if !unique_check(conn.clone(), uuid).await? { - break uuid; - } - }; - - Ok(uuid) -} +use std::future::Future; + +use exun::RawUnexpected; +use sqlx::{Executor, MySql}; +use uuid::Uuid; + +/// Create a unique id, handling duplicate ID's. +/// +/// The given `unique_check` parameter returns `true` if the ID is used and +/// `false` otherwise. +pub async fn new_id< + 'c, + E: Executor<'c, Database = MySql> + Clone, + F: Future>, +>( + conn: E, + unique_check: impl Fn(E, Uuid) -> F, +) -> Result { + let uuid = loop { + let uuid = Uuid::new_v4(); + if !unique_check(conn.clone(), uuid).await? { + break uuid; + } + }; + + Ok(uuid) +} diff --git a/src/services/jwt.rs b/src/services/jwt.rs index 16f5fa6..863eb83 100644 --- a/src/services/jwt.rs +++ b/src/services/jwt.rs @@ -1,291 +1,291 @@ -use chrono::{serde::ts_milliseconds, serde::ts_milliseconds_option, DateTime, Duration, Utc}; -use exun::{Expect, RawUnexpected, ResultErrorExt}; -use jwt::{SignWithKey, VerifyWithKey}; -use raise::yeet; -use serde::{Deserialize, Serialize}; -use sqlx::{Executor, MySql, MySqlPool}; -use thiserror::Error; -use url::Url; -use uuid::Uuid; - -use super::{db, id::new_id, secrets}; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub enum TokenType { - Authorization, - Access, - Refresh, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Claims { - iss: Url, - sub: Uuid, - aud: Box<[String]>, - #[serde(with = "ts_milliseconds")] - exp: DateTime, - #[serde(with = "ts_milliseconds_option")] - nbf: Option>, - #[serde(with = "ts_milliseconds")] - iat: DateTime, - jti: Uuid, - scope: Box, - client_id: Uuid, - token_type: TokenType, - auth_code_id: Option, - redirect_uri: Option, -} - -#[derive(Debug, Clone, Copy, sqlx::Type)] -#[sqlx(rename_all = "kebab-case")] -pub enum RevokedRefreshTokenReason { - ReusedAuthorizationCode, - NewRefreshToken, -} - -impl Claims { - pub async fn auth_code<'c>( - db: &MySqlPool, - self_id: Url, - client_id: Uuid, - sub: Uuid, - scopes: &str, - redirect_uri: &Url, - ) -> Result { - let five_minutes = Duration::minutes(5); - - let id = new_id(db, db::auth_code_exists).await?; - let iat = Utc::now(); - let exp = iat + five_minutes; - - db::create_auth_code(db, id, exp).await?; - - let aud = [self_id.to_string(), client_id.to_string()].into(); - - Ok(Self { - iss: self_id, - sub, - aud, - exp, - nbf: None, - iat, - jti: id, - scope: scopes.into(), - client_id, - auth_code_id: Some(id), - token_type: TokenType::Authorization, - redirect_uri: Some(redirect_uri.clone()), - }) - } - - pub async fn access_token<'c>( - db: &MySqlPool, - auth_code_id: Option, - self_id: Url, - client_id: Uuid, - sub: Uuid, - duration: Duration, - scopes: &str, - ) -> Result { - let id = new_id(db, db::access_token_exists).await?; - let iat = Utc::now(); - let exp = iat + duration; - - db::create_access_token(db, id, auth_code_id, exp) - .await - .unexpect()?; - - let aud = [self_id.to_string(), client_id.to_string()].into(); - - Ok(Self { - iss: self_id, - sub, - aud, - exp, - nbf: None, - iat, - jti: id, - scope: scopes.into(), - client_id, - auth_code_id, - token_type: TokenType::Access, - redirect_uri: None, - }) - } - - pub async fn refresh_token( - db: &MySqlPool, - other_token: &Claims, - ) -> Result { - let one_day = Duration::days(1); - - let id = new_id(db, db::refresh_token_exists).await?; - let iat = Utc::now(); - let exp = other_token.exp + one_day; - - db::create_refresh_token(db, id, other_token.auth_code_id, exp).await?; - - let mut claims = other_token.clone(); - claims.exp = exp; - claims.iat = iat; - claims.jti = id; - claims.token_type = TokenType::Refresh; - - Ok(claims) - } - - pub async fn refreshed_access_token( - db: &MySqlPool, - refresh_token: &Claims, - exp_time: Duration, - ) -> Result { - let id = new_id(db, db::access_token_exists).await?; - let iat = Utc::now(); - let exp = iat + exp_time; - - db::create_access_token(db, id, refresh_token.auth_code_id, exp).await?; - - let mut claims = refresh_token.clone(); - claims.exp = exp; - claims.iat = iat; - claims.jti = id; - claims.token_type = TokenType::Access; - - Ok(claims) - } - - pub fn id(&self) -> Uuid { - self.jti - } - - pub fn subject(&self) -> Uuid { - self.sub - } - - pub fn expires_in(&self) -> i64 { - (self.exp - Utc::now()).num_seconds() - } - - pub fn scopes(&self) -> &str { - &self.scope - } - - pub fn to_jwt(&self) -> Result, RawUnexpected> { - let key = secrets::signing_key()?; - let jwt = self.sign_with_key(&key)?.into_boxed_str(); - Ok(jwt) - } -} - -#[derive(Debug, Error)] -pub enum VerifyJwtError { - #[error("{0}")] - ParseJwtError(#[from] jwt::Error), - #[error("The issuer for this token is incorrect")] - IncorrectIssuer, - #[error("This bearer token was intended for a different client")] - WrongClient, - #[error("The given audience parameter does not contain this issuer")] - BadAudience, - #[error("The redirect URI doesn't match what's in the token")] - IncorrectRedirectUri, - #[error("The token is expired")] - ExpiredToken, - #[error("The token cannot be used yet")] - NotYet, - #[error("The bearer token has been revoked")] - JwtRevoked, -} - -fn verify_jwt( - token: &str, - self_id: &Url, - client_id: Option, -) -> Result> { - let key = secrets::signing_key()?; - let claims: Claims = token - .verify_with_key(&key) - .map_err(|e| VerifyJwtError::from(e))?; - - if &claims.iss != self_id { - yeet!(VerifyJwtError::IncorrectIssuer.into()) - } - - if let Some(client_id) = client_id { - if claims.client_id != client_id { - yeet!(VerifyJwtError::WrongClient.into()) - } - } - - if !claims.aud.contains(&self_id.to_string()) { - yeet!(VerifyJwtError::BadAudience.into()) - } - - let now = Utc::now(); - - if now > claims.exp { - yeet!(VerifyJwtError::ExpiredToken.into()) - } - - if let Some(nbf) = claims.nbf { - if now < nbf { - yeet!(VerifyJwtError::NotYet.into()) - } - } - - Ok(claims) -} - -pub async fn verify_auth_code<'c>( - db: &MySqlPool, - token: &str, - self_id: &Url, - client_id: Uuid, - redirect_uri: Url, -) -> Result> { - let claims = verify_jwt(token, self_id, Some(client_id))?; - - if let Some(claimed_uri) = &claims.redirect_uri { - if claimed_uri.clone() != redirect_uri { - yeet!(VerifyJwtError::IncorrectRedirectUri.into()); - } - } - - if db::delete_auth_code(db, claims.jti).await? { - db::delete_access_tokens_with_auth_code(db, claims.jti).await?; - db::revoke_refresh_tokens_with_auth_code(db, claims.jti).await?; - yeet!(VerifyJwtError::JwtRevoked.into()); - } - - Ok(claims) -} - -pub async fn verify_access_token<'c>( - db: impl Executor<'c, Database = MySql>, - token: &str, - self_id: &Url, - client_id: Uuid, -) -> Result> { - let claims = verify_jwt(token, self_id, Some(client_id))?; - - if !db::access_token_exists(db, claims.jti).await? { - yeet!(VerifyJwtError::JwtRevoked.into()) - } - - Ok(claims) -} - -pub async fn verify_refresh_token<'c>( - db: impl Executor<'c, Database = MySql>, - token: &str, - self_id: &Url, - client_id: Option, -) -> Result> { - let claims = verify_jwt(token, self_id, client_id)?; - - if db::refresh_token_revoked(db, claims.jti).await? { - yeet!(VerifyJwtError::JwtRevoked.into()) - } - - Ok(claims) -} +use chrono::{serde::ts_milliseconds, serde::ts_milliseconds_option, DateTime, Duration, Utc}; +use exun::{Expect, RawUnexpected, ResultErrorExt}; +use jwt::{SignWithKey, VerifyWithKey}; +use raise::yeet; +use serde::{Deserialize, Serialize}; +use sqlx::{Executor, MySql, MySqlPool}; +use thiserror::Error; +use url::Url; +use uuid::Uuid; + +use super::{db, id::new_id, secrets}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum TokenType { + Authorization, + Access, + Refresh, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Claims { + iss: Url, + sub: Uuid, + aud: Box<[String]>, + #[serde(with = "ts_milliseconds")] + exp: DateTime, + #[serde(with = "ts_milliseconds_option")] + nbf: Option>, + #[serde(with = "ts_milliseconds")] + iat: DateTime, + jti: Uuid, + scope: Box, + client_id: Uuid, + token_type: TokenType, + auth_code_id: Option, + redirect_uri: Option, +} + +#[derive(Debug, Clone, Copy, sqlx::Type)] +#[sqlx(rename_all = "kebab-case")] +pub enum RevokedRefreshTokenReason { + ReusedAuthorizationCode, + NewRefreshToken, +} + +impl Claims { + pub async fn auth_code<'c>( + db: &MySqlPool, + self_id: Url, + client_id: Uuid, + sub: Uuid, + scopes: &str, + redirect_uri: &Url, + ) -> Result { + let five_minutes = Duration::minutes(5); + + let id = new_id(db, db::auth_code_exists).await?; + let iat = Utc::now(); + let exp = iat + five_minutes; + + db::create_auth_code(db, id, exp).await?; + + let aud = [self_id.to_string(), client_id.to_string()].into(); + + Ok(Self { + iss: self_id, + sub, + aud, + exp, + nbf: None, + iat, + jti: id, + scope: scopes.into(), + client_id, + auth_code_id: Some(id), + token_type: TokenType::Authorization, + redirect_uri: Some(redirect_uri.clone()), + }) + } + + pub async fn access_token<'c>( + db: &MySqlPool, + auth_code_id: Option, + self_id: Url, + client_id: Uuid, + sub: Uuid, + duration: Duration, + scopes: &str, + ) -> Result { + let id = new_id(db, db::access_token_exists).await?; + let iat = Utc::now(); + let exp = iat + duration; + + db::create_access_token(db, id, auth_code_id, exp) + .await + .unexpect()?; + + let aud = [self_id.to_string(), client_id.to_string()].into(); + + Ok(Self { + iss: self_id, + sub, + aud, + exp, + nbf: None, + iat, + jti: id, + scope: scopes.into(), + client_id, + auth_code_id, + token_type: TokenType::Access, + redirect_uri: None, + }) + } + + pub async fn refresh_token( + db: &MySqlPool, + other_token: &Claims, + ) -> Result { + let one_day = Duration::days(1); + + let id = new_id(db, db::refresh_token_exists).await?; + let iat = Utc::now(); + let exp = other_token.exp + one_day; + + db::create_refresh_token(db, id, other_token.auth_code_id, exp).await?; + + let mut claims = other_token.clone(); + claims.exp = exp; + claims.iat = iat; + claims.jti = id; + claims.token_type = TokenType::Refresh; + + Ok(claims) + } + + pub async fn refreshed_access_token( + db: &MySqlPool, + refresh_token: &Claims, + exp_time: Duration, + ) -> Result { + let id = new_id(db, db::access_token_exists).await?; + let iat = Utc::now(); + let exp = iat + exp_time; + + db::create_access_token(db, id, refresh_token.auth_code_id, exp).await?; + + let mut claims = refresh_token.clone(); + claims.exp = exp; + claims.iat = iat; + claims.jti = id; + claims.token_type = TokenType::Access; + + Ok(claims) + } + + pub fn id(&self) -> Uuid { + self.jti + } + + pub fn subject(&self) -> Uuid { + self.sub + } + + pub fn expires_in(&self) -> i64 { + (self.exp - Utc::now()).num_seconds() + } + + pub fn scopes(&self) -> &str { + &self.scope + } + + pub fn to_jwt(&self) -> Result, RawUnexpected> { + let key = secrets::signing_key()?; + let jwt = self.sign_with_key(&key)?.into_boxed_str(); + Ok(jwt) + } +} + +#[derive(Debug, Error)] +pub enum VerifyJwtError { + #[error("{0}")] + ParseJwtError(#[from] jwt::Error), + #[error("The issuer for this token is incorrect")] + IncorrectIssuer, + #[error("This bearer token was intended for a different client")] + WrongClient, + #[error("The given audience parameter does not contain this issuer")] + BadAudience, + #[error("The redirect URI doesn't match what's in the token")] + IncorrectRedirectUri, + #[error("The token is expired")] + ExpiredToken, + #[error("The token cannot be used yet")] + NotYet, + #[error("The bearer token has been revoked")] + JwtRevoked, +} + +fn verify_jwt( + token: &str, + self_id: &Url, + client_id: Option, +) -> Result> { + let key = secrets::signing_key()?; + let claims: Claims = token + .verify_with_key(&key) + .map_err(|e| VerifyJwtError::from(e))?; + + if &claims.iss != self_id { + yeet!(VerifyJwtError::IncorrectIssuer.into()) + } + + if let Some(client_id) = client_id { + if claims.client_id != client_id { + yeet!(VerifyJwtError::WrongClient.into()) + } + } + + if !claims.aud.contains(&self_id.to_string()) { + yeet!(VerifyJwtError::BadAudience.into()) + } + + let now = Utc::now(); + + if now > claims.exp { + yeet!(VerifyJwtError::ExpiredToken.into()) + } + + if let Some(nbf) = claims.nbf { + if now < nbf { + yeet!(VerifyJwtError::NotYet.into()) + } + } + + Ok(claims) +} + +pub async fn verify_auth_code<'c>( + db: &MySqlPool, + token: &str, + self_id: &Url, + client_id: Uuid, + redirect_uri: Url, +) -> Result> { + let claims = verify_jwt(token, self_id, Some(client_id))?; + + if let Some(claimed_uri) = &claims.redirect_uri { + if claimed_uri.clone() != redirect_uri { + yeet!(VerifyJwtError::IncorrectRedirectUri.into()); + } + } + + if db::delete_auth_code(db, claims.jti).await? { + db::delete_access_tokens_with_auth_code(db, claims.jti).await?; + db::revoke_refresh_tokens_with_auth_code(db, claims.jti).await?; + yeet!(VerifyJwtError::JwtRevoked.into()); + } + + Ok(claims) +} + +pub async fn verify_access_token<'c>( + db: impl Executor<'c, Database = MySql>, + token: &str, + self_id: &Url, + client_id: Uuid, +) -> Result> { + let claims = verify_jwt(token, self_id, Some(client_id))?; + + if !db::access_token_exists(db, claims.jti).await? { + yeet!(VerifyJwtError::JwtRevoked.into()) + } + + Ok(claims) +} + +pub async fn verify_refresh_token<'c>( + db: impl Executor<'c, Database = MySql>, + token: &str, + self_id: &Url, + client_id: Option, +) -> Result> { + let claims = verify_jwt(token, self_id, client_id)?; + + if db::refresh_token_revoked(db, claims.jti).await? { + yeet!(VerifyJwtError::JwtRevoked.into()) + } + + Ok(claims) +} diff --git a/src/services/mod.rs b/src/services/mod.rs index de08b58..4c69367 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -1,7 +1,7 @@ -pub mod authorization; -pub mod config; -pub mod crypto; -pub mod db; -pub mod id; -pub mod jwt; -pub mod secrets; +pub mod authorization; +pub mod config; +pub mod crypto; +pub mod db; +pub mod id; +pub mod jwt; +pub mod secrets; diff --git a/src/services/secrets.rs b/src/services/secrets.rs index 241b2c5..e1d4992 100644 --- a/src/services/secrets.rs +++ b/src/services/secrets.rs @@ -1,24 +1,24 @@ -use std::env; - -use exun::*; -use hmac::{Hmac, Mac}; -use sha2::Sha256; - -/// This is a secret salt, needed for creating passwords. It's used as an extra -/// layer of security, on top of the salt that's already used. -pub fn pepper() -> Result, RawUnexpected> { - let pepper = env::var("SECRET_SALT")?; - let pepper = hex::decode(pepper)?; - Ok(pepper.into_boxed_slice()) -} - -/// The URL to the MySQL database -pub fn database_url() -> Result { - env::var("DATABASE_URL").unexpect() -} - -pub fn signing_key() -> Result, RawUnexpected> { - let key = env::var("PRIVATE_KEY")?; - let key = Hmac::::new_from_slice(key.as_bytes())?; - Ok(key) -} +use std::env; + +use exun::*; +use hmac::{Hmac, Mac}; +use sha2::Sha256; + +/// This is a secret salt, needed for creating passwords. It's used as an extra +/// layer of security, on top of the salt that's already used. +pub fn pepper() -> Result, RawUnexpected> { + let pepper = env::var("SECRET_SALT")?; + let pepper = hex::decode(pepper)?; + Ok(pepper.into_boxed_slice()) +} + +/// The URL to the MySQL database +pub fn database_url() -> Result { + env::var("DATABASE_URL").unexpect() +} + +pub fn signing_key() -> Result, RawUnexpected> { + let key = env::var("PRIVATE_KEY")?; + let key = Hmac::::new_from_slice(key.as_bytes())?; + Ok(key) +} diff --git a/static/config/local.toml b/static/config/local.toml index ed6f9d0..d17967e 100644 --- a/static/config/local.toml +++ b/static/config/local.toml @@ -1,5 +1,5 @@ -# used to identify the issuer of JWTs -self_id = "LockDagger" - -# The URL which the server is hosted on -url = "http://localhost:8080" +# used to identify the issuer of JWTs +self_id = "LockDagger" + +# The URL which the server is hosted on +url = "http://localhost:8080" diff --git a/static/languages/en.ini b/static/languages/en.ini index 32adc51..e90b59e 100644 --- a/static/languages/en.ini +++ b/static/languages/en.ini @@ -1,20 +1,20 @@ -loginTitle = Log In -usernameLabel = Username -usernamePlaceholder = Enter your username -passwordLabel = Password -passwordPlaceholder = Enter your password -loginSubmitButton = Log In - -loginErrorMessage = Incorrect username or password. - -errorTitle = Error -errorHeader_invalidRequest = Invalid Request -errorMessage_invalidRequest = The client sent a bad request. -errorHeader_clientNotFound = Client Not Found -errorMessage_clientNotFound = The client gave an incorrect ID, so we cannot redirect to it. -errorHeader_missingRedirectUri = Missing Redirect URI -errorMessage_missingRedirectUri = There are many redirect URIs for the client, but the client did not specify which one to use. -errorHeader_invalidRedirectUri = Invalid Redirect URI -errorMessage_invalidRedirectUri = The client provided a redirect URI that it is not allowed to redirect to. -errorHeader_internalServerError = Server Error -errorMessage_internalServerError = An unexpected error occurred. +loginTitle = Log In +usernameLabel = Username +usernamePlaceholder = Enter your username +passwordLabel = Password +passwordPlaceholder = Enter your password +loginSubmitButton = Log In + +loginErrorMessage = Incorrect username or password. + +errorTitle = Error +errorHeader_invalidRequest = Invalid Request +errorMessage_invalidRequest = The client sent a bad request. +errorHeader_clientNotFound = Client Not Found +errorMessage_clientNotFound = The client gave an incorrect ID, so we cannot redirect to it. +errorHeader_missingRedirectUri = Missing Redirect URI +errorMessage_missingRedirectUri = There are many redirect URIs for the client, but the client did not specify which one to use. +errorHeader_invalidRedirectUri = Invalid Redirect URI +errorMessage_invalidRedirectUri = The client provided a redirect URI that it is not allowed to redirect to. +errorHeader_internalServerError = Server Error +errorMessage_internalServerError = An unexpected error occurred. diff --git a/static/scripts/tsconfig.json b/static/scripts/tsconfig.json index 9103384..40f6740 100644 --- a/static/scripts/tsconfig.json +++ b/static/scripts/tsconfig.json @@ -1,12 +1,12 @@ -{ - "compilerOptions": { - "strict": true, - "noFallthroughCasesInSwitch": true, - "exactOptionalPropertyTypes": true, - "noImplicitOverride": true, - "noImplicitReturns": true, - "noPropertyAccessFromIndexSignature": true, - "lib": ["DOM"], - "target": "ES6" - } +{ + "compilerOptions": { + "strict": true, + "noFallthroughCasesInSwitch": true, + "exactOptionalPropertyTypes": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noPropertyAccessFromIndexSignature": true, + "lib": ["DOM"], + "target": "ES6" + } } \ No newline at end of file diff --git a/static/templates/base.html b/static/templates/base.html index 021f95e..2096f78 100644 --- a/static/templates/base.html +++ b/static/templates/base.html @@ -1,16 +1,16 @@ - - - - {% block title %}{% endblock title %} - - - - {% block head %}{% endblock head %} - - - {% block content %}{% endblock content %} -
- -
- - + + + + {% block title %}{% endblock title %} + + + + {% block head %}{% endblock head %} + + + {% block content %}{% endblock content %} +
+ +
+ + diff --git a/static/templates/error.html b/static/templates/error.html index 4fbe87f..c3a0dec 100644 --- a/static/templates/error.html +++ b/static/templates/error.html @@ -1,7 +1,7 @@ -{% extends "base.html" %} -{% block title %}{{ msg(key="errorTitle") }}{% endblock title %} - -{% block content %} -

{{ msg(key=errorHeader) }}

-

{{ msg(key=errorMessage) }}

-{% endblock content %} +{% extends "base.html" %} +{% block title %}{{ msg(key="errorTitle") }}{% endblock title %} + +{% block content %} +

{{ msg(key=errorHeader) }}

+

{{ msg(key=errorMessage) }}

+{% endblock content %} diff --git a/static/templates/login.html b/static/templates/login.html index 325dc14..18ec017 100644 --- a/static/templates/login.html +++ b/static/templates/login.html @@ -1,14 +1,14 @@ -{% extends "base.html" %} -{% block title %}{{ msg(key="loginTitle") }}{% endblock title %} -{% block content %} -
- {% if errorMessage %} -

{{ msg(key=errorMessage) }}

- {% endif %} - - - - - -
+{% extends "base.html" %} +{% block title %}{{ msg(key="loginTitle") }}{% endblock title %} +{% block content %} +
+ {% if errorMessage %} +

{{ msg(key=errorMessage) }}

+ {% endif %} + + + + + +
{% endblock content %} \ No newline at end of file -- cgit v1.2.3