summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMica White <botahamec@outlook.com>2025-12-08 20:08:21 -0500
committerMica White <botahamec@outlook.com>2025-12-08 20:08:21 -0500
commit608ce1d9910cd68ce825838ea313e02c598f908e (patch)
tree0bd4ad26f86e5c873f97308983112b0ffe593df3
parent93fd2e82e8fdc5ee62739053385f8ccffc660f02 (diff)
-rw-r--r--.gitignore4
-rw-r--r--Cargo.lock5648
-rw-r--r--Cargo.toml76
-rw-r--r--rustfmt.toml6
-rw-r--r--sqlx-data.json2136
-rw-r--r--src/api/clients.rs966
-rw-r--r--src/api/liveops.rs22
-rw-r--r--src/api/mod.rs26
-rw-r--r--src/api/oauth.rs1852
-rw-r--r--src/api/ops.rs140
-rw-r--r--src/api/users.rs544
-rw-r--r--src/main.rs216
-rw-r--r--src/models/client.rs330
-rw-r--r--src/models/mod.rs4
-rw-r--r--src/models/user.rs98
-rw-r--r--src/resources/languages.rs134
-rw-r--r--src/resources/mod.rs8
-rw-r--r--src/resources/scripts.rs76
-rw-r--r--src/resources/style.rs108
-rw-r--r--src/resources/templates.rs202
-rw-r--r--src/scopes/admin.rs56
-rw-r--r--src/scopes/mod.rs256
-rw-r--r--src/services/authorization.rs164
-rw-r--r--src/services/config.rs148
-rw-r--r--src/services/crypto.rs194
-rw-r--r--src/services/db.rs30
-rw-r--r--src/services/db/client.rs784
-rw-r--r--src/services/db/jwt.rs398
-rw-r--r--src/services/db/user.rs472
-rw-r--r--src/services/id.rs54
-rw-r--r--src/services/jwt.rs582
-rw-r--r--src/services/mod.rs14
-rw-r--r--src/services/secrets.rs48
-rw-r--r--static/config/local.toml10
-rw-r--r--static/languages/en.ini40
-rw-r--r--static/scripts/tsconfig.json22
-rw-r--r--static/templates/base.html32
-rw-r--r--static/templates/error.html14
-rw-r--r--static/templates/login.html26
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<str>,
- client_type: ClientType,
- allowed_scopes: Box<[Box<str>]>,
- default_scopes: Option<Box<[Box<str>]>>,
- is_trusted: bool,
-}
-
-impl From<ClientRow> 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<Uuid>,
- db: web::Data<MySqlPool>,
-) -> Result<HttpResponse, ClientNotFound> {
- 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!("</clients/{client_id}/redirect-uris>; 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<Uuid>,
- db: web::Data<MySqlPool>,
-) -> Result<HttpResponse, ClientNotFound> {
- 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<Uuid>,
- db: web::Data<MySqlPool>,
-) -> Result<HttpResponse, ClientNotFound> {
- 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<Uuid>,
- db: web::Data<MySqlPool>,
-) -> Result<HttpResponse, ClientNotFound> {
- 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<Uuid>,
- db: web::Data<MySqlPool>,
-) -> Result<HttpResponse, ClientNotFound> {
- 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::<Box<[&str]>>();
-
- Ok(HttpResponse::Ok().json(allowed_scopes))
-}
-
-#[get("/{client_id}/default-scopes")]
-async fn get_client_default_scopes(
- client_id: web::Path<Uuid>,
- db: web::Data<MySqlPool>,
-) -> Result<HttpResponse, ClientNotFound> {
- 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::<Box<[Box<str>]>>()
- });
-
- Ok(HttpResponse::Ok().json(default_scopes))
-}
-
-#[get("/{client_id}/is-trusted")]
-async fn get_client_is_trusted(
- client_id: web::Path<Uuid>,
- db: web::Data<MySqlPool>,
-) -> Result<HttpResponse, ClientNotFound> {
- 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<str>,
- ty: ClientType,
- redirect_uris: Box<[Url]>,
- secret: Option<Box<str>>,
- allowed_scopes: Box<[Box<str>]>,
- default_scopes: Option<Box<[Box<str>]>>,
- trusted: bool,
-}
-
-#[derive(Debug, Clone, Error)]
-#[error("The given client alias is already taken")]
-struct AliasTakenError {
- alias: Box<str>,
-}
-
-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<ClientRequest>,
- db: web::Data<MySqlPool>,
-) -> Result<HttpResponse, UpdateClientError> {
- 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<Uuid>,
- body: web::Json<ClientRequest>,
- db: web::Data<MySqlPool>,
-) -> Result<HttpResponse, UpdateClientError> {
- 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<Uuid>,
- body: web::Json<Box<str>>,
- db: web::Data<MySqlPool>,
-) -> Result<HttpResponse, UpdateClientError> {
- 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<Uuid>,
- body: web::Json<ClientType>,
- db: web::Data<MySqlPool>,
-) -> Result<HttpResponse, UpdateClientError> {
- 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<Uuid>,
- body: web::Json<Box<[Box<str>]>>,
- db: web::Data<MySqlPool>,
-) -> Result<HttpResponse, UpdateClientError> {
- 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<Uuid>,
- body: web::Json<Option<Box<[Box<str>]>>>,
- db: web::Data<MySqlPool>,
-) -> Result<HttpResponse, UpdateClientError> {
- 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<Uuid>,
- body: web::Json<bool>,
- db: web::Data<MySqlPool>,
-) -> Result<HttpResponse, UpdateClientError> {
- 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<Uuid>,
- body: web::Json<Box<[Url]>>,
- db: web::Data<MySqlPool>,
-) -> Result<HttpResponse, UpdateClientError> {
- 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<Uuid>,
- body: web::Json<Option<Box<str>>>,
- db: web::Data<MySqlPool>,
-) -> Result<HttpResponse, UpdateClientError> {
- 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<str>,
+ client_type: ClientType,
+ allowed_scopes: Box<[Box<str>]>,
+ default_scopes: Option<Box<[Box<str>]>>,
+ is_trusted: bool,
+}
+
+impl From<ClientRow> 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<Uuid>,
+ db: web::Data<MySqlPool>,
+) -> Result<HttpResponse, ClientNotFound> {
+ 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!("</clients/{client_id}/redirect-uris>; 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<Uuid>,
+ db: web::Data<MySqlPool>,
+) -> Result<HttpResponse, ClientNotFound> {
+ 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<Uuid>,
+ db: web::Data<MySqlPool>,
+) -> Result<HttpResponse, ClientNotFound> {
+ 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<Uuid>,
+ db: web::Data<MySqlPool>,
+) -> Result<HttpResponse, ClientNotFound> {
+ 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<Uuid>,
+ db: web::Data<MySqlPool>,
+) -> Result<HttpResponse, ClientNotFound> {
+ 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::<Box<[&str]>>();
+
+ Ok(HttpResponse::Ok().json(allowed_scopes))
+}
+
+#[get("/{client_id}/default-scopes")]
+async fn get_client_default_scopes(
+ client_id: web::Path<Uuid>,
+ db: web::Data<MySqlPool>,
+) -> Result<HttpResponse, ClientNotFound> {
+ 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::<Box<[Box<str>]>>()
+ });
+
+ Ok(HttpResponse::Ok().json(default_scopes))
+}
+
+#[get("/{client_id}/is-trusted")]
+async fn get_client_is_trusted(
+ client_id: web::Path<Uuid>,
+ db: web::Data<MySqlPool>,
+) -> Result<HttpResponse, ClientNotFound> {
+ 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<str>,
+ ty: ClientType,
+ redirect_uris: Box<[Url]>,
+ secret: Option<Box<str>>,
+ allowed_scopes: Box<[Box<str>]>,
+ default_scopes: Option<Box<[Box<str>]>>,
+ trusted: bool,
+}
+
+#[derive(Debug, Clone, Error)]
+#[error("The given client alias is already taken")]
+struct AliasTakenError {
+ alias: Box<str>,
+}
+
+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<ClientRequest>,
+ db: web::Data<MySqlPool>,
+) -> Result<HttpResponse, UpdateClientError> {
+ 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<Uuid>,
+ body: web::Json<ClientRequest>,
+ db: web::Data<MySqlPool>,
+) -> Result<HttpResponse, UpdateClientError> {
+ 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<Uuid>,
+ body: web::Json<Box<str>>,
+ db: web::Data<MySqlPool>,
+) -> Result<HttpResponse, UpdateClientError> {
+ 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<Uuid>,
+ body: web::Json<ClientType>,
+ db: web::Data<MySqlPool>,
+) -> Result<HttpResponse, UpdateClientError> {
+ 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<Uuid>,
+ body: web::Json<Box<[Box<str>]>>,
+ db: web::Data<MySqlPool>,
+) -> Result<HttpResponse, UpdateClientError> {
+ 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<Uuid>,
+ body: web::Json<Option<Box<[Box<str>]>>>,
+ db: web::Data<MySqlPool>,
+) -> Result<HttpResponse, UpdateClientError> {
+ 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<Uuid>,
+ body: web::Json<bool>,
+ db: web::Data<MySqlPool>,
+) -> Result<HttpResponse, UpdateClientError> {
+ 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<Uuid>,
+ body: web::Json<Box<[Url]>>,
+ db: web::Data<MySqlPool>,
+) -> Result<HttpResponse, UpdateClientError> {
+ 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<Uuid>,
+ body: web::Json<Option<Box<str>>>,
+ db: web::Data<MySqlPool>,
+) -> Result<HttpResponse, UpdateClientError> {
+ 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 = "<!DOCTYPE html><html><head><title>Internal Server Error</title></head><body>Internal Server Error</body></html>";
-
-#[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<str>,
- redirect_uri: Option<Url>,
- scope: Option<Box<str>>,
- state: Option<Box<str>>,
-}
-
-#[derive(Clone, Deserialize)]
-struct AuthorizeCredentials {
- username: Box<str>,
- password: Box<str>,
-}
-
-#[derive(Clone, Serialize)]
-struct AuthCodeResponse {
- code: Box<str>,
- state: Option<Box<str>>,
-}
-
-#[derive(Clone, Serialize)]
-struct AuthTokenResponse {
- access_token: Box<str>,
- token_type: &'static str,
- expires_in: i64,
- scope: Box<str>,
- state: Option<Box<str>>,
-}
-
-#[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<str>,
- // TODO error uri
- state: Option<Box<str>>,
- #[serde(skip)]
- redirect_uri: Url,
-}
-
-impl AuthorizeError {
- fn no_scope(redirect_uri: Url, state: Option<Box<str>>) -> 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<Box<str>>) -> 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<Box<str>>) -> 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<Box<str>>) -> Self {
- Self {
- error: AuthorizeErrorType::ServerError,
- error_description: "An unexpected error occurred".into(),
- state,
- redirect_uri,
- }
- }
-}
-
-impl ResponseError for AuthorizeError {
- fn error_response(&self) -> HttpResponse<actix_web::body::BoxBody> {
- 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<String, RawUnexpected> {
- // 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<Url>,
- db: &MySqlPool,
- client_id: Uuid,
-) -> Result<Url, Expect<templates::ErrorPage>> {
- 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<Box<str>>,
- db: &MySqlPool,
- client_id: Uuid,
- redirect_uri: &Url,
- state: &Option<Box<str>>,
-) -> Result<Box<str>, Expect<AuthorizeError>> {
- 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<Option<Uuid>, 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<MySqlPool>,
- req: web::Query<AuthorizationParameters>,
- credentials: web::Json<AuthorizeCredentials>,
- tera: web::Data<Tera>,
- translations: web::Data<languages::Translations>,
-) -> Result<HttpResponse, AuthorizeError> {
- // 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<MySqlPool>,
- tera: web::Data<Tera>,
- translations: web::Data<languages::Translations>,
- request: HttpRequest,
-) -> Result<HttpResponse, AuthorizeError> {
- 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::<AuthorizationParameters>(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, &params.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(&params.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 = &params.state;
- let internal_server_error =
- AuthorizeError::internal_server_error(redirect_uri.clone(), state.clone());
-
- // verify scope
- let _ = match get_scope(&params.scope, db, client_id, &redirect_uri, &params.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, &params, 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<str>,
- redirect_uri: Url,
- #[serde(rename = "client_id")]
- client_alias: Box<str>,
- },
- Password {
- username: Box<str>,
- password: Box<str>,
- scope: Option<Box<str>>,
- },
- ClientCredentials {
- scope: Option<Box<str>>,
- },
- RefreshToken {
- refresh_token: Box<str>,
- scope: Option<Box<str>>,
- },
- #[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<str>,
- token_type: Box<str>,
- expires_in: i64,
- refresh_token: Option<Box<str>>,
- scope: Box<str>,
-}
-
-#[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<str>,
- // 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<actix_web::body::BoxBody> {
- 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<MySqlPool>,
- req: web::Bytes,
- authorization: Option<web::Header<authorization::BasicAuthorization>>,
-) -> HttpResponse {
- // TODO protect against brute force attacks
- let db = db.get_ref();
- let request = serde_json::from_slice::<TokenRequest>(&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<Uuid>;
- 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 = "<!DOCTYPE html><html><head><title>Internal Server Error</title></head><body>Internal Server Error</body></html>";
+
+#[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<str>,
+ redirect_uri: Option<Url>,
+ scope: Option<Box<str>>,
+ state: Option<Box<str>>,
+}
+
+#[derive(Clone, Deserialize)]
+struct AuthorizeCredentials {
+ username: Box<str>,
+ password: Box<str>,
+}
+
+#[derive(Clone, Serialize)]
+struct AuthCodeResponse {
+ code: Box<str>,
+ state: Option<Box<str>>,
+}
+
+#[derive(Clone, Serialize)]
+struct AuthTokenResponse {
+ access_token: Box<str>,
+ token_type: &'static str,
+ expires_in: i64,
+ scope: Box<str>,
+ state: Option<Box<str>>,
+}
+
+#[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<str>,
+ // TODO error uri
+ state: Option<Box<str>>,
+ #[serde(skip)]
+ redirect_uri: Url,
+}
+
+impl AuthorizeError {
+ fn no_scope(redirect_uri: Url, state: Option<Box<str>>) -> 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<Box<str>>) -> 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<Box<str>>) -> 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<Box<str>>) -> Self {
+ Self {
+ error: AuthorizeErrorType::ServerError,
+ error_description: "An unexpected error occurred".into(),
+ state,
+ redirect_uri,
+ }
+ }
+}
+
+impl ResponseError for AuthorizeError {
+ fn error_response(&self) -> HttpResponse<actix_web::body::BoxBody> {
+ 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<String, RawUnexpected> {
+ // 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<Url>,
+ db: &MySqlPool,
+ client_id: Uuid,
+) -> Result<Url, Expect<templates::ErrorPage>> {
+ 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<Box<str>>,
+ db: &MySqlPool,
+ client_id: Uuid,
+ redirect_uri: &Url,
+ state: &Option<Box<str>>,
+) -> Result<Box<str>, Expect<AuthorizeError>> {
+ 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<Option<Uuid>, 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<MySqlPool>,
+ req: web::Query<AuthorizationParameters>,
+ credentials: web::Json<AuthorizeCredentials>,
+ tera: web::Data<Tera>,
+ translations: web::Data<languages::Translations>,
+) -> Result<HttpResponse, AuthorizeError> {
+ // 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<MySqlPool>,
+ tera: web::Data<Tera>,
+ translations: web::Data<languages::Translations>,
+ request: HttpRequest,
+) -> Result<HttpResponse, AuthorizeError> {
+ 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::<AuthorizationParameters>(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, &params.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(&params.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 = &params.state;
+ let internal_server_error =
+ AuthorizeError::internal_server_error(redirect_uri.clone(), state.clone());
+
+ // verify scope
+ let _ = match get_scope(&params.scope, db, client_id, &redirect_uri, &params.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, &params, 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<str>,
+ redirect_uri: Url,
+ #[serde(rename = "client_id")]
+ client_alias: Box<str>,
+ },
+ Password {
+ username: Box<str>,
+ password: Box<str>,
+ scope: Option<Box<str>>,
+ },
+ ClientCredentials {
+ scope: Option<Box<str>>,
+ },
+ RefreshToken {
+ refresh_token: Box<str>,
+ scope: Option<Box<str>>,
+ },
+ #[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<str>,
+ token_type: Box<str>,
+ expires_in: i64,
+ refresh_token: Option<Box<str>>,
+ scope: Box<str>,
+}
+
+#[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<str>,
+ // 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<actix_web::body::BoxBody> {
+ 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<MySqlPool>,
+ req: web::Bytes,
+ authorization: Option<web::Header<authorization::BasicAuthorization>>,
+) -> HttpResponse {
+ // TODO protect against brute force attacks
+ let db = db.get_ref();
+ let request = serde_json::from_slice::<TokenRequest>(&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<Uuid>;
+ 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<str>,
- password: Box<str>,
-}
-
-/// 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<str> },
- #[error("The given password is incorrect")]
- IncorrectPassword { username: Box<str> },
-}
-
-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<LoginRequest>,
- conn: web::Data<MySqlPool>,
-) -> Result<HttpResponse, LoginFailure> {
- 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<str>,
+ password: Box<str>,
+}
+
+/// 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<str> },
+ #[error("The given password is incorrect")]
+ IncorrectPassword { username: Box<str> },
+}
+
+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<LoginRequest>,
+ conn: web::Data<MySqlPool>,
+) -> Result<HttpResponse, LoginFailure> {
+ 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<str>,
-}
-
-impl From<User> 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<Box<str>>,
- limit: Option<u32>,
- offset: Option<u32>,
-}
-
-#[get("")]
-async fn search_users(params: web::Query<SearchUsers>, conn: web::Data<MySqlPool>) -> 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<Uuid>,
- conn: web::Data<MySqlPool>,
-) -> Result<HttpResponse, UserNotFoundError> {
- 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<Uuid>,
- conn: web::Data<MySqlPool>,
-) -> Result<HttpResponse, UserNotFoundError> {
- 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<str>,
- password: Box<str>,
-}
-
-#[derive(Debug, Clone, Error)]
-#[error("An account with the given username already exists.")]
-struct UsernameTakenError {
- username: Box<str>,
-}
-
-impl ResponseError for UsernameTakenError {
- fn status_code(&self) -> StatusCode {
- StatusCode::CONFLICT
- }
-}
-
-#[post("")]
-async fn create_user(
- body: web::Json<UserRequest>,
- conn: web::Data<MySqlPool>,
-) -> Result<HttpResponse, UsernameTakenError> {
- 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<Uuid>,
- body: web::Json<UserRequest>,
- conn: web::Data<MySqlPool>,
-) -> Result<HttpResponse, UpdateUserError> {
- 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<Uuid>,
- body: web::Json<Box<str>>,
- conn: web::Data<MySqlPool>,
-) -> Result<HttpResponse, UpdateUserError> {
- 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<Uuid>,
- body: web::Json<Box<str>>,
- conn: web::Data<MySqlPool>,
-) -> Result<HttpResponse, UserNotFoundError> {
- 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<str>,
+}
+
+impl From<User> 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<Box<str>>,
+ limit: Option<u32>,
+ offset: Option<u32>,
+}
+
+#[get("")]
+async fn search_users(params: web::Query<SearchUsers>, conn: web::Data<MySqlPool>) -> 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<Uuid>,
+ conn: web::Data<MySqlPool>,
+) -> Result<HttpResponse, UserNotFoundError> {
+ 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<Uuid>,
+ conn: web::Data<MySqlPool>,
+) -> Result<HttpResponse, UserNotFoundError> {
+ 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<str>,
+ password: Box<str>,
+}
+
+#[derive(Debug, Clone, Error)]
+#[error("An account with the given username already exists.")]
+struct UsernameTakenError {
+ username: Box<str>,
+}
+
+impl ResponseError for UsernameTakenError {
+ fn status_code(&self) -> StatusCode {
+ StatusCode::CONFLICT
+ }
+}
+
+#[post("")]
+async fn create_user(
+ body: web::Json<UserRequest>,
+ conn: web::Data<MySqlPool>,
+) -> Result<HttpResponse, UsernameTakenError> {
+ 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<Uuid>,
+ body: web::Json<UserRequest>,
+ conn: web::Data<MySqlPool>,
+) -> Result<HttpResponse, UpdateUserError> {
+ 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<Uuid>,
+ body: web::Json<Box<str>>,
+ conn: web::Data<MySqlPool>,
+) -> Result<HttpResponse, UpdateUserError> {
+ 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<Uuid>,
+ body: web::Json<Box<str>>,
+ conn: web::Data<MySqlPool>,
+) -> Result<HttpResponse, UserNotFoundError> {
+ 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<B>(
- mut res: dev::ServiceResponse,
-) -> actix_web::Result<ErrorHandlerResponse<B>> {
- 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<B>(
+ mut res: dev::ServiceResponse,
+) -> actix_web::Result<ErrorHandlerResponse<B>> {
+ 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<str>,
- secret: Option<PasswordHash>,
- allowed_scopes: Box<[Box<str>]>,
- default_scopes: Option<Box<[Box<str>]>>,
- 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<H: std::hash::Hasher>(&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<str>]>,
- default_scopes: Option<Box<[Box<str>]>>,
- redirect_uris: &[Url],
- trusted: bool,
- ) -> Result<Self, Expect<CreateClientError>> {
- 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<u8> {
- self.secret.as_ref().map(|s| s.version())
- }
-
- pub fn allowed_scopes(&self) -> String {
- self.allowed_scopes.join(" ")
- }
-
- pub fn default_scopes(&self) -> Option<String> {
- self.default_scopes.clone().map(|s| s.join(" "))
- }
-
- pub fn is_trusted(&self) -> bool {
- self.trusted
- }
-
- pub fn check_secret(&self, secret: &str) -> Option<Result<bool, RawUnexpected>> {
- 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<str>,
+ secret: Option<PasswordHash>,
+ allowed_scopes: Box<[Box<str>]>,
+ default_scopes: Option<Box<[Box<str>]>>,
+ 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<H: std::hash::Hasher>(&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<str>]>,
+ default_scopes: Option<Box<[Box<str>]>>,
+ redirect_uris: &[Url],
+ trusted: bool,
+ ) -> Result<Self, Expect<CreateClientError>> {
+ 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<u8> {
+ self.secret.as_ref().map(|s| s.version())
+ }
+
+ pub fn allowed_scopes(&self) -> String {
+ self.allowed_scopes.join(" ")
+ }
+
+ pub fn default_scopes(&self) -> Option<String> {
+ self.default_scopes.clone().map(|s| s.join(" "))
+ }
+
+ pub fn is_trusted(&self) -> bool {
+ self.trusted
+ }
+
+ pub fn check_secret(&self, secret: &str) -> Option<Result<bool, RawUnexpected>> {
+ 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<str>,
- 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<H: std::hash::Hasher>(&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<bool, RawUnexpected> {
- 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<str>,
+ 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<H: std::hash::Hasher>(&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<bool, RawUnexpected> {
+ 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<Language, Properties>,
-}
-
-pub fn initialize() -> Result<Translations, RawUnexpected> {
- 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<String> {
- 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<Translations>) -> HttpResponse {
- HttpResponse::Ok().json(
- translations
- .languages()
- .into_iter()
- .map(|l| l.as_str())
- .collect::<Box<[&str]>>(),
- )
-}
-
-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<Language, Properties>,
+}
+
+pub fn initialize() -> Result<Translations, RawUnexpected> {
+ 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<String> {
+ 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<Translations>) -> HttpResponse {
+ HttpResponse::Ok().json(
+ translations
+ .languages()
+ .into_iter()
+ .map(|l| l.as_str())
+ .collect::<Box<[&str]>>(),
+ )
+}
+
+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<Path>),
-}
-
-impl ResponseError for LoadScriptError {
- fn status_code(&self) -> StatusCode {
- match self {
- Self::FileNotFound(..) => StatusCode::NOT_FOUND,
- }
- }
-}
-
-fn load(script: &str) -> Result<String, Expect<LoadScriptError>> {
- 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<Box<str>>) -> Result<HttpResponse, LoadScriptError> {
- 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<Path>),
+}
+
+impl ResponseError for LoadScriptError {
+ fn status_code(&self) -> StatusCode {
+ match self {
+ Self::FileNotFound(..) => StatusCode::NOT_FOUND,
+ }
+ }
+}
+
+fn load(script: &str) -> Result<String, Expect<LoadScriptError>> {
+ 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<Box<str>>) -> Result<HttpResponse, LoadScriptError> {
+ 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<Path>),
-}
-
-impl ResponseError for LoadStyleError {
- fn status_code(&self) -> StatusCode {
- match self {
- Self::FileNotFound(..) => StatusCode::NOT_FOUND,
- }
- }
-}
-
-pub fn load(stylesheet: &str) -> Result<String, Expect<LoadStyleError>> {
- 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<Box<str>>) -> Result<HttpResponse, LoadStyleError> {
- 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<Path>),
+}
+
+impl ResponseError for LoadStyleError {
+ fn status_code(&self) -> StatusCode {
+ match self {
+ Self::FileNotFound(..) => StatusCode::NOT_FOUND,
+ }
+ }
+}
+
+pub fn load(stylesheet: &str) -> Result<String, Expect<LoadStyleError>> {
+ 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<Box<str>>) -> Result<HttpResponse, LoadStyleError> {
+ 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<String, Value>| -> tera::Result<Value> {
- 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<Tera, RawUnexpected> {
- 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<Tera> {
- 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<String, RawUnexpected> {
- 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<String, RawUnexpected> {
- 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<String, RawUnexpected> {
- 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<String, Value>| -> tera::Result<Value> {
+ 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<Tera, RawUnexpected> {
+ 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<Tera> {
+ 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<String, RawUnexpected> {
+ 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<String, RawUnexpected> {
+ 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<String, RawUnexpected> {
+ 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<Self, Box<str>> {
- Ok(Self)
- }
-
- fn has_user_permission(&self, _: &User, _: &Action<User>) -> bool {
- true
- }
-
- fn has_client_permission(&self, _: &User, _: &Action<Client>) -> 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<Self, Box<str>> {
+ Ok(Self)
+ }
+
+ fn has_user_permission(&self, _: &User, _: &Action<User>) -> bool {
+ true
+ }
+
+ fn has_client_permission(&self, _: &User, _: &Action<Client>) -> 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<T> {
- 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<Self, Box<str>>
- 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<User>) -> 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<Client>) -> bool;
-}
-
-pub struct ParseScopeError {
- scope: Box<str>,
- 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<str>),
- InvalidModifiers(Box<str>),
-}
-
-fn parse_scope(scope: &str) -> Result<Box<dyn Scope>, 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<Vec<Box<dyn Scope>>, ParseScopeError> {
- scopes
- .split_whitespace()
- .map(|scope| parse_scope(scope))
- .collect()
-}
-
-fn parse_scopes_errors(
- results: &[Result<Box<dyn Scope>, 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<User>,
- client_scopes: &str,
-) -> Result<bool, ParseScopeError> {
- 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>,
- client_scopes: &str,
-) -> Result<bool, ParseScopeError> {
- 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<T> {
+ 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<Self, Box<str>>
+ 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<User>) -> 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<Client>) -> bool;
+}
+
+pub struct ParseScopeError {
+ scope: Box<str>,
+ 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<str>),
+ InvalidModifiers(Box<str>),
+}
+
+fn parse_scope(scope: &str) -> Result<Box<dyn Scope>, 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<Vec<Box<dyn Scope>>, ParseScopeError> {
+ scopes
+ .split_whitespace()
+ .map(|scope| parse_scope(scope))
+ .collect()
+}
+
+fn parse_scopes_errors(
+ results: &[Result<Box<dyn Scope>, 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<User>,
+ client_scopes: &str,
+) -> Result<bool, ParseScopeError> {
+ 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>,
+ client_scopes: &str,
+) -> Result<bool, ParseScopeError> {
+ 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<str>,
- password: Box<str>,
-}
-
-impl TryIntoHeaderValue for BasicAuthorization {
- type Error = InvalidHeaderValue;
-
- fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
- 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<M: actix_web::HttpMessage>(msg: &M) -> Result<Self, actix_web::error::ParseError> {
- 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<str>,
+ password: Box<str>,
+}
+
+impl TryIntoHeaderValue for BasicAuthorization {
+ type Error = InvalidHeaderValue;
+
+ fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
+ 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<M: actix_web::HttpMessage>(msg: &M) -> Result<Self, actix_web::error::ParseError> {
+ 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<Environment> = RwLock::new(Environment::Local);
-
-#[derive(Debug, Clone, Deserialize)]
-pub struct Config {
- pub id: Box<str>,
- pub url: Url,
-}
-
-pub fn get_config() -> Result<Config, RawUnexpected> {
- 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<str>,
-}
-
-impl FromStr for Environment {
- type Err = ParseEnvironmentError;
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- 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<Environment> = RwLock::new(Environment::Local);
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct Config {
+ pub id: Box<str>,
+ pub url: Url,
+}
+
+pub fn get_config() -> Result<Config, RawUnexpected> {
+ 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<str>,
+}
+
+impl FromStr for Environment {
+ type Err = ParseEnvironmentError;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ 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<H: std::hash::Hasher>(&self, state: &mut H) {
- state.write(&self.hash)
- }
-}
-
-impl PasswordHash {
- /// Hash a password using Argon2
- pub fn new(password: &str) -> Result<Self, RawUnexpected> {
- 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<bool, RawUnexpected> {
- 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<H: std::hash::Hasher>(&self, state: &mut H) {
+ state.write(&self.hash)
+ }
+}
+
+impl PasswordHash {
+ /// Hash a password using Argon2
+ pub fn new(password: &str) -> Result<Self, RawUnexpected> {
+ 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<bool, RawUnexpected> {
+ 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, RawUnexpected> {
- 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, RawUnexpected> {
+ 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<String>,
- pub is_trusted: bool,
-}
-
-#[derive(Clone, FromRow)]
-struct HashRow {
- secret_hash: Option<Vec<u8>>,
- secret_salt: Option<Vec<u8>>,
- secret_version: Option<u32>,
-}
-
-pub async fn client_id_exists<'c>(
- executor: impl Executor<'c, Database = MySql>,
- id: Uuid,
-) -> Result<bool, RawUnexpected> {
- 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<bool, RawUnexpected> {
- 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<Option<Uuid>, 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<Option<ClientRow>, 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<Option<Box<str>>, 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<Option<ClientType>, 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<Option<Box<str>>, 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<Option<Option<Box<str>>>, 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<Option<PasswordHash>, 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<Option<bool>, 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<Box<[Url]>, 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<bool, RawUnexpected> {
- 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<MySqlQueryResult, sqlx::Error> {
- 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<MySqlQueryResult, sqlx::Error> {
- 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<MySqlQueryResult, sqlx::Error> {
- 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<String>,
-) -> Result<MySqlQueryResult, sqlx::Error> {
- 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<MySqlQueryResult, sqlx::Error> {
- 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<PasswordHash>,
-) -> Result<MySqlQueryResult, sqlx::Error> {
- 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<String>,
+ pub is_trusted: bool,
+}
+
+#[derive(Clone, FromRow)]
+struct HashRow {
+ secret_hash: Option<Vec<u8>>,
+ secret_salt: Option<Vec<u8>>,
+ secret_version: Option<u32>,
+}
+
+pub async fn client_id_exists<'c>(
+ executor: impl Executor<'c, Database = MySql>,
+ id: Uuid,
+) -> Result<bool, RawUnexpected> {
+ 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<bool, RawUnexpected> {
+ 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<Option<Uuid>, 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<Option<ClientRow>, 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<Option<Box<str>>, 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<Option<ClientType>, 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<Option<Box<str>>, 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<Option<Option<Box<str>>>, 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<Option<PasswordHash>, 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<Option<bool>, 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<Box<[Url]>, 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<bool, RawUnexpected> {
+ 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<MySqlQueryResult, sqlx::Error> {
+ 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<MySqlQueryResult, sqlx::Error> {
+ 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<MySqlQueryResult, sqlx::Error> {
+ 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<String>,
+) -> Result<MySqlQueryResult, sqlx::Error> {
+ 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<MySqlQueryResult, sqlx::Error> {
+ 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<PasswordHash>,
+) -> Result<MySqlQueryResult, sqlx::Error> {
+ 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<bool, RawUnexpected> {
- 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<bool, RawUnexpected> {
- 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<bool, RawUnexpected> {
- 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<bool, RawUnexpected> {
- 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<Utc>,
-) -> 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<Uuid>,
- exp: DateTime<Utc>,
-) -> 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<Uuid>,
- exp: DateTime<Utc>,
-) -> 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<bool, RawUnexpected> {
- 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<bool, RawUnexpected> {
- 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<bool, RawUnexpected> {
- 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<bool, RawUnexpected> {
- 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<bool, RawUnexpected> {
+ 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<bool, RawUnexpected> {
+ 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<bool, RawUnexpected> {
+ 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<bool, RawUnexpected> {
+ 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<Utc>,
+) -> 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<Uuid>,
+ exp: DateTime<Utc>,
+) -> 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<Uuid>,
+ exp: DateTime<Utc>,
+) -> 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<bool, RawUnexpected> {
+ 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<bool, RawUnexpected> {
+ 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<bool, RawUnexpected> {
+ 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<bool, RawUnexpected> {
+ 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<u8>,
- password_salt: Vec<u8>,
- password_version: u32,
-}
-
-impl TryFrom<UserRow> for User {
- type Error = RawUnexpected;
-
- fn try_from(row: UserRow) -> Result<Self, Self::Error> {
- 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<bool, RawUnexpected> {
- 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<bool, RawUnexpected> {
- 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<Option<User>, 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<Option<User>, 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<Box<[User]>, 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::<Result<Box<[User]>, 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<Box<[User]>, 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::<Result<Box<[User]>, 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<Option<Box<str>>, 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<MySqlQueryResult, sqlx::Error> {
- 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<MySqlQueryResult, sqlx::Error> {
- 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<MySqlQueryResult, sqlx::Error> {
- 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<MySqlQueryResult, sqlx::Error> {
- 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<u8>,
+ password_salt: Vec<u8>,
+ password_version: u32,
+}
+
+impl TryFrom<UserRow> for User {
+ type Error = RawUnexpected;
+
+ fn try_from(row: UserRow) -> Result<Self, Self::Error> {
+ 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<bool, RawUnexpected> {
+ 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<bool, RawUnexpected> {
+ 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<Option<User>, 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<Option<User>, 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<Box<[User]>, 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::<Result<Box<[User]>, 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<Box<[User]>, 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::<Result<Box<[User]>, 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<Option<Box<str>>, 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<MySqlQueryResult, sqlx::Error> {
+ 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<MySqlQueryResult, sqlx::Error> {
+ 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<MySqlQueryResult, sqlx::Error> {
+ 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<MySqlQueryResult, sqlx::Error> {
+ 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<Output = Result<bool, RawUnexpected>>,
->(
- conn: E,
- unique_check: impl Fn(E, Uuid) -> F,
-) -> Result<Uuid, RawUnexpected> {
- 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<Output = Result<bool, RawUnexpected>>,
+>(
+ conn: E,
+ unique_check: impl Fn(E, Uuid) -> F,
+) -> Result<Uuid, RawUnexpected> {
+ 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<Utc>,
- #[serde(with = "ts_milliseconds_option")]
- nbf: Option<DateTime<Utc>>,
- #[serde(with = "ts_milliseconds")]
- iat: DateTime<Utc>,
- jti: Uuid,
- scope: Box<str>,
- client_id: Uuid,
- token_type: TokenType,
- auth_code_id: Option<Uuid>,
- redirect_uri: Option<Url>,
-}
-
-#[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<Self, RawUnexpected> {
- 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<Uuid>,
- self_id: Url,
- client_id: Uuid,
- sub: Uuid,
- duration: Duration,
- scopes: &str,
- ) -> Result<Self, RawUnexpected> {
- 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<Self, RawUnexpected> {
- 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<Self, RawUnexpected> {
- 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<Box<str>, 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<Uuid>,
-) -> Result<Claims, Expect<VerifyJwtError>> {
- 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<Claims, Expect<VerifyJwtError>> {
- 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<Claims, Expect<VerifyJwtError>> {
- 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<Uuid>,
-) -> Result<Claims, Expect<VerifyJwtError>> {
- 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<Utc>,
+ #[serde(with = "ts_milliseconds_option")]
+ nbf: Option<DateTime<Utc>>,
+ #[serde(with = "ts_milliseconds")]
+ iat: DateTime<Utc>,
+ jti: Uuid,
+ scope: Box<str>,
+ client_id: Uuid,
+ token_type: TokenType,
+ auth_code_id: Option<Uuid>,
+ redirect_uri: Option<Url>,
+}
+
+#[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<Self, RawUnexpected> {
+ 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<Uuid>,
+ self_id: Url,
+ client_id: Uuid,
+ sub: Uuid,
+ duration: Duration,
+ scopes: &str,
+ ) -> Result<Self, RawUnexpected> {
+ 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<Self, RawUnexpected> {
+ 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<Self, RawUnexpected> {
+ 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<Box<str>, 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<Uuid>,
+) -> Result<Claims, Expect<VerifyJwtError>> {
+ 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<Claims, Expect<VerifyJwtError>> {
+ 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<Claims, Expect<VerifyJwtError>> {
+ 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<Uuid>,
+) -> Result<Claims, Expect<VerifyJwtError>> {
+ 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<Box<[u8]>, 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<String, RawUnexpected> {
- env::var("DATABASE_URL").unexpect()
-}
-
-pub fn signing_key() -> Result<Hmac<Sha256>, RawUnexpected> {
- let key = env::var("PRIVATE_KEY")?;
- let key = Hmac::<Sha256>::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<Box<[u8]>, 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<String, RawUnexpected> {
+ env::var("DATABASE_URL").unexpect()
+}
+
+pub fn signing_key() -> Result<Hmac<Sha256>, RawUnexpected> {
+ let key = env::var("PRIVATE_KEY")?;
+ let key = Hmac::<Sha256>::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 @@
-<!DOCTYPE html>
-<html lang="{{lang}}">
- <head>
- <title>{% block title %}{% endblock title %}</title>
- <meta charset="utf-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1" />
- <link rel="stylesheet" href="/style.css" />
- {% block head %}{% endblock head %}
- </head>
- <body>
- {% block content %}{% endblock content %}
- <footer>
- <div id="copyright">&copy; 2023</div>
- </footer>
- </body>
-</html>
+<!DOCTYPE html>
+<html lang="{{lang}}">
+ <head>
+ <title>{% block title %}{% endblock title %}</title>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <link rel="stylesheet" href="/style.css" />
+ {% block head %}{% endblock head %}
+ </head>
+ <body>
+ {% block content %}{% endblock content %}
+ <footer>
+ <div id="copyright">&copy; 2023</div>
+ </footer>
+ </body>
+</html>
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 %}
-<p>{{ msg(key=errorHeader) }}</p>
-<p>{{ msg(key=errorMessage) }}</p>
-{% endblock content %}
+{% extends "base.html" %}
+{% block title %}{{ msg(key="errorTitle") }}{% endblock title %}
+
+{% block content %}
+<p>{{ msg(key=errorHeader) }}</p>
+<p>{{ msg(key=errorMessage) }}</p>
+{% 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 %}
-<form method="post" action="/authorize?{{params}}">
- {% if errorMessage %}
- <p>{{ msg(key=errorMessage) }}</p>
- {% endif %}
- <label for="username">{{ msg(key="usernameLabel") }}</label>
- <input id="username" type="text" name="username" tabindex="0" placeholder="{{ msg(key="usernamePlaceholder") }}" autofocus />
- <label for="password">{{ msg(key="passwordLabel") }}</label>
- <input id="password" type="password" name="password" tabindex="0" placeholder="{{ msg(key="passwordPlaceholder") }}" />
- <input type="submit" tabindex="0" name="login" value="{{ msg(key="loginSubmitButton") }}" />
-</form>
+{% extends "base.html" %}
+{% block title %}{{ msg(key="loginTitle") }}{% endblock title %}
+{% block content %}
+<form method="post" action="/authorize?{{params}}">
+ {% if errorMessage %}
+ <p>{{ msg(key=errorMessage) }}</p>
+ {% endif %}
+ <label for="username">{{ msg(key="usernameLabel") }}</label>
+ <input id="username" type="text" name="username" tabindex="0" placeholder="{{ msg(key="usernamePlaceholder") }}" autofocus />
+ <label for="password">{{ msg(key="passwordLabel") }}</label>
+ <input id="password" type="password" name="password" tabindex="0" placeholder="{{ msg(key="passwordPlaceholder") }}" />
+ <input type="submit" tabindex="0" name="login" value="{{ msg(key="loginSubmitButton") }}" />
+</form>
{% endblock content %} \ No newline at end of file